entangled-worlds-orbiters-shared 5.1.0 → 6.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/README.md CHANGED
@@ -6,7 +6,7 @@ A shared library containing core components for 3D world rendering, moons system
6
6
 
7
7
  - **World System**: Core world rendering with textured spheres, normal materials, GLB models, and cloud shells
8
8
  - **Moons System**: Orbital moon generation and management
9
- - **Loaders**: Texture, GLB, and cubemap loading utilities
9
+ - **Loaders**: Texture and GLB loading utilities
10
10
  - **Core Rendering**: Lighting, Fresnel effects, and material utilities
11
11
  - **Configuration**: Texture and configuration management system
12
12
 
@@ -57,7 +57,6 @@ import * as EntangledWorlds from 'entangled-worlds-orbiters-shared';
57
57
  - `moonsGeneration` - Moon generation logic
58
58
 
59
59
  ### Loaders
60
- - `cubeMapLoader` - Skybox loading
61
60
  - `glbLoader` - GLB loading utilities
62
61
  - `bodyTextureLoader` - Texture loading
63
62
 
@@ -150,7 +149,7 @@ Defaults:
150
149
 
151
150
  Effects:
152
151
  - All URLs returned by `config/textureConfig` helpers are absolute.
153
- - Skybox loader and moons generation resolve relative paths against the configured base.
152
+ - Moons generation resolves relative paths against the configured base.
154
153
  - Cloud shell textures passed to `world.makeSphere` are resolved to absolute URLs.
155
154
 
156
155
  ## Peer Dependencies
@@ -1,3 +1,3 @@
1
- export { loadTextureConfig, getTextureOptions, getTextureChoices, getDefaultTexture, getSkyboxOptions, getDefaultSkybox, getTextureFallbackUrl } from './textureConfig.js';
1
+ export { loadTextureConfig, getTextureOptions, getTextureChoices, getDefaultTexture, getTextureFallbackUrl } from './textureConfig.js';
2
2
  export type { TextureConfig, TextureCategory, TextureItem, TextureChoice, TextureConfigMetadata } from './textureConfig.js';
3
3
  export { setPublicBase, getPublicBase, resolvePublicUrl } from './env.js';
@@ -1,4 +1,4 @@
1
1
  // Configuration exports
2
- export { loadTextureConfig, getTextureOptions, getTextureChoices, getDefaultTexture, getSkyboxOptions, getDefaultSkybox, getTextureFallbackUrl } from './textureConfig.js';
2
+ export { loadTextureConfig, getTextureOptions, getTextureChoices, getDefaultTexture, getTextureFallbackUrl } from './textureConfig.js';
3
3
  // runtime base helpers
4
4
  export { setPublicBase, getPublicBase, resolvePublicUrl } from './env.js';
@@ -9,12 +9,11 @@ export interface TextureItem {
9
9
  fallback?: string;
10
10
  tags?: string[];
11
11
  }
12
- /** One catalogue category (clouds, moons, skyboxes, …). */
12
+ /** One catalogue category (clouds, moons, …). */
13
13
  export interface TextureCategory {
14
14
  category?: string;
15
15
  basePath?: string;
16
16
  defaultTexture?: string;
17
- defaultSkybox?: string;
18
17
  items?: TextureItem[];
19
18
  }
20
19
  /** The texture catalogue as loaded from the config URL. */
@@ -70,7 +69,7 @@ export type TextureChoice = {
70
69
  export declare function getTextureChoices(category: string, config?: TextureConfig | null): Promise<TextureChoice[]>;
71
70
  /**
72
71
  * Get texture options for a specific category (clouds, moons, etc.)
73
- * @param {string} category - Texture category (clouds, moons, skyboxes)
72
+ * @param {string} category - Texture category (clouds, moons)
74
73
  * @param {TextureConfig} config - Optional pre-loaded config object
75
74
  * @returns {Promise<Record<string, string>>} `{ label: url }`, suitable for dropdown option lists
76
75
  */
@@ -114,18 +113,6 @@ export declare function getTextureUrl(category: string, textureId: string, confi
114
113
  * @returns {Promise<TextureItem[]>} Array of matching texture definitions
115
114
  */
116
115
  export declare function searchTexturesByTags(category: string, tags: string[], config?: TextureConfig | null): Promise<TextureItem[]>;
117
- /**
118
- * Get skybox options formatted for GUI dropdowns
119
- * @param {TextureConfig} config - Optional pre-loaded config object
120
- * @returns {Promise<Record<string, string>>} `{ label: id }`
121
- */
122
- export declare function getSkyboxOptions(config?: TextureConfig | null): Promise<Record<string, string>>;
123
- /**
124
- * Get default skybox ID
125
- * @param {TextureConfig} config - Optional pre-loaded config object
126
- * @returns {Promise<string>} Default skybox ID
127
- */
128
- export declare function getDefaultSkybox(config?: TextureConfig | null): Promise<string>;
129
116
  /**
130
117
  * Reload configuration (force refresh)
131
118
  * @param {string} configUrl - Override config URL
@@ -117,7 +117,7 @@ export async function getTextureChoices(category, config) {
117
117
  }
118
118
  /**
119
119
  * Get texture options for a specific category (clouds, moons, etc.)
120
- * @param {string} category - Texture category (clouds, moons, skyboxes)
120
+ * @param {string} category - Texture category (clouds, moons)
121
121
  * @param {TextureConfig} config - Optional pre-loaded config object
122
122
  * @returns {Promise<Record<string, string>>} `{ label: url }`, suitable for dropdown option lists
123
123
  */
@@ -215,40 +215,6 @@ export async function searchTexturesByTags(category, tags, config) {
215
215
  }
216
216
  return categoryConfig.items.filter(item => item.tags && tags.some(tag => item.tags.includes(tag)));
217
217
  }
218
- /**
219
- * Get skybox options formatted for GUI dropdowns
220
- * @param {TextureConfig} config - Optional pre-loaded config object
221
- * @returns {Promise<Record<string, string>>} `{ label: id }`
222
- */
223
- export async function getSkyboxOptions(config) {
224
- const textureConfig = await resolveConfig(config);
225
- const skyboxConfig = textureConfig.textures.skyboxes;
226
- if (!skyboxConfig) {
227
- console.warn('[TextureConfig] Skyboxes category not found');
228
- return {};
229
- }
230
- const options = {};
231
- // Reads `items` unguarded, unlike the other readers: a skyboxes category without items throws here.
232
- skyboxConfig.items.forEach(item => {
233
- const label = item.label || item.id;
234
- const id = item.id;
235
- options[label] = id;
236
- });
237
- return options;
238
- }
239
- /**
240
- * Get default skybox ID
241
- * @param {TextureConfig} config - Optional pre-loaded config object
242
- * @returns {Promise<string>} Default skybox ID
243
- */
244
- export async function getDefaultSkybox(config) {
245
- const textureConfig = await resolveConfig(config);
246
- const skyboxConfig = textureConfig.textures.skyboxes;
247
- if (!skyboxConfig) {
248
- return '001'; // fallback
249
- }
250
- return skyboxConfig.defaultSkybox || '001';
251
- }
252
218
  /**
253
219
  * Hardcoded fallback configuration (minimal)
254
220
  * Used when remote config loading fails
@@ -275,14 +241,6 @@ function getHardcodedFallback() {
275
241
  items: [
276
242
  { id: "default-moon", filename: "moon-equirectangular.ktx2", label: "Default Moon", tags: ["lunar"] }
277
243
  ]
278
- },
279
- skyboxes: {
280
- category: "environment",
281
- basePath: "/skybox",
282
- defaultSkybox: "001",
283
- items: [
284
- { id: "001", label: "Deep Space", tags: ["space"] }
285
- ]
286
244
  }
287
245
  }
288
246
  };
@@ -1,5 +1,3 @@
1
- export { loadCubeMap, setBackground, normalizeSkycubeName, buildSkycubefaceKeys } from './cubeMapLoader.js';
2
1
  export { loadGLB } from './glbLoader.js';
3
2
  export { loadTexture } from './bodyTextureLoader.js';
4
3
  export { getKTX2LoaderForRenderer, disposeKTX2LoaderForRenderer } from './ktx2.js';
5
- export type { SkyboxResource } from './cubeMapLoader.js';
@@ -1,5 +1,4 @@
1
1
  // Loaders exports
2
- export { loadCubeMap, setBackground, normalizeSkycubeName, buildSkycubefaceKeys } from './cubeMapLoader.js';
3
2
  export { loadGLB } from './glbLoader.js';
4
3
  export { loadTexture } from './bodyTextureLoader.js';
5
4
  export { getKTX2LoaderForRenderer, disposeKTX2LoaderForRenderer } from './ktx2.js';
@@ -4,12 +4,12 @@ import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
4
4
  *
5
5
  * `KTX2Loader.detectSupport(renderer)` binds the loader's Basis transcoder worker pool to ONE WebGL
6
6
  * context. A single shared loader therefore only decodes textures for the FIRST renderer it saw —
7
- * every other view's planets/skyboxes silently fail and render untextured (dark). That breaks any
7
+ * every other view's planets silently fail and render untextured (dark). That breaks any
8
8
  * multi-view page: N orbiters in one realm, or N Entangled World planets shown together.
9
9
  *
10
10
  * Keying the loader by renderer gives each view its own correctly-bound loader. Single-renderer apps
11
11
  * (a single orbiter, today's Entangled World) get exactly ONE entry, so behaviour is byte-identical.
12
- * (Was: a single `window.__EW_KTX2_LOADER` global shared by bodyTexture/glb/cubeMap loaders.)
12
+ * (Was: a single `window.__EW_KTX2_LOADER` global shared by the bodyTexture/glb loaders.)
13
13
  */
14
14
  const loadersByRenderer = new WeakMap();
15
15
  function makeLoader(renderer) {
package/dist/sky/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // ./sky — the sky behind a world: a parametric star field (seeded, or the world's own sky as seen
2
2
  // from its host star, read from the backend's precomputed file), an optional nebula + Milky Way
3
- // baked once per change, and constellation figures. Replaces the six-face painted skycube.
3
+ // baked once per change, and constellation figures.
4
4
  export { createSky } from './sky.js';
5
5
  export { DEFAULT_SKY_PARAMS, SKY_PARAM_RANGES, SKY_LOOK_KEYS, SKY_SHAPE_KEYS, normalizeSkyParams, } from './params.js';
6
6
  export { parseWorldSky, WORLD_SKY_MAGIC, WORLD_SKY_FORMAT } from './worldSkyFile.js';
package/dist/sky/sky.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // LineSegments). All three draw after the opaque bodies with depth test
5
5
  // on, so sky behind a planet costs nothing.
6
6
  //
7
- // Cost, measured against the six-face painted skycube it replaces (bench/sky): stars alone about
7
+ // Cost, measured against the six-face painted sky it replaced (bench/sky/results): stars alone about
8
8
  // 40% of its frame time; with the nebula about the same frame time, at 1/5–1/15 of its GPU memory
9
9
  // and with nothing to download.
10
10
  import * as THREE from 'three';
@@ -30,7 +30,7 @@ export interface MinStarDistanceInput extends Partial<SceneSizes> {
30
30
  * that the primary spans at most `MAX_STAR_ANGULAR_RADIUS_DEG`. Pure.
31
31
  */
32
32
  export declare function minStarDistance(input: MinStarDistanceInput): number;
33
- /** Sky margin: the system keeps this fraction of the sky radius clear of the skybox. */
33
+ /** Sky margin: the system keeps this fraction of the sky radius clear of the sky. */
34
34
  export declare const SKY_MARGIN_FRACTION = 0.02;
35
35
  /**
36
36
  * Farthest barycentre distance that keeps a system of `systemExtent` inside a sky of `skyRadius`
@@ -34,7 +34,7 @@ export function minStarDistance(input) {
34
34
  const angular = pos(input.primaryRadius, 0) / Math.tan((MAX_STAR_ANGULAR_RADIUS_DEG * Math.PI) / 180);
35
35
  return Math.max(clear, angular);
36
36
  }
37
- /** Sky margin: the system keeps this fraction of the sky radius clear of the skybox. */
37
+ /** Sky margin: the system keeps this fraction of the sky radius clear of the sky. */
38
38
  export const SKY_MARGIN_FRACTION = 0.02;
39
39
  /**
40
40
  * Farthest barycentre distance that keeps a system of `systemExtent` inside a sky of `skyRadius`
@@ -45,7 +45,7 @@ export declare const DEFAULT_STAR_PARAMS: Readonly<StarParams>;
45
45
  /**
46
46
  * Accepted ranges; values outside are clamped (azimuth wraps). Sized so the star reads as a
47
47
  * background host star: at the minimum (and default) distance the largest radius spans ~10° from the
48
- * planet, 220 stays inside a 500-unit skybox, and no distance puts it among the planet's moons (hosts pass their scene size to StarManager for the
48
+ * planet, 220 stays inside a 500-unit sky, and no distance puts it among the planet's moons (hosts pass their scene size to StarManager for the
49
49
  * exact, non-colliding minimum — see `minStarDistance`).
50
50
  */
51
51
  export declare const STAR_PARAM_RANGES: Readonly<{
@@ -26,7 +26,7 @@ export const DEFAULT_STAR_PARAMS = Object.freeze({
26
26
  /**
27
27
  * Accepted ranges; values outside are clamped (azimuth wraps). Sized so the star reads as a
28
28
  * background host star: at the minimum (and default) distance the largest radius spans ~10° from the
29
- * planet, 220 stays inside a 500-unit skybox, and no distance puts it among the planet's moons (hosts pass their scene size to StarManager for the
29
+ * planet, 220 stays inside a 500-unit sky, and no distance puts it among the planet's moons (hosts pass their scene size to StarManager for the
30
30
  * exact, non-colliding minimum — see `minStarDistance`).
31
31
  */
32
32
  export const STAR_PARAM_RANGES = Object.freeze({
@@ -30,7 +30,7 @@ export interface StarBody {
30
30
  setMinDistance(d: number): void;
31
31
  /**
32
32
  * Upper bound for the barycentre distance (0 / Infinity = none), e.g. to stay inside the host's
33
- * skybox. The minimum wins when the two conflict.
33
+ * sky. The minimum wins when the two conflict.
34
34
  */
35
35
  setMaxDistance(d: number): void;
36
36
  /** The distance actually used: max(minDistance, min(params.distance, maxDistance)). */
@@ -6,7 +6,7 @@ import { type StarQuality } from './quality.js';
6
6
  import { type MinStarDistanceInput } from './bounds.js';
7
7
  /** The host's scene sizes: `outerRadius` directly, or the planet/glow/moon sizes to derive it. */
8
8
  export type StarSceneBounds = Pick<MinStarDistanceInput, 'planetRadius' | 'moonOrbitRadius' | 'moonRadius' | 'cloudOrGlowScale' | 'outerRadius' | 'margin'> & {
9
- /** Radius of the host's sky (e.g. 250 for a 500-unit skybox): the whole system stays inside it. */
9
+ /** Radius of the host's sky (e.g. 250 for a 500-unit sky): the whole system stays inside it. */
10
10
  skyRadius?: number;
11
11
  };
12
12
  export interface StarManagerOptions {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "entangled-worlds-orbiters-shared",
3
- "version": "5.1.0",
3
+ "version": "6.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": "dist/index.js",
@@ -286,79 +286,6 @@
286
286
  "fallback": "silver-glow.jpg"
287
287
  }
288
288
  ]
289
- },
290
- "skyboxes": {
291
- "category": "environment",
292
- "basePath": "/skybox",
293
- "defaultSkybox": "001",
294
- "items": [
295
- {
296
- "id": "001",
297
- "label": "Deep Space",
298
- "description": "Deep space with distant stars",
299
- "tags": ["space", "dark", "minimal"]
300
- },
301
- {
302
- "id": "002",
303
- "label": "Nebula Field",
304
- "description": "Colorful nebula formations",
305
- "tags": ["nebula", "colorful", "cosmic"]
306
- },
307
- {
308
- "id": "003",
309
- "label": "Galaxy Center",
310
- "description": "Dense galactic core region",
311
- "tags": ["galaxy", "dense", "bright"]
312
- },
313
- {
314
- "id": "004",
315
- "label": "Binary System",
316
- "description": "Binary star system environment",
317
- "tags": ["binary", "stars", "orange"]
318
- },
319
- {
320
- "id": "005",
321
- "label": "Cosmic Web",
322
- "description": "Large-scale cosmic structure",
323
- "tags": ["cosmic", "web", "structure"]
324
- },
325
- {
326
- "id": "006",
327
- "label": "Stellar Nursery",
328
- "description": "Star formation region",
329
- "tags": ["formation", "young", "bright"]
330
- },
331
- {
332
- "id": "007",
333
- "label": "Dark Matter",
334
- "description": "Dark matter visualization",
335
- "tags": ["dark", "matter", "theoretical"]
336
- },
337
- {
338
- "id": "008",
339
- "label": "Quantum Foam",
340
- "description": "Quantum scale space-time",
341
- "tags": ["quantum", "foam", "microscopic"]
342
- },
343
- {
344
- "id": "009",
345
- "label": "Event Horizon",
346
- "description": "Black hole event horizon",
347
- "tags": ["black hole", "horizon", "gravity"]
348
- },
349
- {
350
- "id": "010",
351
- "label": "Multiverse",
352
- "description": "Multiple universe visualization",
353
- "tags": ["multiverse", "infinite", "parallel"]
354
- },
355
- {
356
- "id": "011",
357
- "label": "Void Space",
358
- "description": "Empty cosmic void region",
359
- "tags": ["void", "empty", "minimal"]
360
- }
361
- ]
362
289
  }
363
290
  }
364
291
  }
@@ -4,8 +4,6 @@ export {
4
4
  getTextureOptions,
5
5
  getTextureChoices,
6
6
  getDefaultTexture,
7
- getSkyboxOptions,
8
- getDefaultSkybox,
9
7
  getTextureFallbackUrl
10
8
  } from './textureConfig.js';
11
9
  export type {
@@ -16,12 +16,11 @@ export interface TextureItem {
16
16
  tags?: string[];
17
17
  }
18
18
 
19
- /** One catalogue category (clouds, moons, skyboxes, …). */
19
+ /** One catalogue category (clouds, moons, …). */
20
20
  export interface TextureCategory {
21
21
  category?: string;
22
22
  basePath?: string;
23
23
  defaultTexture?: string;
24
- defaultSkybox?: string;
25
24
  items?: TextureItem[];
26
25
  }
27
26
 
@@ -196,7 +195,7 @@ export async function getTextureChoices(category: string, config?: TextureConfig
196
195
 
197
196
  /**
198
197
  * Get texture options for a specific category (clouds, moons, etc.)
199
- * @param {string} category - Texture category (clouds, moons, skyboxes)
198
+ * @param {string} category - Texture category (clouds, moons)
200
199
  * @param {TextureConfig} config - Optional pre-loaded config object
201
200
  * @returns {Promise<Record<string, string>>} `{ label: url }`, suitable for dropdown option lists
202
201
  */
@@ -309,48 +308,6 @@ export async function searchTexturesByTags(category: string, tags: string[], con
309
308
  );
310
309
  }
311
310
 
312
- /**
313
- * Get skybox options formatted for GUI dropdowns
314
- * @param {TextureConfig} config - Optional pre-loaded config object
315
- * @returns {Promise<Record<string, string>>} `{ label: id }`
316
- */
317
- export async function getSkyboxOptions(config?: TextureConfig | null): Promise<Record<string, string>> {
318
- const textureConfig = await resolveConfig(config);
319
- const skyboxConfig = textureConfig.textures.skyboxes;
320
-
321
- if (!skyboxConfig) {
322
- console.warn('[TextureConfig] Skyboxes category not found');
323
- return {};
324
- }
325
-
326
- const options: Record<string, string> = {};
327
-
328
- // Reads `items` unguarded, unlike the other readers: a skyboxes category without items throws here.
329
- skyboxConfig.items!.forEach(item => {
330
- const label = item.label || item.id;
331
- const id = item.id;
332
- options[label] = id;
333
- });
334
-
335
- return options;
336
- }
337
-
338
- /**
339
- * Get default skybox ID
340
- * @param {TextureConfig} config - Optional pre-loaded config object
341
- * @returns {Promise<string>} Default skybox ID
342
- */
343
- export async function getDefaultSkybox(config?: TextureConfig | null): Promise<string> {
344
- const textureConfig = await resolveConfig(config);
345
- const skyboxConfig = textureConfig.textures.skyboxes;
346
-
347
- if (!skyboxConfig) {
348
- return '001'; // fallback
349
- }
350
-
351
- return skyboxConfig.defaultSkybox || '001';
352
- }
353
-
354
311
  /**
355
312
  * Hardcoded fallback configuration (minimal)
356
313
  * Used when remote config loading fails
@@ -377,14 +334,6 @@ function getHardcodedFallback(): TextureConfig {
377
334
  items: [
378
335
  { id: "default-moon", filename: "moon-equirectangular.ktx2", label: "Default Moon", tags: ["lunar"] }
379
336
  ]
380
- },
381
- skyboxes: {
382
- category: "environment",
383
- basePath: "/skybox",
384
- defaultSkybox: "001",
385
- items: [
386
- { id: "001", label: "Deep Space", tags: ["space"] }
387
- ]
388
337
  }
389
338
  }
390
339
  };
@@ -1,6 +1,4 @@
1
1
  // Loaders exports
2
- export { loadCubeMap, setBackground, normalizeSkycubeName, buildSkycubefaceKeys } from './cubeMapLoader.js';
3
2
  export { loadGLB } from './glbLoader.js';
4
3
  export { loadTexture } from './bodyTextureLoader.js';
5
4
  export { getKTX2LoaderForRenderer, disposeKTX2LoaderForRenderer } from './ktx2.js';
6
- export type { SkyboxResource } from './cubeMapLoader.js';
@@ -6,12 +6,12 @@ import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
6
6
  *
7
7
  * `KTX2Loader.detectSupport(renderer)` binds the loader's Basis transcoder worker pool to ONE WebGL
8
8
  * context. A single shared loader therefore only decodes textures for the FIRST renderer it saw —
9
- * every other view's planets/skyboxes silently fail and render untextured (dark). That breaks any
9
+ * every other view's planets silently fail and render untextured (dark). That breaks any
10
10
  * multi-view page: N orbiters in one realm, or N Entangled World planets shown together.
11
11
  *
12
12
  * Keying the loader by renderer gives each view its own correctly-bound loader. Single-renderer apps
13
13
  * (a single orbiter, today's Entangled World) get exactly ONE entry, so behaviour is byte-identical.
14
- * (Was: a single `window.__EW_KTX2_LOADER` global shared by bodyTexture/glb/cubeMap loaders.)
14
+ * (Was: a single `window.__EW_KTX2_LOADER` global shared by the bodyTexture/glb loaders.)
15
15
  */
16
16
  const loadersByRenderer = new WeakMap<THREE.WebGLRenderer, KTX2Loader>();
17
17
 
package/src/sky/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // ./sky — the sky behind a world: a parametric star field (seeded, or the world's own sky as seen
2
2
  // from its host star, read from the backend's precomputed file), an optional nebula + Milky Way
3
- // baked once per change, and constellation figures. Replaces the six-face painted skycube.
3
+ // baked once per change, and constellation figures.
4
4
 
5
5
  export { createSky } from './sky.js';
6
6
  export type { Sky, CreateSkyOptions } from './sky.js';
package/src/sky/sky.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  // LineSegments). All three draw after the opaque bodies with depth test
5
5
  // on, so sky behind a planet costs nothing.
6
6
  //
7
- // Cost, measured against the six-face painted skycube it replaces (bench/sky): stars alone about
7
+ // Cost, measured against the six-face painted sky it replaced (bench/sky/results): stars alone about
8
8
  // 40% of its frame time; with the nebula about the same frame time, at 1/5–1/15 of its GPU memory
9
9
  // and with nothing to download.
10
10
 
@@ -59,7 +59,7 @@ export function minStarDistance(input: MinStarDistanceInput): number {
59
59
  return Math.max(clear, angular);
60
60
  }
61
61
 
62
- /** Sky margin: the system keeps this fraction of the sky radius clear of the skybox. */
62
+ /** Sky margin: the system keeps this fraction of the sky radius clear of the sky. */
63
63
  export const SKY_MARGIN_FRACTION = 0.02;
64
64
 
65
65
  /**
@@ -72,7 +72,7 @@ export const DEFAULT_STAR_PARAMS: Readonly<StarParams> = Object.freeze({
72
72
  /**
73
73
  * Accepted ranges; values outside are clamped (azimuth wraps). Sized so the star reads as a
74
74
  * background host star: at the minimum (and default) distance the largest radius spans ~10° from the
75
- * planet, 220 stays inside a 500-unit skybox, and no distance puts it among the planet's moons (hosts pass their scene size to StarManager for the
75
+ * planet, 220 stays inside a 500-unit sky, and no distance puts it among the planet's moons (hosts pass their scene size to StarManager for the
76
76
  * exact, non-colliding minimum — see `minStarDistance`).
77
77
  */
78
78
  export const STAR_PARAM_RANGES = Object.freeze({
@@ -49,7 +49,7 @@ export interface StarBody {
49
49
  setMinDistance(d: number): void;
50
50
  /**
51
51
  * Upper bound for the barycentre distance (0 / Infinity = none), e.g. to stay inside the host's
52
- * skybox. The minimum wins when the two conflict.
52
+ * sky. The minimum wins when the two conflict.
53
53
  */
54
54
  setMaxDistance(d: number): void;
55
55
  /** The distance actually used: max(minDistance, min(params.distance, maxDistance)). */
@@ -30,7 +30,7 @@ import { maxStarDistance, minStarDistance, type MinStarDistanceInput } from './b
30
30
 
31
31
  /** The host's scene sizes: `outerRadius` directly, or the planet/glow/moon sizes to derive it. */
32
32
  export type StarSceneBounds = Pick<MinStarDistanceInput, 'planetRadius' | 'moonOrbitRadius' | 'moonRadius' | 'cloudOrGlowScale' | 'outerRadius' | 'margin'> & {
33
- /** Radius of the host's sky (e.g. 250 for a 500-unit skybox): the whole system stays inside it. */
33
+ /** Radius of the host's sky (e.g. 250 for a 500-unit sky): the whole system stays inside it. */
34
34
  skyRadius?: number;
35
35
  };
36
36
 
@@ -1,58 +0,0 @@
1
- import * as THREE from 'three';
2
- /** A skycube id (`'7'`, `'07_skycube'`, `'skybox-007'`, …) as its three-digit folder name; `'001'` when none. */
3
- export declare function normalizeSkycubeName(raw: string | null | undefined): string;
4
- export declare function buildSkycubefaceKeys(skycubeId: string | null | undefined, extension?: string): {
5
- right: string;
6
- left: string;
7
- top: string;
8
- bottom: string;
9
- front: string;
10
- back: string;
11
- };
12
- /**
13
- * Main loader function - always uses explicit face URLs.
14
- * `renderer` binds the KTX2 transcoder to the right WebGL context. REQUIRED (3.0) when the KTX2
15
- * path is taken — the old `window.__EW_RENDERER` fallback is gone; thread the owning view's
16
- * renderer. (The PNG/JPG path is renderer-agnostic, so its absence only bites on KTX2.)
17
- *
18
- * - `faceKeys`: explicit face URLs; when absent they are built from `skycubeName`.
19
- * - `skycubeName`: skybox id (e.g. `'001'`), used when `faceKeys` is absent.
20
- * - `preferKTX2` (true).
21
- * - `forceLow`: force (or forbid) the low-resolution faces; omitted = detect the device.
22
- *
23
- * Resolves to what `setBackground` takes.
24
- */
25
- export declare function loadCubeMap({ faceKeys, skycubeName, preferKTX2, forceLow, renderer, }?: {
26
- faceKeys?: {
27
- right: string;
28
- left: string;
29
- top: string;
30
- bottom: string;
31
- front: string;
32
- back: string;
33
- } | undefined;
34
- skycubeName?: string | undefined;
35
- preferKTX2?: boolean | undefined;
36
- forceLow?: boolean | undefined;
37
- renderer?: THREE.WebGLRenderer | null | undefined;
38
- }): Promise<SkyboxResource>;
39
- /**
40
- * What `loadCubeMap` resolves to: a skybox mesh wrapper (the KTX2 path) or a cube texture (the
41
- * PNG/JPG path). Read `mesh` to tell them apart.
42
- */
43
- export type SkyboxResource = {
44
- __ewType: 'skybox-mesh';
45
- mesh: THREE.Mesh;
46
- } | (THREE.CubeTexture & {
47
- mesh?: undefined;
48
- });
49
- /**
50
- * Show a loaded skybox as the scene's background. A previous skybox mesh or cube-texture background is
51
- * disposed (a plain-texture background is not). Takes what `loadCubeMap` resolved to (or any texture);
52
- * anything else, `null` included, clears it.
53
- *
54
- */
55
- export declare function setBackground(scene: THREE.Scene, resource: unknown): void;
56
- export declare function initSpaceScene(scene: THREE.Scene, { renderer }?: {
57
- renderer?: THREE.WebGLRenderer | null;
58
- }): Promise<void>;
@@ -1,291 +0,0 @@
1
- import * as THREE from 'three';
2
- import { getPublicBase } from '../config/env.js';
3
- import { getKTX2LoaderForRenderer } from './ktx2.js';
4
- /** A skycube id (`'7'`, `'07_skycube'`, `'skybox-007'`, …) as its three-digit folder name; `'001'` when none. */
5
- export function normalizeSkycubeName(raw) {
6
- if (!raw)
7
- return '001';
8
- const legacy = raw.match(/^(\d{1,2})_skycube(?:_low)?$/);
9
- if (legacy)
10
- return String(parseInt(legacy[1], 10)).padStart(3, '0');
11
- const num = raw.match(/(\d{1,3})/);
12
- if (num)
13
- return String(parseInt(num[1], 10)).padStart(3, '0');
14
- return '001';
15
- }
16
- // Standard face order for cube maps
17
- const FACE_ORDER = ['right', 'left', 'top', 'bottom', 'front', 'back'];
18
- // Build complete face URL sets for a given skycube ID (now returns relative paths)
19
- export function buildSkycubefaceKeys(skycubeId, extension = 'ktx2') {
20
- const id = normalizeSkycubeName(skycubeId);
21
- const base = `/default/skybox/${id}`;
22
- return {
23
- right: `${base}/right.${extension}`,
24
- left: `${base}/left.${extension}`,
25
- top: `${base}/top.${extension}`,
26
- bottom: `${base}/bottom.${extension}`,
27
- front: `${base}/front.${extension}`,
28
- back: `${base}/back.${extension}`
29
- };
30
- }
31
- // Heuristic low-performance detection
32
- function isLowPerformanceDevice() {
33
- try {
34
- const ua = navigator.userAgent.toLowerCase();
35
- const isMobileUA = /mobi|android|iphone|ipad|ipod/.test(ua);
36
- // `deviceMemory` is not in every browser, nor in the DOM types.
37
- const mem = navigator.deviceMemory || 0; // GB
38
- const cores = navigator.hardwareConcurrency || 0;
39
- const dpr = window.devicePixelRatio || 1;
40
- return isMobileUA || mem && mem <= 4 || cores && cores <= 4 || (dpr > 2 && cores <= 6);
41
- }
42
- catch {
43
- return false;
44
- }
45
- }
46
- // Validate face URLs object
47
- function validatefaceKeys(faceKeys) {
48
- if (!faceKeys || typeof faceKeys !== 'object')
49
- return false;
50
- const keys = faceKeys;
51
- return FACE_ORDER.every(face => keys[face] && typeof keys[face] === 'string');
52
- }
53
- // Detect extension type from face URLs
54
- function detectExtension(faceKeys) {
55
- const url = faceKeys.right || '';
56
- if (url.toLowerCase().includes('.ktx2'))
57
- return 'ktx2';
58
- if (url.toLowerCase().match(/\.(jpg|jpeg|png)$/))
59
- return 'jpg';
60
- return 'ktx2'; // default assumption
61
- }
62
- // How many faces may be in flight at once. Not six: the KTX2 worker pool three.js gives us holds
63
- // four transcoder workers, each carrying its own copy of the basis wasm, and that same pool also
64
- // serves body textures and GLBs. Asking for all six together spawned every worker and queued the
65
- // other loaders behind the whole sky; measured on cold fetches it was no faster than three.
66
- const FACES_IN_FLIGHT = 3;
67
- // Load KTX2 faces and create skybox mesh
68
- async function loadFacesKTX2(faceKeys, renderer) {
69
- const loader = getKTX2LoaderForRenderer(renderer);
70
- const anisotropy = Math.min(8, renderer.capabilities.getMaxAnisotropy());
71
- const loadFace = async (face) => {
72
- const tex = await loader.loadAsync(faceKeys[face]);
73
- tex.flipY = false;
74
- tex.wrapS = THREE.ClampToEdgeWrapping;
75
- tex.wrapT = THREE.ClampToEdgeWrapping;
76
- tex.anisotropy = anisotropy;
77
- return { face, tex };
78
- };
79
- // Faces overlap instead of chaining. Awaiting each one in turn made every face wait on the
80
- // previous face's download *and* its transcode, so the sky arrived six round trips deep.
81
- const settled = [];
82
- for (let i = 0; i < FACE_ORDER.length; i += FACES_IN_FLIGHT) {
83
- const wave = FACE_ORDER.slice(i, i + FACES_IN_FLIGHT).map(face => loadFace(face));
84
- settled.push(...await Promise.allSettled(wave));
85
- }
86
- // allSettled, not all: a bare Promise.all rejects on the first failure and leaks the faces that
87
- // land after it. Settle every face first, then dispose everything that arrived before rethrowing.
88
- const loaded = [];
89
- let failure = null;
90
- for (const result of settled) {
91
- if (result.status === 'fulfilled')
92
- loaded.push(result.value);
93
- else if (!failure)
94
- failure = result.reason;
95
- }
96
- if (failure) {
97
- // Dispose already loaded textures on failure
98
- loaded.forEach(f => f.tex.dispose?.());
99
- throw failure;
100
- }
101
- const size = 500;
102
- const box = new THREE.BoxGeometry(size, size, size);
103
- // Map faces to geometry indices (corrected for orientation)
104
- const orderMap = { right: 0, left: 1, top: 3, bottom: 2, front: 4, back: 5 };
105
- const mats = new Array(6).fill(null);
106
- loaded.forEach(({ face, tex }) => {
107
- mats[orderMap[face]] = new THREE.MeshBasicMaterial({
108
- map: tex,
109
- side: THREE.BackSide,
110
- depthWrite: false
111
- });
112
- });
113
- // Fill any missing materials with black
114
- for (let i = 0; i < 6; i++) {
115
- if (!mats[i]) {
116
- mats[i] = new THREE.MeshBasicMaterial({
117
- color: 0x000000,
118
- side: THREE.BackSide,
119
- depthWrite: false
120
- });
121
- }
122
- }
123
- // Every slot is filled by now (a face's material or black).
124
- const mesh = new THREE.Mesh(box, mats);
125
- mesh.name = 'SkyboxKTX2Mesh';
126
- return { __ewType: 'skybox-mesh', mesh };
127
- }
128
- // Load JPG/PNG faces and create CubeTexture
129
- function loadFacesJPG(faceKeys) {
130
- const urls = FACE_ORDER.map(face => faceKeys[face]);
131
- return new Promise((resolve, reject) => {
132
- new THREE.CubeTextureLoader().load(urls, tex => {
133
- tex.magFilter = THREE.LinearFilter;
134
- tex.minFilter = THREE.LinearMipMapLinearFilter;
135
- tex.anisotropy = 8;
136
- resolve(tex);
137
- }, undefined, err => reject(err));
138
- });
139
- }
140
- // Resolve relative paths to full URLs
141
- function resolveUrls(faceKeys) {
142
- const PUBLIC_BASE = getPublicBase();
143
- // Every face is set below.
144
- const resolved = {};
145
- FACE_ORDER.forEach(face => {
146
- const url = faceKeys[face];
147
- if (url && url.startsWith('/')) {
148
- resolved[face] = `${PUBLIC_BASE}${url}`;
149
- }
150
- else {
151
- resolved[face] = url; // already absolute or invalid
152
- }
153
- });
154
- return resolved;
155
- }
156
- /**
157
- * Main loader function - always uses explicit face URLs.
158
- * `renderer` binds the KTX2 transcoder to the right WebGL context. REQUIRED (3.0) when the KTX2
159
- * path is taken — the old `window.__EW_RENDERER` fallback is gone; thread the owning view's
160
- * renderer. (The PNG/JPG path is renderer-agnostic, so its absence only bites on KTX2.)
161
- *
162
- * - `faceKeys`: explicit face URLs; when absent they are built from `skycubeName`.
163
- * - `skycubeName`: skybox id (e.g. `'001'`), used when `faceKeys` is absent.
164
- * - `preferKTX2` (true).
165
- * - `forceLow`: force (or forbid) the low-resolution faces; omitted = detect the device.
166
- *
167
- * Resolves to what `setBackground` takes.
168
- */
169
- export async function loadCubeMap({ faceKeys, skycubeName, preferKTX2 = true, forceLow, renderer, } = {}) {
170
- // If no face URLs provided, build them from skycube name
171
- if (!faceKeys && skycubeName) {
172
- const low = typeof forceLow === 'boolean' ? forceLow : isLowPerformanceDevice();
173
- const extension = preferKTX2 ? 'ktx2' : 'jpg';
174
- const id = normalizeSkycubeName(skycubeName);
175
- const suffix = low ? '_low' : '';
176
- const relativePath = `/default/skybox/${id}${suffix}`;
177
- faceKeys = {
178
- right: `${relativePath}/right.${extension}`,
179
- left: `${relativePath}/left.${extension}`,
180
- top: `${relativePath}/top.${extension}`,
181
- bottom: `${relativePath}/bottom.${extension}`,
182
- front: `${relativePath}/front.${extension}`,
183
- back: `${relativePath}/back.${extension}`
184
- };
185
- }
186
- if (!validatefaceKeys(faceKeys)) {
187
- throw new Error('Invalid face URLs provided');
188
- }
189
- // Resolve relative paths to full URLs
190
- const resolvedUrls = resolveUrls(faceKeys);
191
- const extension = detectExtension(resolvedUrls);
192
- // Try KTX2 first if preferred and supported
193
- if (preferKTX2 && extension === 'ktx2' && renderer) {
194
- try {
195
- return await loadFacesKTX2(resolvedUrls, renderer);
196
- }
197
- catch (e) {
198
- console.warn('[CubeMapLoader] KTX2 load failed, trying PNG fallback:', e);
199
- // Fall through to PNG attempt
200
- }
201
- }
202
- // Try PNG fallback (convert URLs if needed)
203
- // Every face is set below.
204
- const pngUrls = {};
205
- FACE_ORDER.forEach(face => {
206
- pngUrls[face] = resolvedUrls[face].replace(/\.ktx2$/, '.png');
207
- });
208
- try {
209
- return await loadFacesJPG(pngUrls);
210
- }
211
- catch (e) {
212
- console.error('[CubeMapLoader] All loading attempts failed:', e);
213
- throw new Error('Failed to load cube map with any format');
214
- }
215
- }
216
- // Dispose helpers to ensure clean swaps
217
- function disposeSkyboxMesh(mesh) {
218
- try {
219
- // The skybox mesh's materials are the MeshBasicMaterials `loadFacesKTX2` made.
220
- const material = mesh.material;
221
- const mats = Array.isArray(material) ? material : [material];
222
- mats.forEach(mat => {
223
- if (!mat)
224
- return;
225
- if (mat.map && mat.map.dispose)
226
- mat.map.dispose();
227
- if (mat.dispose)
228
- mat.dispose();
229
- });
230
- if (mesh.geometry && mesh.geometry.dispose)
231
- mesh.geometry.dispose();
232
- }
233
- catch (e) {
234
- console.warn('[CubeMapLoader] Disposal error:', e);
235
- }
236
- }
237
- /**
238
- * Show a loaded skybox as the scene's background. A previous skybox mesh or cube-texture background is
239
- * disposed (a plain-texture background is not). Takes what `loadCubeMap` resolved to (or any texture);
240
- * anything else, `null` included, clears it.
241
- *
242
- */
243
- export function setBackground(scene, resource) {
244
- const sky = scene;
245
- // Read as whatever it may be: a skybox wrapper, a texture, or anything else (which clears).
246
- const res = resource;
247
- // Clean up previous skybox mesh
248
- if (sky.__EW_SKYBOX_MESH && sky.__EW_SKYBOX_MESH.parent === scene) {
249
- const oldMesh = sky.__EW_SKYBOX_MESH;
250
- scene.remove(oldMesh);
251
- disposeSkyboxMesh(oldMesh);
252
- sky.__EW_SKYBOX_MESH = null;
253
- }
254
- // Dispose previous CubeTexture background
255
- // A Color or a Texture; `dispose` is only called once it is a CubeTexture.
256
- const background = scene.background;
257
- if (background && background.isCubeTexture) {
258
- try {
259
- background.dispose();
260
- }
261
- catch (e) {
262
- console.warn('[CubeMapLoader] Background disposal error:', e);
263
- }
264
- }
265
- // Set new background
266
- if (res?.__ewType === 'skybox-mesh') {
267
- scene.background = null;
268
- sky.__EW_SKYBOX_MESH = res.mesh;
269
- scene.add(res.mesh);
270
- }
271
- else if (res?.isCubeTexture || res?.isTexture) {
272
- scene.background = resource;
273
- }
274
- else {
275
- scene.background = null;
276
- }
277
- // Dispatch update event for reactive systems
278
- try {
279
- window.dispatchEvent(new CustomEvent('ew:skycube-updated', {
280
- detail: { type: res?.__ewType || 'texture' }
281
- }));
282
- }
283
- catch (e) {
284
- console.warn('[CubeMapLoader] Event dispatch error:', e);
285
- }
286
- }
287
- export async function initSpaceScene(scene, { renderer } = {}) {
288
- const faceKeys = buildSkycubefaceKeys('001');
289
- const cube = await loadCubeMap({ faceKeys, preferKTX2: true, renderer });
290
- setBackground(scene, cube);
291
- }
@@ -1,342 +0,0 @@
1
- import * as THREE from 'three';
2
- import { getPublicBase } from '../config/env.js';
3
- import { getKTX2LoaderForRenderer } from './ktx2.js';
4
-
5
- // Base URL is resolved at runtime via helper
6
-
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 {
18
- if (!raw) return '001';
19
- const legacy = raw.match(/^(\d{1,2})_skycube(?:_low)?$/);
20
- if (legacy) return String(parseInt(legacy[1], 10)).padStart(3, '0');
21
- const num = raw.match(/(\d{1,3})/);
22
- if (num) return String(parseInt(num[1], 10)).padStart(3, '0');
23
- return '001';
24
- }
25
-
26
- // Standard face order for cube maps
27
- const FACE_ORDER: FaceName[] = ['right', 'left', 'top', 'bottom', 'front', 'back'];
28
-
29
- // Build complete face URL sets for a given skycube ID (now returns relative paths)
30
- export function buildSkycubefaceKeys(skycubeId: string | null | undefined, extension = 'ktx2') {
31
- const id = normalizeSkycubeName(skycubeId);
32
- const base = `/default/skybox/${id}`;
33
- return {
34
- right: `${base}/right.${extension}`,
35
- left: `${base}/left.${extension}`,
36
- top: `${base}/top.${extension}`,
37
- bottom: `${base}/bottom.${extension}`,
38
- front: `${base}/front.${extension}`,
39
- back: `${base}/back.${extension}`
40
- };
41
- }
42
-
43
- // Heuristic low-performance detection
44
- function isLowPerformanceDevice() {
45
- try {
46
- const ua = navigator.userAgent.toLowerCase();
47
- const isMobileUA = /mobi|android|iphone|ipad|ipod/.test(ua);
48
- // `deviceMemory` is not in every browser, nor in the DOM types.
49
- const mem = (navigator as Navigator & { deviceMemory?: number }).deviceMemory || 0; // GB
50
- const cores = navigator.hardwareConcurrency || 0;
51
- const dpr = window.devicePixelRatio || 1;
52
- return isMobileUA || mem && mem <= 4 || cores && cores <= 4 || (dpr > 2 && cores <= 6);
53
- } catch { return false; }
54
- }
55
-
56
- // Validate face URLs object
57
- function validatefaceKeys(faceKeys: unknown): faceKeys is FaceKeys {
58
- if (!faceKeys || typeof faceKeys !== 'object') return false;
59
- const keys = faceKeys as Record<string, unknown>;
60
- return FACE_ORDER.every(face => keys[face] && typeof keys[face] === 'string');
61
- }
62
-
63
- // Detect extension type from face URLs
64
- function detectExtension(faceKeys: FaceKeys): 'ktx2' | 'jpg' {
65
- const url = faceKeys.right || '';
66
- if (url.toLowerCase().includes('.ktx2')) return 'ktx2';
67
- if (url.toLowerCase().match(/\.(jpg|jpeg|png)$/)) return 'jpg';
68
- return 'ktx2'; // default assumption
69
- }
70
-
71
- // How many faces may be in flight at once. Not six: the KTX2 worker pool three.js gives us holds
72
- // four transcoder workers, each carrying its own copy of the basis wasm, and that same pool also
73
- // serves body textures and GLBs. Asking for all six together spawned every worker and queued the
74
- // other loaders behind the whole sky; measured on cold fetches it was no faster than three.
75
- const FACES_IN_FLIGHT = 3;
76
-
77
- // Load KTX2 faces and create skybox mesh
78
- async function loadFacesKTX2(faceKeys: FaceKeys, renderer: THREE.WebGLRenderer): Promise<SkyboxResource> {
79
- const loader = getKTX2LoaderForRenderer(renderer);
80
- const anisotropy = Math.min(8, renderer.capabilities.getMaxAnisotropy());
81
-
82
- const loadFace = async (face: FaceName) => {
83
- const tex = await loader.loadAsync(faceKeys[face]);
84
- tex.flipY = false;
85
- tex.wrapS = THREE.ClampToEdgeWrapping;
86
- tex.wrapT = THREE.ClampToEdgeWrapping;
87
- tex.anisotropy = anisotropy;
88
- return { face, tex };
89
- };
90
-
91
- // Faces overlap instead of chaining. Awaiting each one in turn made every face wait on the
92
- // previous face's download *and* its transcode, so the sky arrived six round trips deep.
93
- const settled = [];
94
- for (let i = 0; i < FACE_ORDER.length; i += FACES_IN_FLIGHT) {
95
- const wave = FACE_ORDER.slice(i, i + FACES_IN_FLIGHT).map(face => loadFace(face));
96
- settled.push(...await Promise.allSettled(wave));
97
- }
98
-
99
- // allSettled, not all: a bare Promise.all rejects on the first failure and leaks the faces that
100
- // land after it. Settle every face first, then dispose everything that arrived before rethrowing.
101
- const loaded: Awaited<ReturnType<typeof loadFace>>[] = [];
102
- let failure: unknown = null;
103
- for (const result of settled) {
104
- if (result.status === 'fulfilled') loaded.push(result.value);
105
- else if (!failure) failure = result.reason;
106
- }
107
-
108
- if (failure) {
109
- // Dispose already loaded textures on failure
110
- loaded.forEach(f => f.tex.dispose?.());
111
- throw failure;
112
- }
113
-
114
- const size = 500;
115
- const box = new THREE.BoxGeometry(size, size, size);
116
- // Map faces to geometry indices (corrected for orientation)
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);
119
-
120
- loaded.forEach(({ face, tex }) => {
121
- mats[orderMap[face]] = new THREE.MeshBasicMaterial({
122
- map: tex,
123
- side: THREE.BackSide,
124
- depthWrite: false
125
- });
126
- });
127
-
128
- // Fill any missing materials with black
129
- for (let i = 0; i < 6; i++) {
130
- if (!mats[i]) {
131
- mats[i] = new THREE.MeshBasicMaterial({
132
- color: 0x000000,
133
- side: THREE.BackSide,
134
- depthWrite: false
135
- });
136
- }
137
- }
138
-
139
- // Every slot is filled by now (a face's material or black).
140
- const mesh = new THREE.Mesh(box, mats as THREE.MeshBasicMaterial[]);
141
- mesh.name = 'SkyboxKTX2Mesh';
142
- return { __ewType: 'skybox-mesh', mesh };
143
- }
144
-
145
- // Load JPG/PNG faces and create CubeTexture
146
- function loadFacesJPG(faceKeys: FaceKeys): Promise<THREE.CubeTexture> {
147
- const urls = FACE_ORDER.map(face => faceKeys[face]);
148
- return new Promise((resolve, reject) => {
149
- new THREE.CubeTextureLoader().load(
150
- urls,
151
- tex => {
152
- tex.magFilter = THREE.LinearFilter;
153
- tex.minFilter = THREE.LinearMipMapLinearFilter;
154
- tex.anisotropy = 8;
155
- resolve(tex);
156
- },
157
- undefined,
158
- err => reject(err)
159
- );
160
- });
161
- }
162
-
163
- // Resolve relative paths to full URLs
164
- function resolveUrls(faceKeys: FaceKeys): FaceKeys {
165
- const PUBLIC_BASE = getPublicBase();
166
- // Every face is set below.
167
- const resolved = {} as FaceKeys;
168
- FACE_ORDER.forEach(face => {
169
- const url = faceKeys[face];
170
- if (url && url.startsWith('/')) {
171
- resolved[face] = `${PUBLIC_BASE}${url}`;
172
- } else {
173
- resolved[face] = url; // already absolute or invalid
174
- }
175
- });
176
- return resolved;
177
- }
178
-
179
- /**
180
- * Main loader function - always uses explicit face URLs.
181
- * `renderer` binds the KTX2 transcoder to the right WebGL context. REQUIRED (3.0) when the KTX2
182
- * path is taken — the old `window.__EW_RENDERER` fallback is gone; thread the owning view's
183
- * renderer. (The PNG/JPG path is renderer-agnostic, so its absence only bites on KTX2.)
184
- *
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.
191
- */
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> {
205
-
206
- // If no face URLs provided, build them from skycube name
207
- if (!faceKeys && skycubeName) {
208
- const low = typeof forceLow === 'boolean' ? forceLow : isLowPerformanceDevice();
209
- const extension = preferKTX2 ? 'ktx2' : 'jpg';
210
- const id = normalizeSkycubeName(skycubeName);
211
- const suffix = low ? '_low' : '';
212
- const relativePath = `/default/skybox/${id}${suffix}`;
213
-
214
- faceKeys = {
215
- right: `${relativePath}/right.${extension}`,
216
- left: `${relativePath}/left.${extension}`,
217
- top: `${relativePath}/top.${extension}`,
218
- bottom: `${relativePath}/bottom.${extension}`,
219
- front: `${relativePath}/front.${extension}`,
220
- back: `${relativePath}/back.${extension}`
221
- };
222
- }
223
-
224
- if (!validatefaceKeys(faceKeys)) {
225
- throw new Error('Invalid face URLs provided');
226
- }
227
-
228
- // Resolve relative paths to full URLs
229
- const resolvedUrls = resolveUrls(faceKeys);
230
- const extension = detectExtension(resolvedUrls);
231
-
232
- // Try KTX2 first if preferred and supported
233
- if (preferKTX2 && extension === 'ktx2' && renderer) {
234
- try {
235
- return await loadFacesKTX2(resolvedUrls, renderer);
236
- } catch (e) {
237
- console.warn('[CubeMapLoader] KTX2 load failed, trying PNG fallback:', e);
238
- // Fall through to PNG attempt
239
- }
240
- }
241
-
242
- // Try PNG fallback (convert URLs if needed)
243
- // Every face is set below.
244
- const pngUrls = {} as FaceKeys;
245
- FACE_ORDER.forEach(face => {
246
- pngUrls[face] = resolvedUrls[face].replace(/\.ktx2$/, '.png');
247
- });
248
-
249
- try {
250
- return await loadFacesJPG(pngUrls);
251
- } catch (e) {
252
- console.error('[CubeMapLoader] All loading attempts failed:', e);
253
- throw new Error('Failed to load cube map with any format');
254
- }
255
- }
256
-
257
- // Dispose helpers to ensure clean swaps
258
- function disposeSkyboxMesh(mesh: THREE.Mesh) {
259
- try {
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];
263
- mats.forEach(mat => {
264
- if (!mat) return;
265
- if (mat.map && mat.map.dispose) mat.map.dispose();
266
- if (mat.dispose) mat.dispose();
267
- });
268
- if (mesh.geometry && mesh.geometry.dispose) mesh.geometry.dispose();
269
- } catch(e) {
270
- console.warn('[CubeMapLoader] Disposal error:', e);
271
- }
272
- }
273
-
274
- /**
275
- * What `loadCubeMap` resolves to: a skybox mesh wrapper (the KTX2 path) or a cube texture (the
276
- * PNG/JPG path). Read `mesh` to tell them apart.
277
- */
278
- export type SkyboxResource =
279
- | { __ewType: 'skybox-mesh'; mesh: THREE.Mesh }
280
- | (THREE.CubeTexture & { mesh?: undefined });
281
-
282
- /**
283
- * Show a loaded skybox as the scene's background. A previous skybox mesh or cube-texture background is
284
- * disposed (a plain-texture background is not). Takes what `loadCubeMap` resolved to (or any texture);
285
- * anything else, `null` included, clears it.
286
- *
287
- */
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;
295
- // Clean up previous skybox mesh
296
- if (sky.__EW_SKYBOX_MESH && sky.__EW_SKYBOX_MESH.parent === scene) {
297
- const oldMesh = sky.__EW_SKYBOX_MESH;
298
- scene.remove(oldMesh);
299
- disposeSkyboxMesh(oldMesh);
300
- sky.__EW_SKYBOX_MESH = null;
301
- }
302
-
303
- // Dispose previous CubeTexture background
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) {
307
- try {
308
- background.dispose();
309
- } catch(e) {
310
- console.warn('[CubeMapLoader] Background disposal error:', e);
311
- }
312
- }
313
-
314
- // Set new background
315
- if (res?.__ewType === 'skybox-mesh') {
316
- scene.background = null;
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;
321
- } else {
322
- scene.background = null;
323
- }
324
-
325
- // Dispatch update event for reactive systems
326
- try {
327
- window.dispatchEvent(new CustomEvent('ew:skycube-updated', {
328
- detail: { type: res?.__ewType || 'texture' }
329
- }));
330
- } catch(e) {
331
- console.warn('[CubeMapLoader] Event dispatch error:', e);
332
- }
333
- }
334
-
335
- export async function initSpaceScene(
336
- scene: THREE.Scene,
337
- { renderer }: { renderer?: THREE.WebGLRenderer | null } = {},
338
- ): Promise<void> {
339
- const faceKeys = buildSkycubefaceKeys('001');
340
- const cube = await loadCubeMap({ faceKeys, preferKTX2: true, renderer });
341
- setBackground(scene, cube);
342
- }