entangled-worlds-orbiters-shared 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "entangled-worlds-orbiters-shared",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "Shared library for entangled worlds orbiters - contains core world rendering, moons system, and UI components",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -142,11 +142,11 @@ function resolveUrls(faceKeys) {
142
142
 
143
143
  /**
144
144
  * Main loader function - always uses explicit face URLs.
145
- * `renderer` binds the KTX2 transcoder to the right WebGL context; it falls back to the standalone
146
- * app's `window.__EW_RENDERER` only transitionally. Pass it explicitly.
145
+ * `renderer` binds the KTX2 transcoder to the right WebGL context. REQUIRED (3.0) when the KTX2
146
+ * path is taken — the old `window.__EW_RENDERER` fallback is gone; thread the owning view's
147
+ * renderer. (The PNG/JPG path is renderer-agnostic, so its absence only bites on KTX2.)
147
148
  */
148
- export async function loadCubeMap({ faceKeys, skycubeName, preferKTX2 = true, forceLow, renderer: rendererOpt } = {}) {
149
- const renderer = rendererOpt ?? (typeof window !== 'undefined' ? window.__EW_RENDERER : null);
149
+ export async function loadCubeMap({ faceKeys, skycubeName, preferKTX2 = true, forceLow, renderer } = {}) {
150
150
 
151
151
  // If no face URLs provided, build them from skycube name
152
152
  if (!faceKeys && skycubeName) {
@@ -5,13 +5,12 @@ import { getKTX2LoaderForRenderer } from './ktx2.js';
5
5
 
6
6
  /**
7
7
  * @param {object} [opts]
8
- * @param {import('three').WebGLRenderer} [opts.renderer] the renderer this GLB's textures bind to.
9
- * Transitional default: `window.__EW_RENDERER` (the standalone app's publisher). Pass it explicitly
10
- * — a page with several renderers has no single correct global, and the default goes away with it.
8
+ * @param {import('three').WebGLRenderer} opts.renderer REQUIRED (3.0): the renderer this GLB's
9
+ * textures bind to. A page with several renderers has no single correct global, so the old
10
+ * `window.__EW_RENDERER` fallback is gone — thread the owning view's renderer.
11
11
  */
12
- export async function loadGLB(url, { retries = 3, signal, onProgress, applyFlags, withCredentials = false, renderer: rendererOpt } = {}) {
13
- const renderer = rendererOpt ?? (typeof window !== 'undefined' ? window.__EW_RENDERER : null);
14
- if (!renderer) throw new Error('Renderer not ready for KTX2');
12
+ export async function loadGLB(url, { retries = 3, signal, onProgress, applyFlags, withCredentials = false, renderer } = {}) {
13
+ if (!renderer) throw new Error('[glb] loadGLB requires the owning renderer (KTX2 textures bind per WebGL context).');
15
14
 
16
15
  let attempt = 0;
17
16
  while (attempt < retries) {
@@ -13,27 +13,22 @@ import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
13
13
  * (Was: a single `window.__EW_KTX2_LOADER` global shared by bodyTexture/glb/cubeMap loaders.)
14
14
  */
15
15
  const loadersByRenderer = new WeakMap();
16
- let fallbackLoader = null;
17
16
 
18
17
  function makeLoader(renderer) {
19
18
  return new KTX2Loader().setTranscoderPath('/basis/').detectSupport(renderer);
20
19
  }
21
20
 
22
21
  /**
23
- * The KTX2Loader bound to `renderer`. Defaults to the current `window.__EW_RENDERER` so existing call
24
- * sites need no change; pass the renderer explicitly when it is in scope (preferred).
25
- * @param {import('three').WebGLRenderer} [renderer]
22
+ * The KTX2Loader bound to `renderer` — REQUIRED (3.0). The old `window.__EW_RENDERER` fallback let
23
+ * call sites compile while silently binding the transcoder to whichever renderer a page published
24
+ * last; on any multi-view page that meant untextured worlds. Every caller now threads its own
25
+ * renderer, so a missing one is a caller bug and fails loudly here rather than as a dark planet.
26
+ * @param {import('three').WebGLRenderer} renderer
26
27
  * @returns {KTX2Loader}
27
28
  */
28
- export function getKTX2LoaderForRenderer(rendererArg) {
29
- // `??` not a default param: callers thread an OPTIONAL renderer down several layers, so `null`
30
- // arrives as often as `undefined` and must fall back the same way.
31
- const renderer = rendererArg ?? (typeof window !== 'undefined' ? window.__EW_RENDERER : null);
29
+ export function getKTX2LoaderForRenderer(renderer) {
32
30
  if (!renderer) {
33
- // No renderer anywhere: an unbound loader can't transcode (detectSupport needs a context), but
34
- // returning one keeps the caller's error local to the texture instead of throwing mid-scene.
35
- if (!fallbackLoader) fallbackLoader = new KTX2Loader().setTranscoderPath('/basis/');
36
- return fallbackLoader;
31
+ throw new Error('[ktx2] getKTX2LoaderForRenderer requires the owning renderer — thread it from the view that renders this texture.');
37
32
  }
38
33
  let loader = loadersByRenderer.get(renderer);
39
34
  if (!loader) {
@@ -24,7 +24,7 @@
24
24
  * solo-renders that voice. The host must not move the canvas itself.
25
25
  * - It owns the realm clear: every frame it re-asserts a fully transparent clear colour.
26
26
  *
27
- * @see `publishRendererGlobal` and `devHandleKey` — the two window globals it can write, both opt-in.
27
+ * @see `devHandleKey` — the one window global it can write, opt-in.
28
28
  */
29
29
  import * as THREE from 'three';
30
30
 
@@ -46,11 +46,6 @@ const DEFAULT_RENDERER_PROFILE = {
46
46
  * builds no grid of its own; when absent, the default `⌈√n⌉` grid is used.
47
47
  * @param {Partial<typeof DEFAULT_RENDERER_PROFILE>} [opts.rendererProfile] renderer construction +
48
48
  * pixel-ratio cap. Omit for the historical defaults.
49
- * @param {boolean} [opts.publishRendererGlobal=false] write `window.__EW_RENDERER = renderer`.
50
- * OPT-IN, and at most ONE compositor per page may set it: it is the fallback the shared loaders read
51
- * when no renderer is threaded to them, so two realms both publishing would make one realm's loads
52
- * bind their KTX2 transcoder to the OTHER realm's context (untextured worlds). Consumers that thread
53
- * the renderer explicitly must leave this off.
54
49
  * @param {string|null} [opts.devHandleKey=null] when set, publish the returned api on
55
50
  * `window[devHandleKey]` so tooling can force a composited frame while rAF is throttled (a hidden
56
51
  * tab during automated checks). Off by default — a library does not name globals for its host.
@@ -67,7 +62,6 @@ const DEFAULT_RENDERER_PROFILE = {
67
62
  export function createViewportCompositor({
68
63
  cellSource = null,
69
64
  rendererProfile = null,
70
- publishRendererGlobal = false,
71
65
  devHandleKey = null,
72
66
  } = {}) {
73
67
  const profile = { ...DEFAULT_RENDERER_PROFILE, ...(rendererProfile || {}) };
@@ -83,12 +77,6 @@ export function createViewportCompositor({
83
77
  document.body.appendChild(canvas);
84
78
 
85
79
  const renderer = createRenderer(canvas, profile);
86
- // The loaders fall back to `window.__EW_RENDERER` when nothing threads a renderer to them. Publishing
87
- // it lets a realm whose voices still rely on that fallback upload every texture to THIS context.
88
- // Opt-in, single-writer: see `publishRendererGlobal`.
89
- if (publishRendererGlobal && typeof window !== 'undefined') {
90
- window.__EW_RENDERER = renderer;
91
- }
92
80
 
93
81
  /** voiceId → { cell, controller, lastW, lastH, rect } */
94
82
  const voices = new Map();
@@ -330,11 +318,8 @@ export function createViewportCompositor({
330
318
  // If disposed while a voice was fullscreen, put the canvas style back before `canvas.remove()`.
331
319
  restoreCanvasHome();
332
320
  voices.clear();
333
- if (typeof window !== 'undefined') {
334
- if (window.__EW_RENDERER === renderer) delete window.__EW_RENDERER;
335
- if (devHandleKey && window[devHandleKey]?.renderer === renderer) {
336
- delete window[devHandleKey];
337
- }
321
+ if (typeof window !== 'undefined' && devHandleKey && window[devHandleKey]?.renderer === renderer) {
322
+ delete window[devHandleKey];
338
323
  }
339
324
  renderer.dispose();
340
325
  grid?.remove();
@@ -7,6 +7,7 @@ async function makeTextureMaterial({
7
7
  opacity = 0.6,
8
8
  textureUrl,
9
9
  speed = 0.015,
10
+ renderer = null,
10
11
  } = {}) {
11
12
  const mat = new THREE.MeshStandardMaterial({
12
13
  color,
@@ -21,7 +22,9 @@ async function makeTextureMaterial({
21
22
  });
22
23
 
23
24
  if (textureUrl) {
24
- const tex = await loadTexture(textureUrl);
25
+ // Thread the owning view's renderer: KTX2 cloud textures must transcode against the caller's
26
+ // context, not the global fallback (which shared-renderer pages don't publish at all).
27
+ const tex = await loadTexture(textureUrl, { renderer });
25
28
  tex.wrapS = THREE.RepeatWrapping;
26
29
  tex.wrapT = THREE.RepeatWrapping;
27
30
  // Use linear space for alpha masks if available
@@ -60,6 +63,7 @@ export async function createCloudShell({
60
63
  color = 0xffffff,
61
64
  opacity = 0.6,
62
65
  speed = 0.015,
66
+ renderer = null,
63
67
  } = {}) {
64
68
  const geom = baseGeometry
65
69
  ? baseGeometry.clone()
@@ -70,6 +74,7 @@ export async function createCloudShell({
70
74
  opacity,
71
75
  textureUrl,
72
76
  speed,
77
+ renderer,
73
78
  });
74
79
 
75
80
  const mesh = new THREE.Mesh(geom, material);
@@ -4,12 +4,17 @@ import { makeSphere } from './worldTexture.js';
4
4
  import { makeNormalSphere } from './worldNormal.js';
5
5
 
6
6
  export class WorldManager {
7
- constructor(scene, { onLoadStart, onLoadEnd, renderer } = {}) {
7
+ constructor(scene, { onLoadStart, onLoadEnd, renderer, externalTick = false } = {}) {
8
8
  this.scene = scene;
9
9
  // The renderer this world belongs to — threaded to the texture loaders so each view's textures
10
- // bind to its OWN WebGL context (multi-view: N orbiters / N planets in one page). Falls back to
11
- // window.__EW_RENDERER for callers that don't pass one, keeping single-renderer apps unchanged.
10
+ // bind to its OWN WebGL context (multi-view: N orbiters / N planets in one page). REQUIRED for
11
+ // textured worlds (3.0): the window.__EW_RENDERER fallback is gone.
12
12
  this.renderer = renderer ?? null;
13
+ // Shared-renderer hosts drive world animation from THEIR loop: with `externalTick` this manager
14
+ // starts no internal rAF for `userData.update` worlds — the host calls `tick(timeSec)` per
15
+ // rendered frame instead. Otherwise N animated worlds on one page would each run their own loop,
16
+ // defeating the one-loop compositor.
17
+ this.externalTick = Boolean(externalTick);
13
18
  this.current = null;
14
19
  this.flags = {};
15
20
  this.onLoadStart = onLoadStart;
@@ -19,6 +24,20 @@ export class WorldManager {
19
24
  this.customCreators = new Map();
20
25
  }
21
26
 
27
+ /** Run the current world's per-frame updater once (externalTick hosts only; no-op otherwise). */
28
+ tick(timeSec) {
29
+ if (!this.externalTick || !this.current) return;
30
+ const updater = this.current.userData?.update;
31
+ if (typeof updater !== 'function') return;
32
+ const renderer = this.renderer;
33
+ if (!renderer) return;
34
+ try {
35
+ updater(renderer, timeSec);
36
+ } catch (e) {
37
+ console.warn('[WorldManager] update tick error:', e);
38
+ }
39
+ }
40
+
22
41
  setFlags(flags) {
23
42
  this.flags = { ...this.flags, ...flags };
24
43
  }
@@ -41,7 +60,7 @@ export class WorldManager {
41
60
  this.onLoadStart?.();
42
61
  try {
43
62
  let obj;
44
- const renderer = this.renderer ?? (typeof window !== 'undefined' ? window.__EW_RENDERER : null);
63
+ const renderer = this.renderer;
45
64
 
46
65
  // Check for custom creators first
47
66
  if (this.customCreators.has(mode)) {
@@ -66,8 +85,9 @@ export class WorldManager {
66
85
  this.scene.add(obj);
67
86
  this.current = obj;
68
87
 
69
- // If the object exposes a frame updater, run a render-time loop
70
- if (obj && obj.userData && typeof obj.userData.update === 'function' && renderer) {
88
+ // If the object exposes a frame updater, run a render-time loop — unless an external host
89
+ // drives it via tick() (shared-renderer compositor: one loop for all worlds).
90
+ if (!this.externalTick && obj && obj.userData && typeof obj.userData.update === 'function' && renderer) {
71
91
  const updater = obj.userData.update;
72
92
  const tick = () => {
73
93
  if (this.current === obj) {
@@ -27,6 +27,7 @@ export async function makeNormalSphere({
27
27
  cloudsColor = 0xffffff,
28
28
  cloudsOpacity = 0.6,
29
29
  cloudsSpeed = 0.015,
30
+ renderer = null,
30
31
  } = {}) {
31
32
  const radius = typeof sphereRadius === 'number' ? Math.max(0.0001, sphereRadius) : EARTH_RADIUS_UNITS;
32
33
  const widthSegs = Number.isFinite(sphereWidthSegments) ? Math.max(3, Math.floor(sphereWidthSegments)) : 64;
@@ -63,6 +64,7 @@ export async function makeNormalSphere({
63
64
  color: cloudsColor,
64
65
  opacity: cloudsOpacity,
65
66
  speed: cloudsSpeed,
67
+ renderer,
66
68
  });
67
69
  mesh.add(cloudMesh);
68
70
  }
@@ -309,6 +309,7 @@ export async function makeSphere({
309
309
  color: cloudsColor,
310
310
  opacity: cloudsOpacity,
311
311
  speed: cloudsSpeed,
312
+ renderer,
312
313
  });
313
314
  mesh.add(cloudMesh);
314
315
  }