a11oy / static /3d /szl3d /szl3d_boot.js
betterwithage's picture
deploy(hf): sync szl-holdings/a11oy@main derived COPY set
a0f8a7d verified
Raw History Blame
12.6 kB
// SPDX-License-Identifier: Apache-2.0
// © 2026 Lutar, Stephen P. Jr. — SZL Holdings · ORCID 0009-0001-0110-4173 · Doctrine v11
//
// szl3d_boot.js — SHARED renderer factory for the holographic estate (Dev0 foundation).
//
// The other 9 surface devs import THIS so every tab shares one renderer setup:
// * WebGPU first (three r170 `three/webgpu` WebGPURenderer), graceful WebGL2 fallback.
// WebGPU is NOT production-safe on Linux/mobile yet (doctrine), so detection is
// defensive: we only use it when navigator.gpu yields a real adapter+device, and
// any failure falls back to WebGLRenderer with NO crash and an honest backend label.
// * Standard scene + perspective camera + OrbitControls + auto-resize.
// * Optional bloom/postprocessing pipeline for the 'holographic' look (UnrealBloomPass).
//
// 0 runtime CDN: all imports resolve through the page's importmap to /static/3d/vendor/.
// Honesty: this module renders pixels only. It NEVER invents data values — values come
// from szl3d_live.poll() and carry doctrine honesty labels (see szl3d_label.js).
//
// Import map every page must include (see VENDOR_MANIFEST.md):
// "three" -> /static/3d/vendor/three/three.module.min.js
// "three/webgpu" -> /static/3d/vendor/three/three.webgpu.min.js
// "three/addons/" -> /static/3d/vendor/three/addons/
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
export const SZL3D_VERSION = "1.0.0";
export const THREE_REVISION = THREE.REVISION;
// Reject if `p` has not settled within `ms`. Used to bound the WebGPU probe: a
// real adapter whose device acquisition hangs (seen on some Linux/driver combos)
// must NOT wedge boot() forever — we time out and fall back to WebGL2 instead.
function _withTimeout(p, ms, tag) {
return new Promise((resolve, reject) => {
let done = false;
const t = setTimeout(() => { if (!done) { done = true; reject(new Error((tag || "op") + " timed out after " + ms + "ms")); } }, ms);
Promise.resolve(p).then(
(v) => { if (!done) { done = true; clearTimeout(t); resolve(v); } },
(e) => { if (!done) { done = true; clearTimeout(t); reject(e); } },
);
});
}
// ---------------------------------------------------------------------------
// WebGPU capability probe. Returns a real WebGPURenderer (already init()'d) when
// the platform supports it, else null. Never throws, and never hangs: each await
// is bounded by a timeout so a stalled adapter/device falls back to WebGL2.
// ---------------------------------------------------------------------------
async function _tryWebGPU(canvas, opts) {
if (typeof navigator === "undefined" || !("gpu" in navigator)) return null;
const tmo = opts.webgpuTimeoutMs || 2500;
try {
const adapter = await _withTimeout(navigator.gpu.requestAdapter(), tmo, "requestAdapter");
if (!adapter) return null;
// three r170 ships WebGPURenderer in the dedicated webgpu build only.
const mod = await import("three/webgpu");
const WebGPURenderer = mod.WebGPURenderer || (mod.default && mod.default.WebGPURenderer);
if (!WebGPURenderer) return null;
const renderer = new WebGPURenderer({ canvas, antialias: opts.antialias !== false, alpha: !!opts.alpha });
await _withTimeout(renderer.init(), tmo, "renderer.init"); // acquires the device; hang/throw -> caught below
renderer._szlBackend = "webgpu";
return renderer;
} catch (e) {
// Honest fallback path — log once, do not crash the surface.
if (typeof console !== "undefined") console.warn("[szl3d] WebGPU unavailable, falling back to WebGL2:", e && e.message);
return null;
}
}
function _makeWebGL(canvas, opts) {
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: opts.antialias !== false,
alpha: !!opts.alpha,
powerPreference: opts.powerPreference || "high-performance",
});
renderer._szlBackend = "webgl2";
return renderer;
}
// ---------------------------------------------------------------------------
// boot(container, opts) -> Promise<Stage>
//
// container : DOM element (or canvas). If an element, a <canvas> is created inside.
// opts:
// webgpu (bool, default true) attempt WebGPU first
// forceWebGL (bool, default false) skip WebGPU entirely (used by the fallback selftest)
// bloom (bool|object, default false) enable the holographic bloom pass
// object: {strength, radius, threshold}
// background (THREE.Color|number|null) scene background (default 0x05070d)
// cameraFov, cameraNear, cameraFar, cameraPos:[x,y,z]
// antialias, alpha, powerPreference, pixelRatioCap (default 2)
// orbit (bool, default true) attach OrbitControls
//
// Returns a Stage object:
// { renderer, scene, camera, controls, backend, composer|null, bloomPass|null,
// start(loop?), stop(), resize(), render(), dispose(), setBloom(on), onFrame(fn) }
// ---------------------------------------------------------------------------
export async function boot(container, opts = {}) {
if (!container) throw new Error("szl3d.boot: container is required");
const isCanvas = (typeof HTMLCanvasElement !== "undefined") && (container instanceof HTMLCanvasElement);
const host = isCanvas ? container.parentElement || document.body : container;
const canvas = isCanvas ? container : document.createElement("canvas");
if (!isCanvas) {
canvas.style.display = "block";
canvas.style.width = "100%";
canvas.style.height = "100%";
host.appendChild(canvas);
}
let renderer = null;
if (!opts.forceWebGL && opts.webgpu !== false) {
renderer = await _tryWebGPU(canvas, opts);
}
if (!renderer) renderer = _makeWebGL(canvas, opts);
const backend = renderer._szlBackend;
const pixelRatioCap = opts.pixelRatioCap || 2;
renderer.setPixelRatio(Math.min((typeof window !== "undefined" ? window.devicePixelRatio : 1) || 1, pixelRatioCap));
const scene = new THREE.Scene();
if (opts.background !== null) {
scene.background = new THREE.Color(opts.background == null ? 0x05070d : opts.background);
}
const camera = new THREE.PerspectiveCamera(
opts.cameraFov || 55, 1, opts.cameraNear || 0.1, opts.cameraFar || 4000,
);
const cp = opts.cameraPos || [0, 6, 18];
camera.position.set(cp[0], cp[1], cp[2]);
camera.lookAt(0, 0, 0);
let controls = null;
if (opts.orbit !== false && typeof renderer.domElement !== "undefined") {
try {
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.08;
} catch (_) { controls = null; }
}
// Default holographic lighting so a freshly-booted scene is never pitch black.
const amb = new THREE.AmbientLight(0xffffff, 0.35);
const key = new THREE.DirectionalLight(0x8fd7ff, 1.1);
key.position.set(8, 16, 10);
scene.add(amb, key);
// -------- optional bloom/postprocessing (holographic glow) --------
// Bloom uses the WebGL EffectComposer. On the WebGPU backend three exposes a
// different post pipeline (PostProcessing/TSL); to keep the foundation honest and
// identical across the fallback we only wire EffectComposer bloom on WebGL2 and
// expose setBloom() as a no-op-safe toggle elsewhere.
let composer = null, bloomPass = null, renderPass = null, outputPass = null;
const bloomReq = !!opts.bloom;
async function _buildComposer() {
if (backend !== "webgl2") return; // see note above; WebGPU bloom is a per-surface opt-in TODO
const { EffectComposer } = await import("three/addons/postprocessing/EffectComposer.js");
const { RenderPass } = await import("three/addons/postprocessing/RenderPass.js");
const { UnrealBloomPass } = await import("three/addons/postprocessing/UnrealBloomPass.js");
const { OutputPass } = await import("three/addons/postprocessing/OutputPass.js");
composer = new EffectComposer(renderer);
renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
const b = (typeof opts.bloom === "object" && opts.bloom) || {};
bloomPass = new UnrealBloomPass(
new THREE.Vector2(1, 1),
b.strength != null ? b.strength : 0.9,
b.radius != null ? b.radius : 0.5,
b.threshold != null ? b.threshold : 0.15,
);
composer.addPass(bloomPass);
outputPass = new OutputPass();
composer.addPass(outputPass);
}
if (bloomReq) { try { await _buildComposer(); } catch (e) { if (console) console.warn("[szl3d] bloom unavailable:", e && e.message); } }
// -------- sizing --------
// `window.resize` alone misses layout-only changes (drawer expansion, dvh
// updates, split-pane resizing, font/tool-bar reflow). Observe the element
// that actually owns the canvas as well. The observer is deliberately
// coalesced to one animation-frame callback and `_size` deduplicates equal
// dimensions, preventing renderer.setSize() from creating a resize loop.
let _disposed = false;
let _lastWidth = 0, _lastHeight = 0;
let _observerRaf = 0;
let _resizeObserver = null;
function _dimensions() {
const w = Math.max(1, host.clientWidth || canvas.clientWidth || 1);
const h = Math.max(1, host.clientHeight || canvas.clientHeight || 1);
return { w, h };
}
function _size() {
if (_disposed) return;
const { w, h } = _dimensions();
_lastWidth = w;
_lastHeight = h;
renderer.setSize(w, h, false);
if (composer) composer.setSize(w, h);
if (bloomPass && bloomPass.resolution) bloomPass.resolution.set(w, h);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
function _queueObservedSize() {
if (_disposed || _observerRaf) return;
const { w, h } = _dimensions();
if (w === _lastWidth && h === _lastHeight) return;
if (typeof requestAnimationFrame === "function") {
_observerRaf = requestAnimationFrame(() => {
_observerRaf = 0;
_size();
});
return;
}
// Non-browser/headless fallback: ResizeObserver itself is already async.
_size();
}
const _frameCbs = [];
function onFrame(fn) { if (typeof fn === "function") _frameCbs.push(fn); }
function render() {
if (controls) controls.update();
for (let i = 0; i < _frameCbs.length; i++) { try { _frameCbs[i](stage); } catch (_) {} }
if (composer) composer.render();
else renderer.render(scene, camera);
}
let _raf = 0, _running = false;
function start(loopFn) {
if (loopFn) onFrame(loopFn);
_running = true;
const tick = () => {
if (!_running) return;
render();
_raf = (typeof requestAnimationFrame !== "undefined") ? requestAnimationFrame(tick) : 0;
};
tick();
}
function stop() {
_running = false;
if (_raf && typeof cancelAnimationFrame !== "undefined") cancelAnimationFrame(_raf);
_raf = 0;
}
function setBloom(on) {
if (bloomPass) bloomPass.enabled = !!on;
}
function dispose() {
_disposed = true;
stop();
if (_observerRaf && typeof cancelAnimationFrame !== "undefined") {
cancelAnimationFrame(_observerRaf);
_observerRaf = 0;
}
try { if (_resizeObserver) _resizeObserver.disconnect(); } catch (_) {}
_resizeObserver = null;
try { if (controls) controls.dispose(); } catch (_) {}
try { if (composer && composer.dispose) composer.dispose(); } catch (_) {}
try { renderer.dispose(); } catch (_) {}
if (typeof window !== "undefined") window.removeEventListener("resize", _size);
}
if (typeof window !== "undefined") window.addEventListener("resize", _size);
if (typeof ResizeObserver !== "undefined") {
try {
_resizeObserver = new ResizeObserver(_queueObservedSize);
_resizeObserver.observe(host);
} catch (_) {
// Unsupported/non-Element hosts retain the existing window resize path.
_resizeObserver = null;
}
}
_size();
const stage = {
renderer, scene, camera, controls, backend,
composer, bloomPass,
THREE,
start, stop, render, resize: _size, dispose, setBloom, onFrame,
hasBloom: () => !!bloomPass,
};
return stage;
}
// Synchronous capability hint (no device acquisition) for UIs that want to show
// the likely backend before boot resolves. Honest: presence of navigator.gpu is
// necessary-not-sufficient; boot() may still fall back.
export function probeBackend() {
const gpu = (typeof navigator !== "undefined") && ("gpu" in navigator);
return { webgpuLikely: !!gpu, three: THREE.REVISION, szl3d: SZL3D_VERSION };
}
export default { boot, probeBackend, SZL3D_VERSION, THREE_REVISION, THREE };