entangled-worlds-orbiters-shared 3.3.0 → 4.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.
Files changed (118) hide show
  1. package/README.md +3 -2
  2. package/dist/config/env.d.ts +8 -0
  3. package/dist/config/env.js +36 -0
  4. package/dist/config/index.d.ts +3 -0
  5. package/dist/config/index.js +4 -0
  6. package/dist/config/textureConfig.d.ts +140 -0
  7. package/dist/config/textureConfig.js +318 -0
  8. package/dist/core/index.d.ts +2 -0
  9. package/dist/core/index.js +3 -0
  10. package/dist/core/lightFresnel.d.ts +6 -0
  11. package/dist/core/lightFresnel.js +63 -0
  12. package/dist/core/lights.d.ts +48 -0
  13. package/dist/core/lights.js +79 -0
  14. package/dist/index.d.ts +6 -0
  15. package/dist/index.js +7 -0
  16. package/dist/loaders/bodyTextureLoader.d.ts +10 -0
  17. package/dist/loaders/bodyTextureLoader.js +28 -0
  18. package/dist/loaders/cubeMapLoader.d.ts +58 -0
  19. package/dist/loaders/cubeMapLoader.js +291 -0
  20. package/dist/loaders/glbLoader.d.ts +24 -0
  21. package/dist/loaders/glbLoader.js +72 -0
  22. package/dist/loaders/index.d.ts +5 -0
  23. package/{src → dist}/loaders/index.js +0 -2
  24. package/dist/{types/loaders → loaders}/ktx2.d.ts +4 -6
  25. package/dist/loaders/ktx2.js +48 -0
  26. package/dist/moons/index.d.ts +3 -0
  27. package/dist/moons/index.js +3 -0
  28. package/dist/moons/moonsGeneration.d.ts +64 -0
  29. package/dist/moons/moonsGeneration.js +238 -0
  30. package/dist/moons/moonsManager.d.ts +43 -0
  31. package/dist/moons/moonsManager.js +93 -0
  32. package/dist/{types/render → render}/frameBudget.d.ts +18 -17
  33. package/dist/render/frameBudget.js +42 -0
  34. package/dist/render/index.d.ts +4 -0
  35. package/dist/render/viewportCompositor.d.ts +124 -0
  36. package/dist/render/viewportCompositor.js +643 -0
  37. package/dist/studio/headerBroadcast.d.ts +10 -1
  38. package/dist/studio/hostTransport.d.ts +2 -2
  39. package/dist/studio/index.d.ts +1 -1
  40. package/dist/utils/dispose.d.ts +5 -0
  41. package/dist/utils/dispose.js +50 -0
  42. package/dist/utils/index.d.ts +3 -0
  43. package/{src → dist}/utils/index.js +1 -1
  44. package/dist/utils/materials.d.ts +10 -0
  45. package/dist/utils/materials.js +36 -0
  46. package/dist/world/cloudShell.d.ts +16 -0
  47. package/dist/world/cloudShell.js +73 -0
  48. package/dist/world/index.d.ts +6 -0
  49. package/{src → dist}/world/index.js +0 -2
  50. package/dist/world/worldGLB.d.ts +17 -0
  51. package/dist/world/worldGLB.js +22 -0
  52. package/dist/world/worldManager.d.ts +57 -0
  53. package/dist/world/worldManager.js +125 -0
  54. package/dist/world/worldNormal.d.ts +30 -0
  55. package/dist/world/worldNormal.js +58 -0
  56. package/dist/world/worldTexture.d.ts +56 -0
  57. package/dist/world/worldTexture.js +299 -0
  58. package/package.json +29 -39
  59. package/src/config/env.ts +48 -0
  60. package/src/config/{index.js → index.ts} +7 -0
  61. package/src/config/{textureConfig.js → textureConfig.ts} +126 -60
  62. package/src/core/{lightFresnel.js → lightFresnel.ts} +1 -1
  63. package/src/core/{lights.js → lights.ts} +21 -21
  64. package/src/loaders/{bodyTextureLoader.js → bodyTextureLoader.ts} +10 -7
  65. package/src/loaders/{cubeMapLoader.js → cubeMapLoader.ts} +83 -49
  66. package/src/loaders/{glbLoader.js → glbLoader.ts} +41 -18
  67. package/src/loaders/index.ts +6 -0
  68. package/src/loaders/{ktx2.js → ktx2.ts} +5 -7
  69. package/src/moons/{index.js → index.ts} +1 -0
  70. package/src/moons/{moonsGeneration.js → moonsGeneration.ts} +45 -32
  71. package/src/moons/{moonsManager.js → moonsManager.ts} +37 -11
  72. package/src/render/{frameBudget.js → frameBudget.ts} +15 -8
  73. package/src/render/index.ts +14 -0
  74. package/src/render/{viewportCompositor.js → viewportCompositor.ts} +121 -59
  75. package/src/studio/headerBroadcast.ts +14 -1
  76. package/src/studio/hostTransport.ts +2 -1
  77. package/src/studio/index.ts +1 -1
  78. package/src/utils/dispose.ts +47 -0
  79. package/src/utils/index.ts +4 -0
  80. package/src/utils/materials.ts +44 -0
  81. package/src/world/{cloudShell.js → cloudShell.ts} +19 -12
  82. package/src/world/index.ts +7 -0
  83. package/src/world/worldGLB.ts +41 -0
  84. package/src/world/{worldManager.js → worldManager.ts} +67 -32
  85. package/src/world/{worldNormal.js → worldNormal.ts} +25 -20
  86. package/src/world/{worldTexture.js → worldTexture.ts} +70 -51
  87. package/dist/types/config/env.d.ts +0 -12
  88. package/dist/types/config/index.d.ts +0 -2
  89. package/dist/types/config/textureConfig.d.ts +0 -121
  90. package/dist/types/core/index.d.ts +0 -2
  91. package/dist/types/core/lightFresnel.d.ts +0 -5
  92. package/dist/types/core/lights.d.ts +0 -58
  93. package/dist/types/index.d.ts +0 -6
  94. package/dist/types/loaders/bodyTextureLoader.d.ts +0 -12
  95. package/dist/types/loaders/cubeMapLoader.d.ts +0 -70
  96. package/dist/types/loaders/glbLoader.d.ts +0 -24
  97. package/dist/types/loaders/index.d.ts +0 -5
  98. package/dist/types/moons/index.d.ts +0 -2
  99. package/dist/types/moons/moonsGeneration.d.ts +0 -72
  100. package/dist/types/moons/moonsManager.d.ts +0 -27
  101. package/dist/types/render/index.d.ts +0 -2
  102. package/dist/types/render/viewportCompositor.d.ts +0 -58
  103. package/dist/types/utils/dispose.d.ts +0 -2
  104. package/dist/types/utils/index.d.ts +0 -2
  105. package/dist/types/utils/materials.d.ts +0 -2
  106. package/dist/types/world/cloudShell.d.ts +0 -25
  107. package/dist/types/world/index.d.ts +0 -6
  108. package/dist/types/world/worldGLB.d.ts +0 -20
  109. package/dist/types/world/worldManager.d.ts +0 -63
  110. package/dist/types/world/worldNormal.d.ts +0 -43
  111. package/dist/types/world/worldTexture.d.ts +0 -80
  112. package/src/config/env.js +0 -38
  113. package/src/utils/dispose.js +0 -39
  114. package/src/utils/materials.js +0 -31
  115. package/src/world/worldGLB.js +0 -19
  116. /package/{src → dist}/render/index.js +0 -0
  117. /package/src/core/{index.js → index.ts} +0 -0
  118. /package/src/{index.js → index.ts} +0 -0
@@ -4,7 +4,17 @@ import { getKTX2LoaderForRenderer } from './ktx2.js';
4
4
 
5
5
  // Base URL is resolved at runtime via helper
6
6
 
7
- export function normalizeSkycubeName(raw) {
7
+ /** One skybox face, in the order cube maps list them. */
8
+ type FaceName = 'right' | 'left' | 'top' | 'bottom' | 'front' | 'back';
9
+ /** A URL for each of the six faces. */
10
+ type FaceKeys = Record<FaceName, string>;
11
+
12
+ /** The scene field `setBackground` keeps the current skybox mesh in. It stays a field on the scene,
13
+ * not a private map, because the apps read it to tell whether a sky is already showing. */
14
+ type SkyScene = THREE.Scene & { __EW_SKYBOX_MESH?: THREE.Mesh | null };
15
+
16
+ /** A skycube id (`'7'`, `'07_skycube'`, `'skybox-007'`, …) as its three-digit folder name; `'001'` when none. */
17
+ export function normalizeSkycubeName(raw: string | null | undefined): string {
8
18
  if (!raw) return '001';
9
19
  const legacy = raw.match(/^(\d{1,2})_skycube(?:_low)?$/);
10
20
  if (legacy) return String(parseInt(legacy[1], 10)).padStart(3, '0');
@@ -14,10 +24,10 @@ export function normalizeSkycubeName(raw) {
14
24
  }
15
25
 
16
26
  // Standard face order for cube maps
17
- const FACE_ORDER = ['right', 'left', 'top', 'bottom', 'front', 'back'];
27
+ const FACE_ORDER: FaceName[] = ['right', 'left', 'top', 'bottom', 'front', 'back'];
18
28
 
19
29
  // Build complete face URL sets for a given skycube ID (now returns relative paths)
20
- export function buildSkycubefaceKeys(skycubeId, extension = 'ktx2') {
30
+ export function buildSkycubefaceKeys(skycubeId: string | null | undefined, extension = 'ktx2') {
21
31
  const id = normalizeSkycubeName(skycubeId);
22
32
  const base = `/default/skybox/${id}`;
23
33
  return {
@@ -35,7 +45,8 @@ function isLowPerformanceDevice() {
35
45
  try {
36
46
  const ua = navigator.userAgent.toLowerCase();
37
47
  const isMobileUA = /mobi|android|iphone|ipad|ipod/.test(ua);
38
- const mem = navigator.deviceMemory || 0; // GB
48
+ // `deviceMemory` is not in every browser, nor in the DOM types.
49
+ const mem = (navigator as Navigator & { deviceMemory?: number }).deviceMemory || 0; // GB
39
50
  const cores = navigator.hardwareConcurrency || 0;
40
51
  const dpr = window.devicePixelRatio || 1;
41
52
  return isMobileUA || mem && mem <= 4 || cores && cores <= 4 || (dpr > 2 && cores <= 6);
@@ -43,13 +54,14 @@ function isLowPerformanceDevice() {
43
54
  }
44
55
 
45
56
  // Validate face URLs object
46
- function validatefaceKeys(faceKeys) {
57
+ function validatefaceKeys(faceKeys: unknown): faceKeys is FaceKeys {
47
58
  if (!faceKeys || typeof faceKeys !== 'object') return false;
48
- return FACE_ORDER.every(face => faceKeys[face] && typeof faceKeys[face] === 'string');
59
+ const keys = faceKeys as Record<string, unknown>;
60
+ return FACE_ORDER.every(face => keys[face] && typeof keys[face] === 'string');
49
61
  }
50
62
 
51
63
  // Detect extension type from face URLs
52
- function detectExtension(faceKeys) {
64
+ function detectExtension(faceKeys: FaceKeys): 'ktx2' | 'jpg' {
53
65
  const url = faceKeys.right || '';
54
66
  if (url.toLowerCase().includes('.ktx2')) return 'ktx2';
55
67
  if (url.toLowerCase().match(/\.(jpg|jpeg|png)$/)) return 'jpg';
@@ -63,11 +75,11 @@ function detectExtension(faceKeys) {
63
75
  const FACES_IN_FLIGHT = 3;
64
76
 
65
77
  // Load KTX2 faces and create skybox mesh
66
- async function loadFacesKTX2(faceKeys, renderer) {
78
+ async function loadFacesKTX2(faceKeys: FaceKeys, renderer: THREE.WebGLRenderer): Promise<SkyboxResource> {
67
79
  const loader = getKTX2LoaderForRenderer(renderer);
68
80
  const anisotropy = Math.min(8, renderer.capabilities.getMaxAnisotropy());
69
81
 
70
- const loadFace = async face => {
82
+ const loadFace = async (face: FaceName) => {
71
83
  const tex = await loader.loadAsync(faceKeys[face]);
72
84
  tex.flipY = false;
73
85
  tex.wrapS = THREE.ClampToEdgeWrapping;
@@ -86,8 +98,8 @@ async function loadFacesKTX2(faceKeys, renderer) {
86
98
 
87
99
  // allSettled, not all: a bare Promise.all rejects on the first failure and leaks the faces that
88
100
  // land after it. Settle every face first, then dispose everything that arrived before rethrowing.
89
- const loaded = [];
90
- let failure = null;
101
+ const loaded: Awaited<ReturnType<typeof loadFace>>[] = [];
102
+ let failure: unknown = null;
91
103
  for (const result of settled) {
92
104
  if (result.status === 'fulfilled') loaded.push(result.value);
93
105
  else if (!failure) failure = result.reason;
@@ -102,8 +114,8 @@ async function loadFacesKTX2(faceKeys, renderer) {
102
114
  const size = 500;
103
115
  const box = new THREE.BoxGeometry(size, size, size);
104
116
  // Map faces to geometry indices (corrected for orientation)
105
- const orderMap = { right:0, left:1, top:3, bottom:2, front:4, back:5 };
106
- const mats = new Array(6).fill(null);
117
+ const orderMap: Record<FaceName, number> = { right:0, left:1, top:3, bottom:2, front:4, back:5 };
118
+ const mats: (THREE.MeshBasicMaterial | null)[] = new Array(6).fill(null);
107
119
 
108
120
  loaded.forEach(({ face, tex }) => {
109
121
  mats[orderMap[face]] = new THREE.MeshBasicMaterial({
@@ -124,13 +136,14 @@ async function loadFacesKTX2(faceKeys, renderer) {
124
136
  }
125
137
  }
126
138
 
127
- const mesh = new THREE.Mesh(box, mats);
139
+ // Every slot is filled by now (a face's material or black).
140
+ const mesh = new THREE.Mesh(box, mats as THREE.MeshBasicMaterial[]);
128
141
  mesh.name = 'SkyboxKTX2Mesh';
129
142
  return { __ewType: 'skybox-mesh', mesh };
130
143
  }
131
144
 
132
145
  // Load JPG/PNG faces and create CubeTexture
133
- function loadFacesJPG(faceKeys) {
146
+ function loadFacesJPG(faceKeys: FaceKeys): Promise<THREE.CubeTexture> {
134
147
  const urls = FACE_ORDER.map(face => faceKeys[face]);
135
148
  return new Promise((resolve, reject) => {
136
149
  new THREE.CubeTextureLoader().load(
@@ -148,9 +161,10 @@ function loadFacesJPG(faceKeys) {
148
161
  }
149
162
 
150
163
  // Resolve relative paths to full URLs
151
- function resolveUrls(faceKeys) {
164
+ function resolveUrls(faceKeys: FaceKeys): FaceKeys {
152
165
  const PUBLIC_BASE = getPublicBase();
153
- const resolved = {};
166
+ // Every face is set below.
167
+ const resolved = {} as FaceKeys;
154
168
  FACE_ORDER.forEach(face => {
155
169
  const url = faceKeys[face];
156
170
  if (url && url.startsWith('/')) {
@@ -168,16 +182,26 @@ function resolveUrls(faceKeys) {
168
182
  * path is taken — the old `window.__EW_RENDERER` fallback is gone; thread the owning view's
169
183
  * renderer. (The PNG/JPG path is renderer-agnostic, so its absence only bites on KTX2.)
170
184
  *
171
- * @param {object} [opts]
172
- * @param {{ right: string, left: string, top: string, bottom: string, front: string, back: string }} [opts.faceKeys]
173
- * explicit face URLs; when absent they are built from `skycubeName`.
174
- * @param {string} [opts.skycubeName] skybox id (e.g. `'001'`), used when `faceKeys` is absent.
175
- * @param {boolean} [opts.preferKTX2=true]
176
- * @param {boolean} [opts.forceLow] force (or forbid) the low-resolution faces; omitted = detect the device.
177
- * @param {import('three').WebGLRenderer | null} [opts.renderer]
178
- * @returns {Promise<SkyboxResource>} pass it to `setBackground`.
185
+ * - `faceKeys`: explicit face URLs; when absent they are built from `skycubeName`.
186
+ * - `skycubeName`: skybox id (e.g. `'001'`), used when `faceKeys` is absent.
187
+ * - `preferKTX2` (true).
188
+ * - `forceLow`: force (or forbid) the low-resolution faces; omitted = detect the device.
189
+ *
190
+ * Resolves to what `setBackground` takes.
179
191
  */
180
- export async function loadCubeMap({ faceKeys, skycubeName, preferKTX2 = true, forceLow, renderer } = {}) {
192
+ export async function loadCubeMap({
193
+ faceKeys,
194
+ skycubeName,
195
+ preferKTX2 = true,
196
+ forceLow,
197
+ renderer,
198
+ }: {
199
+ faceKeys?: { right: string; left: string; top: string; bottom: string; front: string; back: string } | undefined;
200
+ skycubeName?: string | undefined;
201
+ preferKTX2?: boolean | undefined;
202
+ forceLow?: boolean | undefined;
203
+ renderer?: THREE.WebGLRenderer | null | undefined;
204
+ } = {}): Promise<SkyboxResource> {
181
205
 
182
206
  // If no face URLs provided, build them from skycube name
183
207
  if (!faceKeys && skycubeName) {
@@ -216,7 +240,8 @@ export async function loadCubeMap({ faceKeys, skycubeName, preferKTX2 = true, fo
216
240
  }
217
241
 
218
242
  // Try PNG fallback (convert URLs if needed)
219
- const pngUrls = {};
243
+ // Every face is set below.
244
+ const pngUrls = {} as FaceKeys;
220
245
  FACE_ORDER.forEach(face => {
221
246
  pngUrls[face] = resolvedUrls[face].replace(/\.ktx2$/, '.png');
222
247
  });
@@ -230,9 +255,11 @@ export async function loadCubeMap({ faceKeys, skycubeName, preferKTX2 = true, fo
230
255
  }
231
256
 
232
257
  // Dispose helpers to ensure clean swaps
233
- function disposeSkyboxMesh(mesh) {
258
+ function disposeSkyboxMesh(mesh: THREE.Mesh) {
234
259
  try {
235
- const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
260
+ // The skybox mesh's materials are the MeshBasicMaterials `loadFacesKTX2` made.
261
+ const material = mesh.material as THREE.MeshBasicMaterial | THREE.MeshBasicMaterial[];
262
+ const mats = Array.isArray(material) ? material : [material];
236
263
  mats.forEach(mat => {
237
264
  if (!mat) return;
238
265
  if (mat.map && mat.map.dispose) mat.map.dispose();
@@ -247,42 +274,50 @@ function disposeSkyboxMesh(mesh) {
247
274
  /**
248
275
  * What `loadCubeMap` resolves to: a skybox mesh wrapper (the KTX2 path) or a cube texture (the
249
276
  * PNG/JPG path). Read `mesh` to tell them apart.
250
- * @typedef {{ __ewType: 'skybox-mesh', mesh: import('three').Mesh } | (import('three').CubeTexture & { mesh?: undefined })} SkyboxResource
251
277
  */
278
+ export type SkyboxResource =
279
+ | { __ewType: 'skybox-mesh'; mesh: THREE.Mesh }
280
+ | (THREE.CubeTexture & { mesh?: undefined });
252
281
 
253
282
  /**
254
283
  * Show a loaded skybox as the scene's background. A previous skybox mesh or cube-texture background is
255
284
  * disposed (a plain-texture background is not). Takes what `loadCubeMap` resolved to (or any texture);
256
285
  * anything else, `null` included, clears it.
257
286
  *
258
- * @param {import('three').Scene} scene
259
- * @param {unknown} resource
260
287
  */
261
- export function setBackground(scene, resource) {
288
+ export function setBackground(scene: THREE.Scene, resource: unknown): void {
289
+ const sky = scene as SkyScene;
290
+ // Read as whatever it may be: a skybox wrapper, a texture, or anything else (which clears).
291
+ const res = resource as
292
+ | { __ewType?: unknown; mesh: THREE.Mesh; isCubeTexture?: boolean; isTexture?: boolean }
293
+ | null
294
+ | undefined;
262
295
  // Clean up previous skybox mesh
263
- if (scene.__EW_SKYBOX_MESH && scene.__EW_SKYBOX_MESH.parent === scene) {
264
- const oldMesh = scene.__EW_SKYBOX_MESH;
296
+ if (sky.__EW_SKYBOX_MESH && sky.__EW_SKYBOX_MESH.parent === scene) {
297
+ const oldMesh = sky.__EW_SKYBOX_MESH;
265
298
  scene.remove(oldMesh);
266
299
  disposeSkyboxMesh(oldMesh);
267
- scene.__EW_SKYBOX_MESH = null;
300
+ sky.__EW_SKYBOX_MESH = null;
268
301
  }
269
302
 
270
303
  // Dispose previous CubeTexture background
271
- if (scene.background && scene.background.isCubeTexture) {
304
+ // A Color or a Texture; `dispose` is only called once it is a CubeTexture.
305
+ const background = scene.background as { isCubeTexture?: boolean; dispose(): void } | null;
306
+ if (background && background.isCubeTexture) {
272
307
  try {
273
- scene.background.dispose();
308
+ background.dispose();
274
309
  } catch(e) {
275
310
  console.warn('[CubeMapLoader] Background disposal error:', e);
276
311
  }
277
312
  }
278
313
 
279
314
  // Set new background
280
- if (resource?.__ewType === 'skybox-mesh') {
315
+ if (res?.__ewType === 'skybox-mesh') {
281
316
  scene.background = null;
282
- scene.__EW_SKYBOX_MESH = resource.mesh;
283
- scene.add(resource.mesh);
284
- } else if (resource?.isCubeTexture || resource?.isTexture) {
285
- scene.background = resource;
317
+ sky.__EW_SKYBOX_MESH = res.mesh;
318
+ scene.add(res.mesh);
319
+ } else if (res?.isCubeTexture || res?.isTexture) {
320
+ scene.background = resource as THREE.Texture;
286
321
  } else {
287
322
  scene.background = null;
288
323
  }
@@ -290,18 +325,17 @@ export function setBackground(scene, resource) {
290
325
  // Dispatch update event for reactive systems
291
326
  try {
292
327
  window.dispatchEvent(new CustomEvent('ew:skycube-updated', {
293
- detail: { type: resource?.__ewType || 'texture' }
328
+ detail: { type: res?.__ewType || 'texture' }
294
329
  }));
295
330
  } catch(e) {
296
331
  console.warn('[CubeMapLoader] Event dispatch error:', e);
297
332
  }
298
333
  }
299
334
 
300
- /**
301
- * @param {import('three').Scene} scene
302
- * @param {{ renderer?: import('three').WebGLRenderer | null }} [opts]
303
- */
304
- export async function initSpaceScene(scene, { renderer } = {}) {
335
+ export async function initSpaceScene(
336
+ scene: THREE.Scene,
337
+ { renderer }: { renderer?: THREE.WebGLRenderer | null } = {},
338
+ ): Promise<void> {
305
339
  const faceKeys = buildSkycubefaceKeys('001');
306
340
  const cube = await loadCubeMap({ faceKeys, preferKTX2: true, renderer });
307
341
  setBackground(scene, cube);
@@ -1,24 +1,44 @@
1
1
  import * as THREE from 'three';
2
2
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
3
- import { ensureStandardMaterial, applyFlagsToMaterial } from '../utils/materials.js';
3
+ import { ensureStandardMaterial, applyFlagsToMaterial, type MaterialFlags } from '../utils/materials.js';
4
4
  import { getKTX2LoaderForRenderer } from './ktx2.js';
5
5
 
6
6
  /**
7
- * @param {string} url
8
- * @param {object} [opts] `renderer` is REQUIRED at runtime (throws without one).
9
- * @param {number} [opts.retries=3] load attempts before giving up.
10
- * @param {AbortSignal} [opts.signal] rejects the load on abort.
11
- * @param {(event: ProgressEvent) => void} [opts.onProgress]
12
- * @param {object} [opts.applyFlags] material flags (`forceOpaque`, `wireframe`) applied to every mesh.
13
- * @param {boolean} [opts.withCredentials=false]
14
- * @param {number} [opts.emissiveIntensity=1] self-glow of textured materials (their map doubles as the
15
- * emissive map). 1 keeps the old flat-lit look; with a directional star light pass ≈0.1–0.15 so the
16
- * night side reads.
17
- * @param {import('three').WebGLRenderer} opts.renderer REQUIRED (3.0): the renderer this GLB's
18
- * textures bind to. A page with several renderers has no single correct global, so the old
19
- * `window.__EW_RENDERER` fallback is gone — thread the owning view's renderer.
7
+ * `renderer` is REQUIRED at runtime (throws without one).
8
+ * - `retries` (3): load attempts before giving up.
9
+ * - `signal`: rejects the load on abort.
10
+ * - `applyFlags`: material flags (`forceOpaque`, `wireframe`) applied to every mesh.
11
+ * - `withCredentials` (false).
12
+ * - `emissiveIntensity` (1): self-glow of textured materials (their map doubles as the emissive
13
+ * map). 1 keeps the old flat-lit look; with a directional star light pass ≈0.1–0.15 so the night
14
+ * side reads.
15
+ * - `renderer`: REQUIRED (3.0): the renderer this GLB's textures bind to. A page with several
16
+ * renderers has no single correct global, so the old `window.__EW_RENDERER` fallback is gone —
17
+ * thread the owning view's renderer.
20
18
  */
21
- export async function loadGLB(url, { retries = 3, signal, onProgress, applyFlags, withCredentials = false, renderer, emissiveIntensity = 1.0 } = {}) {
19
+ export async function loadGLB(
20
+ url: string,
21
+ {
22
+ retries = 3,
23
+ signal,
24
+ onProgress,
25
+ applyFlags,
26
+ withCredentials = false,
27
+ renderer,
28
+ emissiveIntensity = 1.0,
29
+ }: {
30
+ retries?: number | undefined;
31
+ signal?: AbortSignal | undefined;
32
+ onProgress?: ((event: ProgressEvent) => void) | undefined;
33
+ applyFlags?: MaterialFlags | undefined;
34
+ withCredentials?: boolean | undefined;
35
+ emissiveIntensity?: number | undefined;
36
+ renderer: THREE.WebGLRenderer;
37
+ // The options stay optional as a whole, as the JSDoc had them: without them `renderer` is
38
+ // missing and the guard below throws, as it always did.
39
+ } = {} as { renderer: THREE.WebGLRenderer },
40
+ // The loaded scene. `undefined` only when `retries` is 0 or less: no attempt is made.
41
+ ): Promise<THREE.Group | undefined> {
22
42
  if (!renderer) throw new Error('[glb] loadGLB requires the owning renderer (KTX2 textures bind per WebGL context).');
23
43
 
24
44
  let attempt = 0;
@@ -29,7 +49,7 @@ export async function loadGLB(url, { retries = 3, signal, onProgress, applyFlags
29
49
  .setWithCredentials(Boolean(withCredentials))
30
50
  .setKTX2Loader(getKTX2LoaderForRenderer(renderer));
31
51
 
32
- const root = await new Promise((resolve, reject) => {
52
+ const root = await new Promise<THREE.Group>((resolve, reject) => {
33
53
  loader.load(
34
54
  url,
35
55
  (gltf) => resolve(gltf.scene),
@@ -42,7 +62,10 @@ export async function loadGLB(url, { retries = 3, signal, onProgress, applyFlags
42
62
  });
43
63
 
44
64
  // Post-process materials
45
- root.traverse((child) => {
65
+ root.traverse((object) => {
66
+ // The meshes a GLB loads carry one material each, and `ensureStandardMaterial` just made it a
67
+ // MeshStandardMaterial, which is what the lines below write to.
68
+ const child = object as THREE.Mesh<THREE.BufferGeometry, THREE.MeshStandardMaterial>;
46
69
  if (child.isMesh && child.material) {
47
70
  ensureStandardMaterial(child);
48
71
  if (applyFlags) applyFlagsToMaterial(child.material, applyFlags);
@@ -69,7 +92,7 @@ export async function loadGLB(url, { retries = 3, signal, onProgress, applyFlags
69
92
  root.position.set(0, 0, 0);
70
93
  return root;
71
94
  } catch (e) {
72
- console.warn(`GLB load attempt ${attempt + 1} failed:`, e.message);
95
+ console.warn(`GLB load attempt ${attempt + 1} failed:`, (e as Error).message);
73
96
  if (signal?.aborted || attempt >= retries - 1) throw e;
74
97
  attempt++;
75
98
  await new Promise(resolve => setTimeout(resolve, 500)); // Brief delay between retries
@@ -0,0 +1,6 @@
1
+ // Loaders exports
2
+ export { loadCubeMap, setBackground, normalizeSkycubeName, buildSkycubefaceKeys } from './cubeMapLoader.js';
3
+ export { loadGLB } from './glbLoader.js';
4
+ export { loadTexture } from './bodyTextureLoader.js';
5
+ export { getKTX2LoaderForRenderer, disposeKTX2LoaderForRenderer } from './ktx2.js';
6
+ export type { SkyboxResource } from './cubeMapLoader.js';
@@ -1,3 +1,4 @@
1
+ import type * as THREE from 'three';
1
2
  import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
2
3
 
3
4
  /**
@@ -12,9 +13,9 @@ import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
12
13
  * (a single orbiter, today's Entangled World) get exactly ONE entry, so behaviour is byte-identical.
13
14
  * (Was: a single `window.__EW_KTX2_LOADER` global shared by bodyTexture/glb/cubeMap loaders.)
14
15
  */
15
- const loadersByRenderer = new WeakMap();
16
+ const loadersByRenderer = new WeakMap<THREE.WebGLRenderer, KTX2Loader>();
16
17
 
17
- function makeLoader(renderer) {
18
+ function makeLoader(renderer: THREE.WebGLRenderer): KTX2Loader {
18
19
  return new KTX2Loader().setTranscoderPath('/basis/').detectSupport(renderer);
19
20
  }
20
21
 
@@ -23,10 +24,8 @@ function makeLoader(renderer) {
23
24
  * call sites compile while silently binding the transcoder to whichever renderer a page published
24
25
  * last; on any multi-view page that meant untextured worlds. Every caller now threads its own
25
26
  * 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
27
- * @returns {KTX2Loader}
28
27
  */
29
- export function getKTX2LoaderForRenderer(renderer) {
28
+ export function getKTX2LoaderForRenderer(renderer: THREE.WebGLRenderer): KTX2Loader {
30
29
  if (!renderer) {
31
30
  throw new Error('[ktx2] getKTX2LoaderForRenderer requires the owning renderer — thread it from the view that renders this texture.');
32
31
  }
@@ -41,9 +40,8 @@ export function getKTX2LoaderForRenderer(renderer) {
41
40
  /**
42
41
  * Dispose and forget the KTX2Loader for a renderer — call on that renderer's / scene's teardown so the
43
42
  * transcoder workers are terminated (avoids leaking worker threads + GPU memory across view churn).
44
- * @param {import('three').WebGLRenderer} renderer
45
43
  */
46
- export function disposeKTX2LoaderForRenderer(renderer) {
44
+ export function disposeKTX2LoaderForRenderer(renderer: THREE.WebGLRenderer): void {
47
45
  const loader = renderer && loadersByRenderer.get(renderer);
48
46
  if (loader) {
49
47
  try { loader.dispose?.(); } catch { /* already gone */ }
@@ -1,5 +1,6 @@
1
1
  // Moons system exports
2
2
  export { MoonsManager } from './moonsManager.js';
3
+ export type { MoonAmountOptions } from './moonsManager.js';
3
4
  export {
4
5
  generateMoons,
5
6
  DEFAULT_MOON_TEX_KTX2,
@@ -2,7 +2,7 @@ import * as THREE from 'three';
2
2
  import { loadTexture } from '../loaders/bodyTextureLoader.js';
3
3
  import { resolvePublicUrl } from '../config/env.js';
4
4
 
5
- function moonTexPath(file) {
5
+ function moonTexPath(file: string): string {
6
6
  return resolvePublicUrl(`default/textures/moons/${file}`);
7
7
  }
8
8
  export const DEFAULT_MOON_TEX_KTX2 = moonTexPath('moon-equirectangular.ktx2');
@@ -22,34 +22,18 @@ export const MOON_ORBIT_SCATTER = 0.22;
22
22
  export const MOON_HEIGHT_SCATTER = 0.12;
23
23
 
24
24
  /**
25
- * @param {object} [opts]
26
- * @param {number} [opts.count=0] 0 or less resolves to null.
27
- * @param {number} [opts.orbitRadius=1.5]
28
- * @param {string} [opts.textureKTX2]
29
- * @param {string} [opts.texturePNG]
30
- * @param {string | null} [opts.textureFallback] used instead of `texturePNG` when set.
31
- * @param {boolean} [opts.useTexture=true]
32
- * @param {number | string | null} [opts.seed] deterministic positions when set.
33
- * @param {string | null} [opts.textureKey] replaces `textureKTX2` when set (the fallback stays
34
- * `textureFallback` or `texturePNG`).
35
- * @param {string} [opts.moonColor='#ffffff']
36
- * @param {number} [opts.brightness=1.15]
37
- * @param {number} [opts.emissiveColor=0x333333]
38
- * @param {number} [opts.emissiveIntensity=0.18]
39
- * @param {number} [opts.emissiveLift] night-side self-glow 0..1; omitted = `emissiveIntensity`.
40
- * @param {number} [opts.roughness=0.85]
41
- * @param {number} [opts.metalness=0]
42
- * @param {boolean} [opts.debug=true]
43
- * @param {boolean} [opts.smooth=true]
44
- * @param {number} [opts.geometryWidthSegments=16]
45
- * @param {number} [opts.geometryHeightSegments=12]
46
- * @param {boolean} [opts.useBumpFromColor=true]
47
- * @param {number} [opts.bumpScale=0.15]
48
- * @param {boolean} [opts.debugShowUvGrid=false]
49
- * @param {boolean} [opts.debugShowReferenceMoon=false]
50
- * @param {number} [opts.referenceMoonScale=0.25]
51
- * @param {boolean} [opts.useEmissiveMapFromTexture=true]
52
- * @param {import('three').WebGLRenderer | null} [opts.renderer] binds the KTX2 moon texture.
25
+ * - `count` (0): 0 or less resolves to null. `orbitRadius` 1.5.
26
+ * - `textureFallback`: used instead of `texturePNG` when set.
27
+ * - `seed`: deterministic positions when set.
28
+ * - `textureKey`: replaces `textureKTX2` when set (the fallback stays `textureFallback` or
29
+ * `texturePNG`).
30
+ * - `emissiveLift`: night-side self-glow 0..1; omitted = `emissiveIntensity`.
31
+ * - `renderer`: binds the KTX2 moon texture.
32
+ * - Defaults: `moonColor` '#ffffff', `brightness` 1.15, `emissiveColor` 0x333333,
33
+ * `emissiveIntensity` 0.18, `roughness` 0.85, `metalness` 0, `debug` true, `smooth` true,
34
+ * `geometryWidthSegments` 16, `geometryHeightSegments` 12, `useBumpFromColor` true, `bumpScale`
35
+ * 0.15, `debugShowUvGrid` false, `debugShowReferenceMoon` false, `referenceMoonScale` 0.25,
36
+ * `useEmissiveMapFromTexture` true.
53
37
  */
54
38
  export async function generateMoons({
55
39
  count = 0,
@@ -88,6 +72,33 @@ export async function generateMoons({
88
72
  // The renderer whose KTX2 transcoder decodes the moon texture. REQUIRED for the KTX2 texture (3.0:
89
73
  // the loader has no global fallback).
90
74
  renderer,
75
+ }: {
76
+ count?: number | undefined;
77
+ orbitRadius?: number | undefined;
78
+ textureKTX2?: string | undefined;
79
+ texturePNG?: string | undefined;
80
+ textureFallback?: string | null | undefined;
81
+ useTexture?: boolean | undefined;
82
+ seed?: string | number | null | undefined;
83
+ textureKey?: string | null | undefined;
84
+ moonColor?: string | undefined;
85
+ brightness?: number | undefined;
86
+ emissiveColor?: number | undefined;
87
+ emissiveIntensity?: number | undefined;
88
+ emissiveLift?: number | undefined;
89
+ roughness?: number | undefined;
90
+ metalness?: number | undefined;
91
+ debug?: boolean | undefined;
92
+ smooth?: boolean | undefined;
93
+ geometryWidthSegments?: number | undefined;
94
+ geometryHeightSegments?: number | undefined;
95
+ useBumpFromColor?: boolean | undefined;
96
+ bumpScale?: number | undefined;
97
+ debugShowUvGrid?: boolean | undefined;
98
+ debugShowReferenceMoon?: boolean | undefined;
99
+ referenceMoonScale?: number | undefined;
100
+ useEmissiveMapFromTexture?: boolean | undefined;
101
+ renderer?: THREE.WebGLRenderer | null | undefined;
91
102
  } = {}) {
92
103
  if (count <= 0) return null;
93
104
 
@@ -104,7 +115,8 @@ export async function generateMoons({
104
115
  }
105
116
 
106
117
  // Seeded random function (Linear Congruential Generator)
107
- let currentSeed = seedValue !== null ? Math.abs(seedValue) : Date.now();
118
+ // A string seed was hashed to a number above.
119
+ let currentSeed = seedValue !== null ? Math.abs(seedValue as number) : Date.now();
108
120
  const seededRandom = () => {
109
121
  if (seedValue !== null) {
110
122
  currentSeed = (currentSeed * 1664525 + 1013904223) % 4294967296;
@@ -116,13 +128,14 @@ export async function generateMoons({
116
128
  const moonGeometry = new THREE.SphereGeometry(MOON_BASE_RADIUS, geometryWidthSegments, geometryHeightSegments);
117
129
 
118
130
  // Optional UV grid override
119
- let sharedTexture = null;
131
+ let sharedTexture: THREE.Texture | null = null;
120
132
  let usedUvGrid = false;
121
133
  async function buildUvGridTexture() {
122
134
  const w = 512, h = 256; // 2:1 aspect
123
135
  const canvas = document.createElement('canvas');
124
136
  canvas.width = w; canvas.height = h;
125
- const ctx = canvas.getContext('2d');
137
+ // A missing 2D context throws on first use; the caller catches it, as before.
138
+ const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
126
139
  ctx.fillStyle = '#101018'; ctx.fillRect(0,0,w,h);
127
140
  for (let lon=0; lon<=12; lon++) {
128
141
  const x = (lon/12)*w; ctx.strokeStyle = lon % 3 ===0 ? '#ff4d4d':'#444'; ctx.lineWidth = lon%3===0?2:1; ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,h); ctx.stroke();
@@ -2,13 +2,36 @@ import { disposeObject3D } from '../utils/dispose.js';
2
2
  import * as THREE from 'three';
3
3
  import { generateMoons } from './moonsGeneration.js';
4
4
 
5
+ /** The `setAmount` options: what the moons look like and where they orbit. */
6
+ export interface MoonAmountOptions {
7
+ /** 1.5 by default. */
8
+ orbitRadius?: number;
9
+ textureKTX2?: string;
10
+ texturePNG?: string;
11
+ /** true by default. */
12
+ useTexture?: boolean;
13
+ /** Deterministic positions when set. */
14
+ seed?: string | number | null;
15
+ /** Replaces `textureKTX2` when set. */
16
+ textureKey?: string | null;
17
+ /** '#ffffff' by default. */
18
+ moonColor?: string;
19
+ /** Used instead of `texturePNG` when set. */
20
+ textureFallback?: string | null;
21
+ /** Night-side self-glow 0..1, kept for later rebuilds; anything but a number is ignored. */
22
+ emissiveLift?: number;
23
+ }
24
+
5
25
  export class MoonsManager {
6
- /**
7
- * @param {import('three').Scene} scene
8
- * @param {object} [opts]
9
- * @param {import('three').WebGLRenderer | null} [opts.renderer] renderer the moon texture binds to (KTX2).
10
- */
11
- constructor(scene, { renderer } = {}) {
26
+ // `declare`d so the emitted class assigns them in the constructor only, as the JS did.
27
+ declare scene: THREE.Scene;
28
+ declare renderer: THREE.WebGLRenderer | null | undefined;
29
+ declare group: THREE.Group | null;
30
+ declare _opId: number;
31
+ declare _emissiveLift: number | undefined;
32
+
33
+ /** `renderer`: the renderer the moon texture binds to (KTX2). */
34
+ constructor(scene: THREE.Scene, { renderer }: { renderer?: THREE.WebGLRenderer | null | undefined } = {}) {
12
35
  this.scene = scene;
13
36
  this.renderer = renderer;
14
37
  this.group = null;
@@ -16,7 +39,8 @@ export class MoonsManager {
16
39
  this._emissiveLift = undefined; // night-side self-glow; undefined = generator default
17
40
  }
18
41
 
19
- async setAmount(count, opts = {}) {
42
+ /** Replace the moons with `count` new ones; 0 or less (or none) just removes them. */
43
+ async setAmount(count: number | null | undefined, opts: MoonAmountOptions = {}): Promise<void> {
20
44
  // Increment operation id to invalidate older async calls
21
45
  const opId = ++this._opId;
22
46
 
@@ -41,7 +65,7 @@ export class MoonsManager {
41
65
  } = opts;
42
66
  if (typeof emissiveLift === 'number') this._emissiveLift = emissiveLift;
43
67
 
44
- let newGroup = null;
68
+ let newGroup: THREE.Group | null = null;
45
69
  try {
46
70
  newGroup = await generateMoons({
47
71
  count,
@@ -79,12 +103,14 @@ export class MoonsManager {
79
103
  * Night-side self-glow 0..1, applied in place to the live moons and kept for later rebuilds.
80
104
  * Low values (≈0.05) let a star-lit scene show the moons' dark sides.
81
105
  */
82
- setEmissiveLift(v) {
106
+ setEmissiveLift(v: number): void {
83
107
  if (typeof v !== 'number' || !Number.isFinite(v)) return;
84
108
  this._emissiveLift = Math.min(1, Math.max(0, v));
85
- this.group?.traverse((child) => {
109
+ this.group?.traverse((object) => {
110
+ // Only a mesh whose material has `emissiveIntensity` is changed; the value was just set above.
111
+ const child = object as THREE.Mesh<THREE.BufferGeometry, THREE.MeshLambertMaterial>;
86
112
  if (child.isMesh && child.material && 'emissiveIntensity' in child.material) {
87
- child.material.emissiveIntensity = this._emissiveLift;
113
+ child.material.emissiveIntensity = this._emissiveLift as number;
88
114
  }
89
115
  });
90
116
  }