entangled-worlds-orbiters-shared 1.5.0 → 2.0.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": "1.5.0",
3
+ "version": "2.0.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",
@@ -1,11 +1,5 @@
1
1
  import * as THREE from 'three';
2
2
 
3
- // `hemiRef`/`ambientRef` track the MOST-RECENTLY ensured scene's lights, only so the legacy
4
- // `updateDefaultLights()` (which takes no scene) keeps targeting the active scene. They are NOT a
5
- // "lights already created" cache — see ensureDefaultLights.
6
- let hemiRef = null;
7
- let ambientRef = null;
8
-
9
3
  /**
10
4
  * Ensure a scene has the default hemisphere + ambient lights. Idempotent PER SCENE.
11
5
  *
@@ -29,29 +23,39 @@ export function ensureDefaultLights(scene) {
29
23
  ambient.name = 'ew_AmbientLight';
30
24
  scene.add(ambient);
31
25
  }
32
- // updateDefaultLights() (no scene arg) targets the most-recently ensured scene's lights.
33
- hemiRef = hemi;
34
- ambientRef = ambient;
35
26
  return { hemi, ambient };
36
27
  }
37
28
 
38
- export function updateDefaultLights({ ambientIntensity, diffuseIntensity, lightColor, lightDirection } = {}) {
39
- if (typeof ambientIntensity === 'number' && ambientRef) ambientRef.intensity = Math.max(0, ambientIntensity);
40
- if (hemiRef) {
41
- if (typeof diffuseIntensity === 'number') hemiRef.intensity = Math.max(0, diffuseIntensity);
42
- if (lightColor != null) {
43
- if (lightColor instanceof THREE.Color) {
44
- hemiRef.color.copy(lightColor);
45
- } else if (typeof lightColor === 'number') {
46
- hemiRef.color.setHex(lightColor);
47
- } else if (typeof lightColor === 'string') {
48
- hemiRef.color.set(lightColor);
49
- }
50
- }
51
- if (lightDirection && typeof lightDirection.x === 'number') {
52
- hemiRef.position.set(lightDirection.x, lightDirection.y ?? 0, lightDirection.z ?? 0).normalize();
29
+ /**
30
+ * Apply a lighting patch to ONE scene's default lights.
31
+ *
32
+ * The scene is required. This used to address module-global light refs pointing at whichever scene
33
+ * was ensured last, so on a page rendering several scenes every lighting update landed on one of
34
+ * them — a shared-renderer page (N feed worlds, N orbiters) lit the wrong world.
35
+ *
36
+ * @param {import('three').Scene} scene
37
+ */
38
+ export function updateDefaultLights(scene, { ambientIntensity, diffuseIntensity, lightColor, lightDirection } = {}) {
39
+ if (!scene?.isObject3D) {
40
+ console.error('[lights] updateDefaultLights(scene, patch) requires the scene to update; ignoring patch.');
41
+ return;
42
+ }
43
+ const { hemi, ambient } = ensureDefaultLights(scene);
44
+
45
+ if (typeof ambientIntensity === 'number') ambient.intensity = Math.max(0, ambientIntensity);
46
+ if (typeof diffuseIntensity === 'number') hemi.intensity = Math.max(0, diffuseIntensity);
47
+ if (lightColor != null) {
48
+ if (lightColor instanceof THREE.Color) {
49
+ hemi.color.copy(lightColor);
50
+ } else if (typeof lightColor === 'number') {
51
+ hemi.color.setHex(lightColor);
52
+ } else if (typeof lightColor === 'string') {
53
+ hemi.color.set(lightColor);
53
54
  }
54
55
  }
56
+ if (lightDirection && typeof lightDirection.x === 'number') {
57
+ hemi.position.set(lightDirection.x, lightDirection.y ?? 0, lightDirection.z ?? 0).normalize();
58
+ }
55
59
  }
56
60
 
57
61
  // Back-compat helper
@@ -140,10 +140,14 @@ function resolveUrls(faceKeys) {
140
140
  return resolved;
141
141
  }
142
142
 
143
- // Main loader function - always uses explicit face URLs
144
- export async function loadCubeMap({ faceKeys, skycubeName, preferKTX2 = true, forceLow } = {}) {
145
- const renderer = window.__EW_RENDERER;
146
-
143
+ /**
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.
147
+ */
148
+ export async function loadCubeMap({ faceKeys, skycubeName, preferKTX2 = true, forceLow, renderer: rendererOpt } = {}) {
149
+ const renderer = rendererOpt ?? (typeof window !== 'undefined' ? window.__EW_RENDERER : null);
150
+
147
151
  // If no face URLs provided, build them from skycube name
148
152
  if (!faceKeys && skycubeName) {
149
153
  const low = typeof forceLow === 'boolean' ? forceLow : isLowPerformanceDevice();
@@ -175,19 +179,19 @@ export async function loadCubeMap({ faceKeys, skycubeName, preferKTX2 = true, fo
175
179
  try {
176
180
  return await loadFacesKTX2(resolvedUrls, renderer);
177
181
  } catch (e) {
178
- console.warn('[CubeMapLoader] KTX2 load failed, trying JPG fallback:', e);
179
- // Fall through to JPG attempt
182
+ console.warn('[CubeMapLoader] KTX2 load failed, trying PNG fallback:', e);
183
+ // Fall through to PNG attempt
180
184
  }
181
185
  }
182
186
 
183
- // Try JPG fallback (convert URLs if needed)
184
- const jpgUrls = {};
187
+ // Try PNG fallback (convert URLs if needed)
188
+ const pngUrls = {};
185
189
  FACE_ORDER.forEach(face => {
186
- jpgUrls[face] = resolvedUrls[face].replace(/\.ktx2$/, '.jpg');
190
+ pngUrls[face] = resolvedUrls[face].replace(/\.ktx2$/, '.png');
187
191
  });
188
192
 
189
193
  try {
190
- return await loadFacesJPG(jpgUrls);
194
+ return await loadFacesJPG(pngUrls);
191
195
  } catch (e) {
192
196
  console.error('[CubeMapLoader] All loading attempts failed:', e);
193
197
  throw new Error('Failed to load cube map with any format');
@@ -248,8 +252,8 @@ export function setBackground(scene, resource) {
248
252
  }
249
253
  }
250
254
 
251
- export async function initSpaceScene(scene) {
255
+ export async function initSpaceScene(scene, { renderer } = {}) {
252
256
  const faceKeys = buildSkycubefaceKeys('001');
253
- const cube = await loadCubeMap({ faceKeys, preferKTX2: true });
257
+ const cube = await loadCubeMap({ faceKeys, preferKTX2: true, renderer });
254
258
  setBackground(scene, cube);
255
259
  }
@@ -3,8 +3,14 @@ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
3
3
  import { ensureStandardMaterial, applyFlagsToMaterial } from '../utils/materials.js';
4
4
  import { getKTX2LoaderForRenderer } from './ktx2.js';
5
5
 
6
- export async function loadGLB(url, { retries = 3, signal, onProgress, applyFlags, withCredentials = false } = {}) {
7
- const renderer = window.__EW_RENDERER;
6
+ /**
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.
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);
8
14
  if (!renderer) throw new Error('Renderer not ready for KTX2');
9
15
 
10
16
  let attempt = 0;
@@ -1,4 +1,5 @@
1
1
  // Loaders exports
2
2
  export { loadCubeMap, setBackground, normalizeSkycubeName, buildSkycubefaceKeys } from './cubeMapLoader.js';
3
3
  export { loadGLB } from './glbLoader.js';
4
- export { loadTexture } from './bodyTextureLoader.js';
4
+ export { loadTexture } from './bodyTextureLoader.js';
5
+ export { getKTX2LoaderForRenderer, disposeKTX2LoaderForRenderer } from './ktx2.js';
@@ -25,11 +25,14 @@ function makeLoader(renderer) {
25
25
  * @param {import('three').WebGLRenderer} [renderer]
26
26
  * @returns {KTX2Loader}
27
27
  */
28
- export function getKTX2LoaderForRenderer(
29
- renderer = (typeof window !== 'undefined' ? window.__EW_RENDERER : null),
30
- ) {
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);
31
32
  if (!renderer) {
32
- if (!fallbackLoader) fallbackLoader = makeLoader(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/');
33
36
  return fallbackLoader;
34
37
  }
35
38
  let loader = loadersByRenderer.get(renderer);
@@ -40,6 +40,9 @@ export async function generateMoons({
40
40
  // behavior flags
41
41
  preserveInstanceTint = false, // keep per-instance color even when textured
42
42
  useEmissiveMapFromTexture = true, // reuse color texture as emissiveMap to lift dark sides
43
+ // The renderer whose KTX2 transcoder decodes the moon texture. Required on any page with more than
44
+ // one renderer; falls back to the standalone app's global inside the loader.
45
+ renderer,
43
46
  } = {}) {
44
47
  if (count <= 0) return null;
45
48
 
@@ -95,11 +98,11 @@ export async function generateMoons({
95
98
  const fallbackTexture = textureFallback || texturePNG;
96
99
 
97
100
  try {
98
- sharedTexture = await loadTexture(primaryTexture);
101
+ sharedTexture = await loadTexture(primaryTexture, { renderer });
99
102
  } catch (eKTX) {
100
103
  if (debug) console.warn('[Moons] Primary texture failed, trying fallback:', eKTX);
101
104
  try {
102
- sharedTexture = await loadTexture(fallbackTexture);
105
+ sharedTexture = await loadTexture(fallbackTexture, { renderer });
103
106
  } catch (ePNG) {
104
107
  if (debug) console.warn('[Moons] Both primary and fallback texture loading failed');
105
108
  }
@@ -195,15 +198,7 @@ export async function generateMoons({
195
198
  group.add(instanced);
196
199
  group.name = 'moonsGroup';
197
200
 
198
- // Add disposal method for cleanup
199
- group.dispose = () => {
200
- if (moonGeometry) moonGeometry.dispose();
201
- if (moonMaterial.map && moonMaterial.map !== sharedTexture) moonMaterial.map.dispose();
202
- if (moonMaterial.emissiveMap && moonMaterial.emissiveMap !== sharedTexture) moonMaterial.emissiveMap.dispose();
203
- if (moonMaterial.bumpMap && moonMaterial.bumpMap !== sharedTexture) moonMaterial.bumpMap.dispose();
204
- moonMaterial.dispose();
205
- instanced.dispose();
206
- };
207
-
201
+ // Teardown goes through `disposeObject3D` (MoonsManager.dispose / setAmount), which frees the
202
+ // geometry, the material's textures and the instance buffers.
208
203
  return group;
209
204
  }
@@ -3,8 +3,14 @@ import * as THREE from 'three';
3
3
  import { generateMoons } from './moonsGeneration.js';
4
4
 
5
5
  export class MoonsManager {
6
- constructor(scene) {
6
+ /**
7
+ * @param {import('three').Scene} scene
8
+ * @param {object} [opts]
9
+ * @param {import('three').WebGLRenderer} [opts.renderer] renderer the moon texture binds to (KTX2).
10
+ */
11
+ constructor(scene, { renderer } = {}) {
7
12
  this.scene = scene;
13
+ this.renderer = renderer;
8
14
  this.group = null;
9
15
  this._opId = 0; // concurrency guard
10
16
  }
@@ -44,6 +50,7 @@ export class MoonsManager {
44
50
  textureKey, // Pass texture key for custom moon textures
45
51
  textureFallback,
46
52
  moonColor, // Pass moon color to generator
53
+ renderer: this.renderer,
47
54
  });
48
55
  } catch (e) {
49
56
  console.warn('[MoonsManager] Failed to generate moons:', e);
@@ -63,4 +70,16 @@ export class MoonsManager {
63
70
  console.warn('[MoonsManager] generateMoons did not return an Object3D');
64
71
  }
65
72
  }
73
+
74
+ /**
75
+ * Remove the moons from the scene and free their GPU resources. `scene.remove(group)` alone leaks
76
+ * the instanced geometry, materials and the shared moon texture on every view teardown.
77
+ */
78
+ dispose() {
79
+ this._opId++; // abandon any in-flight setAmount
80
+ if (this.group) {
81
+ disposeObject3D(this.group);
82
+ this.group = null;
83
+ }
84
+ }
66
85
  }
@@ -30,6 +30,9 @@ export function disposeObject3D(obj) {
30
30
  if (child.isMesh) {
31
31
  if (child.geometry) child.geometry.dispose();
32
32
  disposeMaterial(child.material);
33
+ // An InstancedMesh also owns `instanceMatrix` / `instanceColor` GPU buffers, which hang off the
34
+ // mesh rather than the (shared) geometry — only its own dispose() frees them.
35
+ if (child.isInstancedMesh) child.dispose();
33
36
  }
34
37
  });
35
38
  if (obj.parent) obj.parent.remove(obj);
@@ -1,7 +1,7 @@
1
1
  import { loadGLB } from '../loaders/glbLoader.js';
2
2
 
3
- export async function makeGLB({ modelUrl, flags, retries = 3, withCredentials = false } = {}) {
3
+ export async function makeGLB({ modelUrl, flags, retries = 3, withCredentials = false, renderer } = {}) {
4
4
  if (!modelUrl) throw new Error('modelUrl is required');
5
- const obj = await loadGLB(modelUrl, { retries, applyFlags: flags, withCredentials });
5
+ const obj = await loadGLB(modelUrl, { retries, applyFlags: flags, withCredentials, renderer });
6
6
  return obj;
7
7
  }