entangled-worlds-orbiters-shared 1.2.1 → 1.4.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/dist/clock/track-metadata.d.ts +57 -0
- package/dist/clock/track-metadata.js +66 -0
- package/package.json +21 -5
- package/src/clock/track-metadata.ts +113 -0
- package/src/core/lights.js +27 -10
- package/src/loaders/bodyTextureLoader.js +3 -12
- package/src/loaders/cubeMapLoader.js +2 -12
- package/src/loaders/glbLoader.js +2 -9
- package/src/loaders/ktx2.js +54 -0
- package/src/world/worldManager.js +13 -7
- package/src/world/worldTexture.js +2 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The slice of a track's data these resolvers read. Every field is optional and untyped at the leaf
|
|
3
|
+
* because the runtime objects come from different app layers (release metadata, effective owner
|
|
4
|
+
* settings, per-user overrides, audio analysis). Resolvers coerce leaves with Number(...) and ignore
|
|
5
|
+
* anything non-finite, so a permissive shape here is intentional — not every consumer fills every path.
|
|
6
|
+
*/
|
|
7
|
+
export interface SyncSettingsLike {
|
|
8
|
+
trackBpm?: unknown;
|
|
9
|
+
primaryGridMarkerTimeSec?: unknown;
|
|
10
|
+
gridMarkers?: ReadonlyArray<{
|
|
11
|
+
sourceTimeSec?: unknown;
|
|
12
|
+
} | null | undefined> | null;
|
|
13
|
+
gridStartTimeSec?: unknown;
|
|
14
|
+
}
|
|
15
|
+
export interface TrackMetadataLike {
|
|
16
|
+
trackBpm?: unknown;
|
|
17
|
+
primaryGridMarkerTimeSec?: unknown;
|
|
18
|
+
effectiveSettings?: {
|
|
19
|
+
sync?: SyncSettingsLike | null;
|
|
20
|
+
} | null;
|
|
21
|
+
audioAnalysis?: {
|
|
22
|
+
analysis?: {
|
|
23
|
+
tempo_bpm?: unknown;
|
|
24
|
+
} | null;
|
|
25
|
+
} | null;
|
|
26
|
+
wrap?: {
|
|
27
|
+
gridMarkers?: ReadonlyArray<{
|
|
28
|
+
sourceTimeSec?: unknown;
|
|
29
|
+
} | null | undefined> | null;
|
|
30
|
+
gridStartTimeSec?: unknown;
|
|
31
|
+
} | null;
|
|
32
|
+
}
|
|
33
|
+
export interface TrackDataLike {
|
|
34
|
+
track?: {
|
|
35
|
+
metadata?: TrackMetadataLike | null;
|
|
36
|
+
} | null;
|
|
37
|
+
release?: {
|
|
38
|
+
metadata?: TrackMetadataLike | null;
|
|
39
|
+
} | null;
|
|
40
|
+
metadata?: TrackMetadataLike | null;
|
|
41
|
+
trackUserSettings?: {
|
|
42
|
+
sync?: SyncSettingsLike | null;
|
|
43
|
+
} | null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the effective track BPM. Precedence (first finite, > 0 wins):
|
|
47
|
+
* owner `trackBpm` → owner `effectiveSettings.sync.trackBpm` → per-user `trackUserSettings.sync.trackBpm`
|
|
48
|
+
* → auto-detected `audioAnalysis.analysis.tempo_bpm`. Returns null when nothing resolves.
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveTrackBpmFromTrackData(trackData?: TrackDataLike | null): number | null;
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the primary grid-marker (downbeat) time in seconds — the grid offset that becomes
|
|
53
|
+
* `tempoMap[0].startSec`. Precedence (first finite, >= 0 wins) walks the explicit
|
|
54
|
+
* `primaryGridMarkerTimeSec`, then per-user grid markers / start time, then the wrap fallback.
|
|
55
|
+
* Returns null when nothing resolves (consumers default to 0).
|
|
56
|
+
*/
|
|
57
|
+
export declare function resolvePrimaryGridMarkerTimeSecFromTrackData(trackData?: TrackDataLike | null): number | null;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// ./clock/track-metadata — pure resolution of a track's tempo (BPM) and its primary grid-marker
|
|
2
|
+
// (downbeat) time from the loosely-typed metadata both apps already carry. Framework-free, no three,
|
|
3
|
+
// no react: it walks a fixed precedence (owner setting → per-user setting → auto-detected analysis)
|
|
4
|
+
// and returns the first finite value, so a consumer can feed `new MusicalProjection({ tempoMap: [{
|
|
5
|
+
// startSec: gridSec, bpm }] })`. Promoted out of the orbiters app (ORB-137) so EW reuses it verbatim.
|
|
6
|
+
function toPositiveNumber(value) {
|
|
7
|
+
const numeric = Number(value);
|
|
8
|
+
return Number.isFinite(numeric) && numeric > 0 ? numeric : null;
|
|
9
|
+
}
|
|
10
|
+
function toNonNegativeNumber(value) {
|
|
11
|
+
const numeric = Number(value);
|
|
12
|
+
return Number.isFinite(numeric) && numeric >= 0 ? numeric : null;
|
|
13
|
+
}
|
|
14
|
+
function firstResolvedValue(candidates, parser) {
|
|
15
|
+
for (const candidate of candidates) {
|
|
16
|
+
const resolved = parser(candidate);
|
|
17
|
+
if (resolved !== null) {
|
|
18
|
+
return resolved;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the effective track BPM. Precedence (first finite, > 0 wins):
|
|
25
|
+
* owner `trackBpm` → owner `effectiveSettings.sync.trackBpm` → per-user `trackUserSettings.sync.trackBpm`
|
|
26
|
+
* → auto-detected `audioAnalysis.analysis.tempo_bpm`. Returns null when nothing resolves.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveTrackBpmFromTrackData(trackData = null) {
|
|
29
|
+
return firstResolvedValue([
|
|
30
|
+
trackData?.track?.metadata?.trackBpm,
|
|
31
|
+
trackData?.release?.metadata?.trackBpm,
|
|
32
|
+
trackData?.metadata?.trackBpm,
|
|
33
|
+
trackData?.track?.metadata?.effectiveSettings?.sync?.trackBpm,
|
|
34
|
+
trackData?.release?.metadata?.effectiveSettings?.sync?.trackBpm,
|
|
35
|
+
trackData?.metadata?.effectiveSettings?.sync?.trackBpm,
|
|
36
|
+
trackData?.trackUserSettings?.sync?.trackBpm,
|
|
37
|
+
trackData?.track?.metadata?.audioAnalysis?.analysis?.tempo_bpm,
|
|
38
|
+
trackData?.release?.metadata?.audioAnalysis?.analysis?.tempo_bpm,
|
|
39
|
+
trackData?.metadata?.audioAnalysis?.analysis?.tempo_bpm,
|
|
40
|
+
], toPositiveNumber);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the primary grid-marker (downbeat) time in seconds — the grid offset that becomes
|
|
44
|
+
* `tempoMap[0].startSec`. Precedence (first finite, >= 0 wins) walks the explicit
|
|
45
|
+
* `primaryGridMarkerTimeSec`, then per-user grid markers / start time, then the wrap fallback.
|
|
46
|
+
* Returns null when nothing resolves (consumers default to 0).
|
|
47
|
+
*/
|
|
48
|
+
export function resolvePrimaryGridMarkerTimeSecFromTrackData(trackData = null) {
|
|
49
|
+
return firstResolvedValue([
|
|
50
|
+
trackData?.track?.metadata?.primaryGridMarkerTimeSec,
|
|
51
|
+
trackData?.release?.metadata?.primaryGridMarkerTimeSec,
|
|
52
|
+
trackData?.metadata?.primaryGridMarkerTimeSec,
|
|
53
|
+
trackData?.track?.metadata?.effectiveSettings?.sync?.primaryGridMarkerTimeSec,
|
|
54
|
+
trackData?.release?.metadata?.effectiveSettings?.sync?.primaryGridMarkerTimeSec,
|
|
55
|
+
trackData?.metadata?.effectiveSettings?.sync?.primaryGridMarkerTimeSec,
|
|
56
|
+
trackData?.trackUserSettings?.sync?.primaryGridMarkerTimeSec,
|
|
57
|
+
trackData?.trackUserSettings?.sync?.gridMarkers?.[0]?.sourceTimeSec,
|
|
58
|
+
trackData?.trackUserSettings?.sync?.gridStartTimeSec,
|
|
59
|
+
trackData?.track?.metadata?.wrap?.gridMarkers?.[0]?.sourceTimeSec,
|
|
60
|
+
trackData?.release?.metadata?.wrap?.gridMarkers?.[0]?.sourceTimeSec,
|
|
61
|
+
trackData?.metadata?.wrap?.gridMarkers?.[0]?.sourceTimeSec,
|
|
62
|
+
trackData?.track?.metadata?.wrap?.gridStartTimeSec,
|
|
63
|
+
trackData?.release?.metadata?.wrap?.gridStartTimeSec,
|
|
64
|
+
trackData?.metadata?.wrap?.gridStartTimeSec,
|
|
65
|
+
], toNonNegativeNumber);
|
|
66
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "entangled-worlds-orbiters-shared",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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",
|
|
@@ -13,10 +13,26 @@
|
|
|
13
13
|
"./core": "./src/core/index.js",
|
|
14
14
|
"./config": "./src/config/index.js",
|
|
15
15
|
"./utils": "./src/utils/index.js",
|
|
16
|
-
"./clock": {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
"./clock": {
|
|
17
|
+
"types": "./dist/clock/index.d.ts",
|
|
18
|
+
"import": "./dist/clock/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./clock/musical": {
|
|
21
|
+
"types": "./dist/clock/musical.d.ts",
|
|
22
|
+
"import": "./dist/clock/musical.js"
|
|
23
|
+
},
|
|
24
|
+
"./clock/track-metadata": {
|
|
25
|
+
"types": "./dist/clock/track-metadata.d.ts",
|
|
26
|
+
"import": "./dist/clock/track-metadata.js"
|
|
27
|
+
},
|
|
28
|
+
"./clock/sync": {
|
|
29
|
+
"types": "./dist/clock/sync.d.ts",
|
|
30
|
+
"import": "./dist/clock/sync.js"
|
|
31
|
+
},
|
|
32
|
+
"./clock/curves": {
|
|
33
|
+
"types": "./dist/clock/curves.d.ts",
|
|
34
|
+
"import": "./dist/clock/curves.js"
|
|
35
|
+
}
|
|
20
36
|
},
|
|
21
37
|
"files": [
|
|
22
38
|
"src/**/*",
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// ./clock/track-metadata — pure resolution of a track's tempo (BPM) and its primary grid-marker
|
|
2
|
+
// (downbeat) time from the loosely-typed metadata both apps already carry. Framework-free, no three,
|
|
3
|
+
// no react: it walks a fixed precedence (owner setting → per-user setting → auto-detected analysis)
|
|
4
|
+
// and returns the first finite value, so a consumer can feed `new MusicalProjection({ tempoMap: [{
|
|
5
|
+
// startSec: gridSec, bpm }] })`. Promoted out of the orbiters app (ORB-137) so EW reuses it verbatim.
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The slice of a track's data these resolvers read. Every field is optional and untyped at the leaf
|
|
9
|
+
* because the runtime objects come from different app layers (release metadata, effective owner
|
|
10
|
+
* settings, per-user overrides, audio analysis). Resolvers coerce leaves with Number(...) and ignore
|
|
11
|
+
* anything non-finite, so a permissive shape here is intentional — not every consumer fills every path.
|
|
12
|
+
*/
|
|
13
|
+
export interface SyncSettingsLike {
|
|
14
|
+
trackBpm?: unknown;
|
|
15
|
+
primaryGridMarkerTimeSec?: unknown;
|
|
16
|
+
gridMarkers?: ReadonlyArray<{ sourceTimeSec?: unknown } | null | undefined> | null;
|
|
17
|
+
gridStartTimeSec?: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TrackMetadataLike {
|
|
21
|
+
trackBpm?: unknown;
|
|
22
|
+
primaryGridMarkerTimeSec?: unknown;
|
|
23
|
+
effectiveSettings?: { sync?: SyncSettingsLike | null } | null;
|
|
24
|
+
audioAnalysis?: { analysis?: { tempo_bpm?: unknown } | null } | null;
|
|
25
|
+
wrap?: {
|
|
26
|
+
gridMarkers?: ReadonlyArray<{ sourceTimeSec?: unknown } | null | undefined> | null;
|
|
27
|
+
gridStartTimeSec?: unknown;
|
|
28
|
+
} | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TrackDataLike {
|
|
32
|
+
track?: { metadata?: TrackMetadataLike | null } | null;
|
|
33
|
+
release?: { metadata?: TrackMetadataLike | null } | null;
|
|
34
|
+
metadata?: TrackMetadataLike | null;
|
|
35
|
+
trackUserSettings?: { sync?: SyncSettingsLike | null } | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function toPositiveNumber(value: unknown): number | null {
|
|
39
|
+
const numeric = Number(value);
|
|
40
|
+
return Number.isFinite(numeric) && numeric > 0 ? numeric : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function toNonNegativeNumber(value: unknown): number | null {
|
|
44
|
+
const numeric = Number(value);
|
|
45
|
+
return Number.isFinite(numeric) && numeric >= 0 ? numeric : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function firstResolvedValue(
|
|
49
|
+
candidates: ReadonlyArray<unknown>,
|
|
50
|
+
parser: (value: unknown) => number | null,
|
|
51
|
+
): number | null {
|
|
52
|
+
for (const candidate of candidates) {
|
|
53
|
+
const resolved = parser(candidate);
|
|
54
|
+
if (resolved !== null) {
|
|
55
|
+
return resolved;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolve the effective track BPM. Precedence (first finite, > 0 wins):
|
|
63
|
+
* owner `trackBpm` → owner `effectiveSettings.sync.trackBpm` → per-user `trackUserSettings.sync.trackBpm`
|
|
64
|
+
* → auto-detected `audioAnalysis.analysis.tempo_bpm`. Returns null when nothing resolves.
|
|
65
|
+
*/
|
|
66
|
+
export function resolveTrackBpmFromTrackData(trackData: TrackDataLike | null = null): number | null {
|
|
67
|
+
return firstResolvedValue(
|
|
68
|
+
[
|
|
69
|
+
trackData?.track?.metadata?.trackBpm,
|
|
70
|
+
trackData?.release?.metadata?.trackBpm,
|
|
71
|
+
trackData?.metadata?.trackBpm,
|
|
72
|
+
trackData?.track?.metadata?.effectiveSettings?.sync?.trackBpm,
|
|
73
|
+
trackData?.release?.metadata?.effectiveSettings?.sync?.trackBpm,
|
|
74
|
+
trackData?.metadata?.effectiveSettings?.sync?.trackBpm,
|
|
75
|
+
trackData?.trackUserSettings?.sync?.trackBpm,
|
|
76
|
+
trackData?.track?.metadata?.audioAnalysis?.analysis?.tempo_bpm,
|
|
77
|
+
trackData?.release?.metadata?.audioAnalysis?.analysis?.tempo_bpm,
|
|
78
|
+
trackData?.metadata?.audioAnalysis?.analysis?.tempo_bpm,
|
|
79
|
+
],
|
|
80
|
+
toPositiveNumber,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Resolve the primary grid-marker (downbeat) time in seconds — the grid offset that becomes
|
|
86
|
+
* `tempoMap[0].startSec`. Precedence (first finite, >= 0 wins) walks the explicit
|
|
87
|
+
* `primaryGridMarkerTimeSec`, then per-user grid markers / start time, then the wrap fallback.
|
|
88
|
+
* Returns null when nothing resolves (consumers default to 0).
|
|
89
|
+
*/
|
|
90
|
+
export function resolvePrimaryGridMarkerTimeSecFromTrackData(
|
|
91
|
+
trackData: TrackDataLike | null = null,
|
|
92
|
+
): number | null {
|
|
93
|
+
return firstResolvedValue(
|
|
94
|
+
[
|
|
95
|
+
trackData?.track?.metadata?.primaryGridMarkerTimeSec,
|
|
96
|
+
trackData?.release?.metadata?.primaryGridMarkerTimeSec,
|
|
97
|
+
trackData?.metadata?.primaryGridMarkerTimeSec,
|
|
98
|
+
trackData?.track?.metadata?.effectiveSettings?.sync?.primaryGridMarkerTimeSec,
|
|
99
|
+
trackData?.release?.metadata?.effectiveSettings?.sync?.primaryGridMarkerTimeSec,
|
|
100
|
+
trackData?.metadata?.effectiveSettings?.sync?.primaryGridMarkerTimeSec,
|
|
101
|
+
trackData?.trackUserSettings?.sync?.primaryGridMarkerTimeSec,
|
|
102
|
+
trackData?.trackUserSettings?.sync?.gridMarkers?.[0]?.sourceTimeSec,
|
|
103
|
+
trackData?.trackUserSettings?.sync?.gridStartTimeSec,
|
|
104
|
+
trackData?.track?.metadata?.wrap?.gridMarkers?.[0]?.sourceTimeSec,
|
|
105
|
+
trackData?.release?.metadata?.wrap?.gridMarkers?.[0]?.sourceTimeSec,
|
|
106
|
+
trackData?.metadata?.wrap?.gridMarkers?.[0]?.sourceTimeSec,
|
|
107
|
+
trackData?.track?.metadata?.wrap?.gridStartTimeSec,
|
|
108
|
+
trackData?.release?.metadata?.wrap?.gridStartTimeSec,
|
|
109
|
+
trackData?.metadata?.wrap?.gridStartTimeSec,
|
|
110
|
+
],
|
|
111
|
+
toNonNegativeNumber,
|
|
112
|
+
);
|
|
113
|
+
}
|
package/src/core/lights.js
CHANGED
|
@@ -1,21 +1,38 @@
|
|
|
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.
|
|
3
6
|
let hemiRef = null;
|
|
4
7
|
let ambientRef = null;
|
|
5
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Ensure a scene has the default hemisphere + ambient lights. Idempotent PER SCENE.
|
|
11
|
+
*
|
|
12
|
+
* ORB-128: a multi-orbiter realm renders N independent scenes through one renderer. A module-level
|
|
13
|
+
* singleton light is wrong twice over — it's only added to the FIRST scene, and a THREE light can
|
|
14
|
+
* live in just one scene (adding it to a second removes it from the first), so scenes 2..N rendered
|
|
15
|
+
* DARK ("only one entangled world"). So create + add the lights PER scene, keyed by name so a
|
|
16
|
+
* re-call on the same scene is a no-op. Single-orbiter (one scene) is unchanged.
|
|
17
|
+
*/
|
|
6
18
|
export function ensureDefaultLights(scene) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
19
|
+
let hemi = scene.getObjectByName('ew_HemisphereLight');
|
|
20
|
+
if (!hemi) {
|
|
21
|
+
hemi = new THREE.HemisphereLight(0xffffff, 0xffffff, 1.0);
|
|
22
|
+
hemi.position.set(1, 1, 1);
|
|
23
|
+
hemi.name = 'ew_HemisphereLight';
|
|
24
|
+
scene.add(hemi);
|
|
12
25
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
26
|
+
let ambient = scene.getObjectByName('ew_AmbientLight');
|
|
27
|
+
if (!ambient) {
|
|
28
|
+
ambient = new THREE.AmbientLight(0xffffff, 1.0);
|
|
29
|
+
ambient.name = 'ew_AmbientLight';
|
|
30
|
+
scene.add(ambient);
|
|
17
31
|
}
|
|
18
|
-
|
|
32
|
+
// updateDefaultLights() (no scene arg) targets the most-recently ensured scene's lights.
|
|
33
|
+
hemiRef = hemi;
|
|
34
|
+
ambientRef = ambient;
|
|
35
|
+
return { hemi, ambient };
|
|
19
36
|
}
|
|
20
37
|
|
|
21
38
|
export function updateDefaultLights({ ambientIntensity, diffuseIntensity, lightColor, lightDirection } = {}) {
|
|
@@ -1,21 +1,12 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
|
-
import {
|
|
2
|
+
import { getKTX2LoaderForRenderer } from './ktx2.js';
|
|
3
3
|
|
|
4
4
|
const textureLoader = new THREE.TextureLoader();
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
function getSharedKTX2Loader() {
|
|
8
|
-
if (window.__EW_KTX2_LOADER) return window.__EW_KTX2_LOADER;
|
|
9
|
-
const renderer = window.__EW_RENDERER;
|
|
10
|
-
const loader = new KTX2Loader().setTranscoderPath('/basis/').detectSupport(renderer);
|
|
11
|
-
window.__EW_KTX2_LOADER = loader;
|
|
12
|
-
return loader;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function loadTexture(url, { signal } = {}) {
|
|
6
|
+
export function loadTexture(url, { signal, renderer } = {}) {
|
|
16
7
|
return new Promise((resolve, reject) => {
|
|
17
8
|
if (url && url.endsWith('.ktx2')) {
|
|
18
|
-
const ktx2 =
|
|
9
|
+
const ktx2 = getKTX2LoaderForRenderer(renderer);
|
|
19
10
|
const onAbort = () => reject(new Error('aborted'));
|
|
20
11
|
if (signal) signal.addEventListener('abort', onAbort, { once: true });
|
|
21
12
|
ktx2.load(
|
|
@@ -1,19 +1,9 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
|
-
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
|
|
3
2
|
import { getPublicBase } from '../config/env.js';
|
|
3
|
+
import { getKTX2LoaderForRenderer } from './ktx2.js';
|
|
4
4
|
|
|
5
5
|
// Base URL is resolved at runtime via helper
|
|
6
6
|
|
|
7
|
-
// Shared loader + caches
|
|
8
|
-
const ktx2SupportCache = new Map(); // base -> true|false
|
|
9
|
-
|
|
10
|
-
function getSharedKTX2Loader(renderer) {
|
|
11
|
-
if (window.__EW_KTX2_LOADER) return window.__EW_KTX2_LOADER;
|
|
12
|
-
const loader = new KTX2Loader().setTranscoderPath('/basis/').detectSupport(renderer);
|
|
13
|
-
window.__EW_KTX2_LOADER = loader;
|
|
14
|
-
return loader;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
7
|
export function normalizeSkycubeName(raw) {
|
|
18
8
|
if (!raw) return '001';
|
|
19
9
|
const legacy = raw.match(/^(\d{1,2})_skycube(?:_low)?$/);
|
|
@@ -68,7 +58,7 @@ function detectExtension(faceKeys) {
|
|
|
68
58
|
|
|
69
59
|
// Load KTX2 faces and create skybox mesh
|
|
70
60
|
async function loadFacesKTX2(faceKeys, renderer) {
|
|
71
|
-
const loader =
|
|
61
|
+
const loader = getKTX2LoaderForRenderer(renderer);
|
|
72
62
|
const loaded = [];
|
|
73
63
|
|
|
74
64
|
for (const face of FACE_ORDER) {
|
package/src/loaders/glbLoader.js
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
|
3
|
-
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
|
|
4
3
|
import { ensureStandardMaterial, applyFlagsToMaterial } from '../utils/materials.js';
|
|
5
|
-
|
|
6
|
-
function getSharedKTX2Loader(renderer) {
|
|
7
|
-
if (window.__EW_KTX2_LOADER) return window.__EW_KTX2_LOADER;
|
|
8
|
-
const loader = new KTX2Loader().setTranscoderPath('/basis/').detectSupport(renderer);
|
|
9
|
-
window.__EW_KTX2_LOADER = loader;
|
|
10
|
-
return loader;
|
|
11
|
-
}
|
|
4
|
+
import { getKTX2LoaderForRenderer } from './ktx2.js';
|
|
12
5
|
|
|
13
6
|
export async function loadGLB(url, { retries = 3, signal, onProgress, applyFlags, withCredentials = false } = {}) {
|
|
14
7
|
const renderer = window.__EW_RENDERER;
|
|
@@ -20,7 +13,7 @@ export async function loadGLB(url, { retries = 3, signal, onProgress, applyFlags
|
|
|
20
13
|
const loader = new GLTFLoader()
|
|
21
14
|
.setCrossOrigin(withCredentials ? 'use-credentials' : 'anonymous')
|
|
22
15
|
.setWithCredentials(Boolean(withCredentials))
|
|
23
|
-
.setKTX2Loader(
|
|
16
|
+
.setKTX2Loader(getKTX2LoaderForRenderer(renderer));
|
|
24
17
|
|
|
25
18
|
const root = await new Promise((resolve, reject) => {
|
|
26
19
|
loader.load(
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One KTX2Loader PER RENDERER.
|
|
5
|
+
*
|
|
6
|
+
* `KTX2Loader.detectSupport(renderer)` binds the loader's Basis transcoder worker pool to ONE WebGL
|
|
7
|
+
* context. A single shared loader therefore only decodes textures for the FIRST renderer it saw —
|
|
8
|
+
* every other view's planets/skyboxes silently fail and render untextured (dark). That breaks any
|
|
9
|
+
* multi-view page: N orbiters in one realm, or N Entangled World planets shown together.
|
|
10
|
+
*
|
|
11
|
+
* Keying the loader by renderer gives each view its own correctly-bound loader. Single-renderer apps
|
|
12
|
+
* (a single orbiter, today's Entangled World) get exactly ONE entry, so behaviour is byte-identical.
|
|
13
|
+
* (Was: a single `window.__EW_KTX2_LOADER` global shared by bodyTexture/glb/cubeMap loaders.)
|
|
14
|
+
*/
|
|
15
|
+
const loadersByRenderer = new WeakMap();
|
|
16
|
+
let fallbackLoader = null;
|
|
17
|
+
|
|
18
|
+
function makeLoader(renderer) {
|
|
19
|
+
return new KTX2Loader().setTranscoderPath('/basis/').detectSupport(renderer);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The KTX2Loader bound to `renderer`. Defaults to the current `window.__EW_RENDERER` so existing call
|
|
24
|
+
* sites need no change; pass the renderer explicitly when it is in scope (preferred).
|
|
25
|
+
* @param {import('three').WebGLRenderer} [renderer]
|
|
26
|
+
* @returns {KTX2Loader}
|
|
27
|
+
*/
|
|
28
|
+
export function getKTX2LoaderForRenderer(
|
|
29
|
+
renderer = (typeof window !== 'undefined' ? window.__EW_RENDERER : null),
|
|
30
|
+
) {
|
|
31
|
+
if (!renderer) {
|
|
32
|
+
if (!fallbackLoader) fallbackLoader = makeLoader(renderer);
|
|
33
|
+
return fallbackLoader;
|
|
34
|
+
}
|
|
35
|
+
let loader = loadersByRenderer.get(renderer);
|
|
36
|
+
if (!loader) {
|
|
37
|
+
loader = makeLoader(renderer);
|
|
38
|
+
loadersByRenderer.set(renderer, loader);
|
|
39
|
+
}
|
|
40
|
+
return loader;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Dispose and forget the KTX2Loader for a renderer — call on that renderer's / scene's teardown so the
|
|
45
|
+
* transcoder workers are terminated (avoids leaking worker threads + GPU memory across view churn).
|
|
46
|
+
* @param {import('three').WebGLRenderer} renderer
|
|
47
|
+
*/
|
|
48
|
+
export function disposeKTX2LoaderForRenderer(renderer) {
|
|
49
|
+
const loader = renderer && loadersByRenderer.get(renderer);
|
|
50
|
+
if (loader) {
|
|
51
|
+
try { loader.dispose?.(); } catch { /* already gone */ }
|
|
52
|
+
loadersByRenderer.delete(renderer);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -4,8 +4,12 @@ import { makeSphere } from './worldTexture.js';
|
|
|
4
4
|
import { makeNormalSphere } from './worldNormal.js';
|
|
5
5
|
|
|
6
6
|
export class WorldManager {
|
|
7
|
-
constructor(scene, { onLoadStart, onLoadEnd } = {}) {
|
|
7
|
+
constructor(scene, { onLoadStart, onLoadEnd, renderer } = {}) {
|
|
8
8
|
this.scene = scene;
|
|
9
|
+
// The renderer this world belongs to — threaded to the texture loaders so each view's textures
|
|
10
|
+
// bind to its OWN WebGL context (multi-view: N orbiters / N planets in one page). Falls back to
|
|
11
|
+
// window.__EW_RENDERER for callers that don't pass one, keeping single-renderer apps unchanged.
|
|
12
|
+
this.renderer = renderer ?? null;
|
|
9
13
|
this.current = null;
|
|
10
14
|
this.flags = {};
|
|
11
15
|
this.onLoadStart = onLoadStart;
|
|
@@ -37,20 +41,22 @@ export class WorldManager {
|
|
|
37
41
|
this.onLoadStart?.();
|
|
38
42
|
try {
|
|
39
43
|
let obj;
|
|
40
|
-
|
|
44
|
+
const renderer = this.renderer ?? (typeof window !== 'undefined' ? window.__EW_RENDERER : null);
|
|
45
|
+
|
|
41
46
|
// Check for custom creators first
|
|
42
47
|
if (this.customCreators.has(mode)) {
|
|
43
|
-
obj = await this.customCreators.get(mode)({ flags: this.flags, ...args });
|
|
48
|
+
obj = await this.customCreators.get(mode)({ flags: this.flags, ...args, renderer });
|
|
44
49
|
} else if (mode === 2) {
|
|
45
50
|
obj = await makeGLB({
|
|
46
51
|
modelUrl: args.modelUrl,
|
|
47
52
|
flags: this.flags,
|
|
48
53
|
withCredentials: Boolean(args.withCredentials),
|
|
54
|
+
renderer,
|
|
49
55
|
});
|
|
50
56
|
} else if (mode === 3) {
|
|
51
|
-
obj = await makeNormalSphere({ flags: this.flags, ...args });
|
|
57
|
+
obj = await makeNormalSphere({ flags: this.flags, ...args, renderer });
|
|
52
58
|
} else {
|
|
53
|
-
obj = await makeSphere({ textureUrl: args.textureUrl, flags: this.flags, ...args });
|
|
59
|
+
obj = await makeSphere({ textureUrl: args.textureUrl, flags: this.flags, ...args, renderer });
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
if (this.token !== myToken) {
|
|
@@ -61,12 +67,12 @@ export class WorldManager {
|
|
|
61
67
|
this.current = obj;
|
|
62
68
|
|
|
63
69
|
// If the object exposes a frame updater, run a render-time loop
|
|
64
|
-
if (obj && obj.userData && typeof obj.userData.update === 'function' &&
|
|
70
|
+
if (obj && obj.userData && typeof obj.userData.update === 'function' && renderer) {
|
|
65
71
|
const updater = obj.userData.update;
|
|
66
72
|
const tick = () => {
|
|
67
73
|
if (this.current === obj) {
|
|
68
74
|
try {
|
|
69
|
-
updater(
|
|
75
|
+
updater(renderer, performance.now() * 0.001);
|
|
70
76
|
} catch (e) {
|
|
71
77
|
console.warn('[WorldManager] update loop error:', e);
|
|
72
78
|
}
|
|
@@ -52,6 +52,7 @@ function isLikelyEquirectangular(img) {
|
|
|
52
52
|
|
|
53
53
|
export async function makeSphere({
|
|
54
54
|
textureUrl,
|
|
55
|
+
renderer,
|
|
55
56
|
flags,
|
|
56
57
|
uOffset,
|
|
57
58
|
textureOffsetDeg,
|
|
@@ -169,7 +170,7 @@ export async function makeSphere({
|
|
|
169
170
|
const chosenTexture = textureUrl; // require explicit textureUrl — no implicit default
|
|
170
171
|
if (chosenTexture) {
|
|
171
172
|
try {
|
|
172
|
-
const tex = await loadTexture(chosenTexture);
|
|
173
|
+
const tex = await loadTexture(chosenTexture, { renderer });
|
|
173
174
|
tex.colorSpace = THREE.SRGBColorSpace;
|
|
174
175
|
tex.anisotropy = 8;
|
|
175
176
|
if (typeof flipY === 'boolean') tex.flipY = flipY;
|