entangled-worlds-orbiters-shared 3.3.0 → 4.1.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 +3 -2
- package/dist/clock/Transport.d.ts +8 -2
- package/dist/clock/Transport.js +46 -7
- package/dist/clock/musical.js +13 -8
- package/dist/clock/sync.js +15 -2
- package/dist/config/env.d.ts +8 -0
- package/dist/config/env.js +36 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.js +4 -0
- package/dist/config/textureConfig.d.ts +140 -0
- package/dist/config/textureConfig.js +318 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +3 -0
- package/dist/core/lightFresnel.d.ts +6 -0
- package/dist/core/lightFresnel.js +63 -0
- package/dist/core/lights.d.ts +48 -0
- package/dist/core/lights.js +79 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/loaders/bodyTextureLoader.d.ts +10 -0
- package/dist/loaders/bodyTextureLoader.js +28 -0
- package/dist/loaders/cubeMapLoader.d.ts +58 -0
- package/dist/loaders/cubeMapLoader.js +291 -0
- package/dist/loaders/glbLoader.d.ts +24 -0
- package/dist/loaders/glbLoader.js +85 -0
- package/dist/loaders/index.d.ts +5 -0
- package/{src → dist}/loaders/index.js +0 -2
- package/dist/{types/loaders → loaders}/ktx2.d.ts +4 -6
- package/dist/loaders/ktx2.js +48 -0
- package/dist/moons/index.d.ts +3 -0
- package/dist/moons/index.js +3 -0
- package/dist/moons/moonsGeneration.d.ts +64 -0
- package/dist/moons/moonsGeneration.js +238 -0
- package/dist/moons/moonsManager.d.ts +43 -0
- package/dist/moons/moonsManager.js +93 -0
- package/dist/{types/render → render}/frameBudget.d.ts +18 -17
- package/dist/render/frameBudget.js +42 -0
- package/dist/render/index.d.ts +4 -0
- package/dist/render/viewportCompositor.d.ts +124 -0
- package/dist/render/viewportCompositor.js +643 -0
- package/dist/studio/headerBroadcast.d.ts +10 -1
- package/dist/studio/hostTransport.d.ts +2 -2
- package/dist/studio/index.d.ts +1 -1
- package/dist/utils/dispose.d.ts +5 -0
- package/dist/utils/dispose.js +50 -0
- package/dist/utils/index.d.ts +3 -0
- package/{src → dist}/utils/index.js +1 -1
- package/dist/utils/materials.d.ts +10 -0
- package/dist/utils/materials.js +36 -0
- package/dist/world/cloudShell.d.ts +16 -0
- package/dist/world/cloudShell.js +73 -0
- package/dist/world/index.d.ts +6 -0
- package/{src → dist}/world/index.js +0 -2
- package/dist/world/worldGLB.d.ts +17 -0
- package/dist/world/worldGLB.js +22 -0
- package/dist/world/worldManager.d.ts +57 -0
- package/dist/world/worldManager.js +125 -0
- package/dist/world/worldNormal.d.ts +30 -0
- package/dist/world/worldNormal.js +58 -0
- package/dist/world/worldTexture.d.ts +56 -0
- package/dist/world/worldTexture.js +299 -0
- package/package.json +29 -39
- package/src/clock/Transport.ts +37 -6
- package/src/clock/musical.ts +18 -8
- package/src/clock/sync.ts +15 -2
- package/src/config/env.ts +48 -0
- package/src/config/{index.js → index.ts} +7 -0
- package/src/config/{textureConfig.js → textureConfig.ts} +126 -60
- package/src/core/{lightFresnel.js → lightFresnel.ts} +1 -1
- package/src/core/{lights.js → lights.ts} +21 -21
- package/src/loaders/{bodyTextureLoader.js → bodyTextureLoader.ts} +10 -7
- package/src/loaders/{cubeMapLoader.js → cubeMapLoader.ts} +83 -49
- package/src/loaders/glbLoader.ts +110 -0
- package/src/loaders/index.ts +6 -0
- package/src/loaders/{ktx2.js → ktx2.ts} +5 -7
- package/src/moons/{index.js → index.ts} +1 -0
- package/src/moons/{moonsGeneration.js → moonsGeneration.ts} +45 -32
- package/src/moons/{moonsManager.js → moonsManager.ts} +37 -11
- package/src/render/{frameBudget.js → frameBudget.ts} +15 -8
- package/src/render/index.ts +14 -0
- package/src/render/{viewportCompositor.js → viewportCompositor.ts} +121 -59
- package/src/studio/headerBroadcast.ts +14 -1
- package/src/studio/hostTransport.ts +2 -1
- package/src/studio/index.ts +1 -1
- package/src/utils/dispose.ts +47 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/materials.ts +44 -0
- package/src/world/{cloudShell.js → cloudShell.ts} +19 -12
- package/src/world/index.ts +7 -0
- package/src/world/worldGLB.ts +41 -0
- package/src/world/{worldManager.js → worldManager.ts} +67 -32
- package/src/world/{worldNormal.js → worldNormal.ts} +25 -20
- package/src/world/{worldTexture.js → worldTexture.ts} +70 -51
- package/dist/types/config/env.d.ts +0 -12
- package/dist/types/config/index.d.ts +0 -2
- package/dist/types/config/textureConfig.d.ts +0 -121
- package/dist/types/core/index.d.ts +0 -2
- package/dist/types/core/lightFresnel.d.ts +0 -5
- package/dist/types/core/lights.d.ts +0 -58
- package/dist/types/index.d.ts +0 -6
- package/dist/types/loaders/bodyTextureLoader.d.ts +0 -12
- package/dist/types/loaders/cubeMapLoader.d.ts +0 -70
- package/dist/types/loaders/glbLoader.d.ts +0 -24
- package/dist/types/loaders/index.d.ts +0 -5
- package/dist/types/moons/index.d.ts +0 -2
- package/dist/types/moons/moonsGeneration.d.ts +0 -72
- package/dist/types/moons/moonsManager.d.ts +0 -27
- package/dist/types/render/index.d.ts +0 -2
- package/dist/types/render/viewportCompositor.d.ts +0 -58
- package/dist/types/utils/dispose.d.ts +0 -2
- package/dist/types/utils/index.d.ts +0 -2
- package/dist/types/utils/materials.d.ts +0 -2
- package/dist/types/world/cloudShell.d.ts +0 -25
- package/dist/types/world/index.d.ts +0 -6
- package/dist/types/world/worldGLB.d.ts +0 -20
- package/dist/types/world/worldManager.d.ts +0 -63
- package/dist/types/world/worldNormal.d.ts +0 -43
- package/dist/types/world/worldTexture.d.ts +0 -80
- package/src/config/env.js +0 -38
- package/src/loaders/glbLoader.js +0 -78
- package/src/utils/dispose.js +0 -39
- package/src/utils/materials.js +0 -31
- package/src/world/worldGLB.js +0 -19
- /package/{src → dist}/render/index.js +0 -0
- /package/src/core/{index.js → index.ts} +0 -0
- /package/src/{index.js → index.ts} +0 -0
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@ In `entangled-worlds-orbiters-shared/`:
|
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
npm install
|
|
24
|
+
npm run build # consumers run dist/: build again after every change
|
|
24
25
|
npm link
|
|
25
26
|
```
|
|
26
27
|
|
|
@@ -145,10 +146,10 @@ Effects:
|
|
|
145
146
|
|
|
146
147
|
## Peer Dependencies
|
|
147
148
|
|
|
148
|
-
This library requires Three.js
|
|
149
|
+
This library requires Three.js as a peer dependency:
|
|
149
150
|
|
|
150
151
|
```bash
|
|
151
|
-
npm install three
|
|
152
|
+
npm install three
|
|
152
153
|
```
|
|
153
154
|
|
|
154
155
|
## License
|
|
@@ -71,9 +71,15 @@ export declare class Transport {
|
|
|
71
71
|
private _nextDist;
|
|
72
72
|
setProjection(projection: MusicalProjectionLike | null): void;
|
|
73
73
|
musicalPosition(): unknown;
|
|
74
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Apply a peer's anchored state. A malformed one (unknown playState, non-finite position, anchor or
|
|
76
|
+
* loop bounds, or a rate that is not a finite number > 0) is DROPPED whole and `false` is returned: the transport keeps its last
|
|
77
|
+
* good state until the next good one arrives. It never throws — this runs inside `tick()`.
|
|
78
|
+
*/
|
|
79
|
+
applyAnchoredState(s: AnchoredState): boolean;
|
|
75
80
|
/** Arm an anchored state (with optional control) to apply when audio time reaches `atAudioSec`. */
|
|
76
|
-
|
|
81
|
+
/** Returns `false` (and arms nothing) for a non-finite `atAudioSec` or a malformed state. */
|
|
82
|
+
scheduleStateChange(atAudioSec: number, state: AnchoredState, meta?: ScheduleStateMeta): boolean;
|
|
77
83
|
/** Drop pending changes older than a freshly-applied (term,seq) — supersession (Codex fix #4). */
|
|
78
84
|
dropPendingOlderThan(term: number, seq: number): void;
|
|
79
85
|
private _drainPending;
|
package/dist/clock/Transport.js
CHANGED
|
@@ -11,6 +11,22 @@
|
|
|
11
11
|
// - Looping is handled by computing each event's NEXT occurrence distance (wrapping across the loop
|
|
12
12
|
// boundary), so events just after loopStart are armed during the pre-wrap look-ahead.
|
|
13
13
|
const EPS = 1e-9;
|
|
14
|
+
function isValidRate(rate) {
|
|
15
|
+
return Number.isFinite(rate) && rate > 0;
|
|
16
|
+
}
|
|
17
|
+
function isValidAnchoredState(s) {
|
|
18
|
+
if (s.playState !== 'playing' && s.playState !== 'paused')
|
|
19
|
+
return false;
|
|
20
|
+
if (!Number.isFinite(s.positionSec))
|
|
21
|
+
return false;
|
|
22
|
+
if (s.anchorAudioSec != null && !Number.isFinite(s.anchorAudioSec))
|
|
23
|
+
return false;
|
|
24
|
+
if (s.rate != null && !isValidRate(s.rate))
|
|
25
|
+
return false;
|
|
26
|
+
if (s.loop && !(Number.isFinite(s.loop.startSec) && Number.isFinite(s.loop.endSec)))
|
|
27
|
+
return false;
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
14
30
|
export class Transport {
|
|
15
31
|
backend;
|
|
16
32
|
lookaheadSec;
|
|
@@ -63,13 +79,15 @@ export class Transport {
|
|
|
63
79
|
this._cancelArmed(); // armed future audio must not sound during pause; re-armed on resume
|
|
64
80
|
}
|
|
65
81
|
seekSec(sec) {
|
|
82
|
+
if (!Number.isFinite(sec))
|
|
83
|
+
return; // a NaN seek is ignored, never freezes the transport
|
|
66
84
|
this._reanchor(Math.max(0, sec));
|
|
67
85
|
this._cancelArmed();
|
|
68
86
|
this._reopenEvents(); // a seek MOVES position -> fired events may replay
|
|
69
87
|
}
|
|
70
88
|
setRate(rate) {
|
|
71
|
-
if (rate
|
|
72
|
-
throw new Error('rate must be > 0');
|
|
89
|
+
if (!isValidRate(rate))
|
|
90
|
+
throw new Error('rate must be a finite number > 0');
|
|
73
91
|
this._reanchor();
|
|
74
92
|
this._rate = rate;
|
|
75
93
|
this._cancelArmed(); // window/audio-time changed -> re-arm pending events (NOT replay fired ones)
|
|
@@ -81,6 +99,8 @@ export class Transport {
|
|
|
81
99
|
return this._playing;
|
|
82
100
|
}
|
|
83
101
|
setLoopSec(loop) {
|
|
102
|
+
if (loop && !(Number.isFinite(loop.startSec) && Number.isFinite(loop.endSec)))
|
|
103
|
+
return; // ignored
|
|
84
104
|
this._loop = loop ? { startSec: loop.startSec, endSec: loop.endSec } : null;
|
|
85
105
|
this._cancelArmed();
|
|
86
106
|
this._reopenEvents(); // loop topology changed -> events may re-enter/leave the loop window
|
|
@@ -149,6 +169,11 @@ export class Transport {
|
|
|
149
169
|
d += len; // wrapped into the next pass
|
|
150
170
|
return d;
|
|
151
171
|
}
|
|
172
|
+
// At or past the loop end: the transport wraps back to startSec before getting there, so the
|
|
173
|
+
// event never occurs while looping. Without this, its look-ahead distance came out positive just
|
|
174
|
+
// before the wrap and it armed (and sounded) at the wrap instant, at a position never reached.
|
|
175
|
+
if (len > 0 && ev.atSec >= endSec - EPS)
|
|
176
|
+
return null;
|
|
152
177
|
}
|
|
153
178
|
const d = ev.atSec - pos;
|
|
154
179
|
return d >= -EPS ? d : null; // non-loop: a passed event won't fire again until a seek
|
|
@@ -161,7 +186,14 @@ export class Transport {
|
|
|
161
186
|
return this._projection ? this._projection.musicalAt(this.positionSec()) : null;
|
|
162
187
|
}
|
|
163
188
|
// ---- sync apply (Inc 3) -------------------------------------------------------------
|
|
189
|
+
/**
|
|
190
|
+
* Apply a peer's anchored state. A malformed one (unknown playState, non-finite position, anchor or
|
|
191
|
+
* loop bounds, or a rate that is not a finite number > 0) is DROPPED whole and `false` is returned: the transport keeps its last
|
|
192
|
+
* good state until the next good one arrives. It never throws — this runs inside `tick()`.
|
|
193
|
+
*/
|
|
164
194
|
applyAnchoredState(s) {
|
|
195
|
+
if (!isValidAnchoredState(s))
|
|
196
|
+
return false;
|
|
165
197
|
this._playing = s.playState === 'playing';
|
|
166
198
|
if (s.rate != null)
|
|
167
199
|
this._rate = s.rate;
|
|
@@ -171,9 +203,14 @@ export class Transport {
|
|
|
171
203
|
this._loop = s.loop ? { ...s.loop } : null;
|
|
172
204
|
this._cancelArmed();
|
|
173
205
|
this._reopenEvents(); // anchored apply repositions the timeline -> events re-evaluate
|
|
206
|
+
return true;
|
|
174
207
|
}
|
|
175
208
|
/** Arm an anchored state (with optional control) to apply when audio time reaches `atAudioSec`. */
|
|
209
|
+
/** Returns `false` (and arms nothing) for a non-finite `atAudioSec` or a malformed state. */
|
|
176
210
|
scheduleStateChange(atAudioSec, state, meta = {}) {
|
|
211
|
+
// checked at intake too: a NaN time would never drain and would block every later change
|
|
212
|
+
if (!Number.isFinite(atAudioSec) || !isValidAnchoredState(state))
|
|
213
|
+
return false;
|
|
177
214
|
this._pending.push({
|
|
178
215
|
atAudioSec,
|
|
179
216
|
state,
|
|
@@ -182,6 +219,7 @@ export class Transport {
|
|
|
182
219
|
onApply: meta.onApply ?? null,
|
|
183
220
|
});
|
|
184
221
|
this._pending.sort((a, b) => a.atAudioSec - b.atAudioSec);
|
|
222
|
+
return true;
|
|
185
223
|
}
|
|
186
224
|
/** Drop pending changes older than a freshly-applied (term,seq) — supersession (Codex fix #4). */
|
|
187
225
|
dropPendingOlderThan(term, seq) {
|
|
@@ -191,9 +229,10 @@ export class Transport {
|
|
|
191
229
|
const now = this.backend.nowSec();
|
|
192
230
|
while (this._pending.length && this._pending[0].atAudioSec <= now + EPS) {
|
|
193
231
|
const p = this._pending.shift();
|
|
194
|
-
this.applyAnchoredState({ ...p.state, anchorAudioSec: p.state.anchorAudioSec ?? now });
|
|
195
|
-
|
|
196
|
-
|
|
232
|
+
const applied = this.applyAnchoredState({ ...p.state, anchorAudioSec: p.state.anchorAudioSec ?? now });
|
|
233
|
+
// control rides WITH the state, applied atomically here (fix #5) — a dropped state drops its control too
|
|
234
|
+
if (applied && p.onApply)
|
|
235
|
+
p.onApply();
|
|
197
236
|
}
|
|
198
237
|
}
|
|
199
238
|
// ---- the pump -----------------------------------------------------------------------
|
|
@@ -248,8 +287,8 @@ export class Transport {
|
|
|
248
287
|
return;
|
|
249
288
|
const { startSec, endSec } = this._loop;
|
|
250
289
|
const len = endSec - startSec;
|
|
251
|
-
if (len
|
|
252
|
-
return;
|
|
290
|
+
if (!(len > 0))
|
|
291
|
+
return; // also catches NaN
|
|
253
292
|
const pos = this.positionSec();
|
|
254
293
|
if (pos < endSec - EPS)
|
|
255
294
|
return;
|
package/dist/clock/musical.js
CHANGED
|
@@ -14,20 +14,25 @@ export class MusicalProjection {
|
|
|
14
14
|
_sig;
|
|
15
15
|
constructor(spec = {}) {
|
|
16
16
|
this.ppq = spec.ppq ?? DEFAULT_PPQ;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
// An entry with bpm <= 0, a NaN bpm or a non-finite start would make beat <-> seconds return
|
|
18
|
+
// Infinity/NaN — skip it. If nothing usable is left, fall back to the same 120 BPM default as an
|
|
19
|
+
// empty map.
|
|
20
|
+
const usable = (spec.tempoMap ?? []).filter((e) => Number.isFinite(e.startSec) && Number.isFinite(e.bpm) && e.bpm > 0);
|
|
21
|
+
const tempo = (usable.length ? usable : [{ startSec: 0, bpm: 120 }]).sort((a, b) => a.startSec - b.startSec);
|
|
20
22
|
// precompute the cumulative beat position at each tempo segment boundary
|
|
21
23
|
this._tempo = tempo.map((s) => ({ ...s, startBeat: 0 }));
|
|
22
24
|
for (let i = 1; i < this._tempo.length; i++) {
|
|
23
25
|
const prev = this._tempo[i - 1];
|
|
24
26
|
this._tempo[i].startBeat = prev.startBeat + (this._tempo[i].startSec - prev.startSec) * (prev.bpm / 60);
|
|
25
27
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
.
|
|
30
|
-
.
|
|
28
|
+
// Same rule for signatures: a zero/negative/NaN numerator or denominator, or a non-finite start
|
|
29
|
+
// bar, would give Infinity/NaN bars — skip it, and fall back to 4/4 if nothing usable is left.
|
|
30
|
+
const usableSig = (spec.signatureMap ?? []).filter((e) => Number.isFinite(e.startBar) &&
|
|
31
|
+
Number.isFinite(e.numerator) &&
|
|
32
|
+
e.numerator > 0 &&
|
|
33
|
+
Number.isFinite(e.denominator) &&
|
|
34
|
+
e.denominator > 0);
|
|
35
|
+
const sig = (usableSig.length ? usableSig : [{ startBar: 0, numerator: 4, denominator: 4 }]).sort((a, b) => a.startBar - b.startBar);
|
|
31
36
|
// precompute the cumulative beat position at each signature segment's startBar
|
|
32
37
|
this._sig = sig.map((s) => ({ ...s, beatsPerBar: (s.numerator * 4) / s.denominator, startBeat: 0 }));
|
|
33
38
|
for (let i = 1; i < this._sig.length; i++) {
|
package/dist/clock/sync.js
CHANGED
|
@@ -12,6 +12,13 @@
|
|
|
12
12
|
// Reuses the session-time substrate offset estimation; the server `sessionEpoch` gives every peer a
|
|
13
13
|
// shared `sessionMs`; the beat timeline is anchored to that.
|
|
14
14
|
const mod = (n, m) => ((n % m) + m) % m;
|
|
15
|
+
/** A usable timeline: a finite tempo above zero and finite anchors. Anything else makes every beat
|
|
16
|
+
* <-> time conversion NaN or Infinity, for this peer and for every peer that adopts it. */
|
|
17
|
+
const isValidTimeline = (t) => !!t &&
|
|
18
|
+
Number.isFinite(t.tempoBpm) &&
|
|
19
|
+
t.tempoBpm > 0 &&
|
|
20
|
+
Number.isFinite(t.anchorBeat) &&
|
|
21
|
+
Number.isFinite(t.anchorSessionMs);
|
|
15
22
|
const newerTs = (a, b) => a.time > b.time || (a.time === b.time && a.id > b.id);
|
|
16
23
|
/** Cristian/SNTP offset estimate: median of (serverStamp + rtt/2 - t1). Asymmetry biases by (up-down)/2. */
|
|
17
24
|
export function estimateOffset(node, relay, { samples = 7, upMs = relay.upMs, downMs = relay.downMs, jitterFn = () => 0 } = {}) {
|
|
@@ -132,6 +139,9 @@ export class BeatTimeline {
|
|
|
132
139
|
}
|
|
133
140
|
// ---- timeline mutations (all leaderless: broadcast + last-writer-wins) ---------------
|
|
134
141
|
_commit(timeline) {
|
|
142
|
+
// Validated before it is broadcast: last-writer-wins would hand a broken timeline to every peer.
|
|
143
|
+
if (!isValidTimeline(timeline))
|
|
144
|
+
throw new Error('beat timeline needs a finite tempo > 0 and finite anchors');
|
|
135
145
|
this.timeline = { ...timeline };
|
|
136
146
|
this._ts = { time: this.sessionNowMs(), id: this.id };
|
|
137
147
|
if (this.enabled)
|
|
@@ -177,7 +187,9 @@ export class BeatTimeline {
|
|
|
177
187
|
// ---- start/stop intent (opt-in; never adopted on join) ------------------------------
|
|
178
188
|
setLocalPlaying(playing) {
|
|
179
189
|
this.localPlaying = playing;
|
|
180
|
-
|
|
190
|
+
// Solo (`enabled` false) broadcasts nothing, as with the timeline and hello messages.
|
|
191
|
+
if (this.enabled)
|
|
192
|
+
this.relay.send(this.id, { kind: 'beat:transport', id: this.id, playing, atSessionMs: this.sessionNowMs() });
|
|
181
193
|
if (this.onChange)
|
|
182
194
|
this.onChange();
|
|
183
195
|
}
|
|
@@ -196,7 +208,8 @@ export class BeatTimeline {
|
|
|
196
208
|
case 'beat:timeline':
|
|
197
209
|
if (msg.id)
|
|
198
210
|
this.peers.add(msg.id);
|
|
199
|
-
|
|
211
|
+
// A malformed timeline (tempo 0, NaN anchor, from a buggy or older peer) is never adopted.
|
|
212
|
+
if (isValidTimeline(msg.timeline) && msg.ts && newerTs(msg.ts, this._ts)) {
|
|
200
213
|
this.timeline = { ...msg.timeline };
|
|
201
214
|
this._ts = msg.ts;
|
|
202
215
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare let __PUBLIC_BASE: string | null;
|
|
2
|
+
export declare function setPublicBase(url: string | null | undefined): void;
|
|
3
|
+
export declare function getPublicBase(): string;
|
|
4
|
+
/**
|
|
5
|
+
* @param path root-relative, relative, or an absolute http(s) URL (returned unchanged).
|
|
6
|
+
* An empty string comes back as is.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolvePublicUrl(path: string): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Runtime public base URL helper for building absolute asset URLs at runtime
|
|
2
|
+
// Mutable module-scoped variable to allow apps to override at startup
|
|
3
|
+
export let __PUBLIC_BASE = null;
|
|
4
|
+
// Set the public base (strips trailing slash) and mirror to window for convenience
|
|
5
|
+
export function setPublicBase(url) {
|
|
6
|
+
__PUBLIC_BASE = (url || '').replace(/\/$/, '');
|
|
7
|
+
if (typeof window !== 'undefined')
|
|
8
|
+
window.VITE_PUBLIC_HERBARIUM_BASE = __PUBLIC_BASE;
|
|
9
|
+
}
|
|
10
|
+
// Get the current public base from (in order): explicit set value, envs, or default
|
|
11
|
+
export function getPublicBase() {
|
|
12
|
+
// The cast stays inline on each read so the emitted JS is the literal `import.meta.env.X` a
|
|
13
|
+
// bundler looks for.
|
|
14
|
+
const envVal = (typeof import.meta !== 'undefined' &&
|
|
15
|
+
import.meta.env &&
|
|
16
|
+
import.meta.env.VITE_PUBLIC_HERBARIUM_BASE) ||
|
|
17
|
+
(typeof window !== 'undefined' && window.VITE_PUBLIC_HERBARIUM_BASE) ||
|
|
18
|
+
null;
|
|
19
|
+
const base = (__PUBLIC_BASE || envVal || '').toString();
|
|
20
|
+
return base ? base.replace(/\/$/, '') : '';
|
|
21
|
+
}
|
|
22
|
+
// Resolve a possibly-relative path against the current public base
|
|
23
|
+
/**
|
|
24
|
+
* @param path root-relative, relative, or an absolute http(s) URL (returned unchanged).
|
|
25
|
+
* An empty string comes back as is.
|
|
26
|
+
*/
|
|
27
|
+
export function resolvePublicUrl(path) {
|
|
28
|
+
if (!path)
|
|
29
|
+
return path;
|
|
30
|
+
if (/^https?:\/\//i.test(path))
|
|
31
|
+
return path;
|
|
32
|
+
const base = getPublicBase();
|
|
33
|
+
const p = path.startsWith('/') ? path : `/${path}`;
|
|
34
|
+
// If base is empty, return a root-relative path (same-origin)
|
|
35
|
+
return base ? `${base}${p}` : p;
|
|
36
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { loadTextureConfig, getTextureOptions, getTextureChoices, getDefaultTexture, getSkyboxOptions, getDefaultSkybox, getTextureFallbackUrl } from './textureConfig.js';
|
|
2
|
+
export type { TextureConfig, TextureCategory, TextureItem, TextureChoice, TextureConfigMetadata } from './textureConfig.js';
|
|
3
|
+
export { setPublicBase, getPublicBase, resolvePublicUrl } from './env.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Configuration exports
|
|
2
|
+
export { loadTextureConfig, getTextureOptions, getTextureChoices, getDefaultTexture, getSkyboxOptions, getDefaultSkybox, getTextureFallbackUrl } from './textureConfig.js';
|
|
3
|
+
// runtime base helpers
|
|
4
|
+
export { setPublicBase, getPublicBase, resolvePublicUrl } from './env.js';
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/** One catalogue item. */
|
|
2
|
+
export interface TextureItem {
|
|
3
|
+
id: string;
|
|
4
|
+
filename?: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
/** Picture of the item (a thumbnail), relative like `filename`. */
|
|
7
|
+
still?: string;
|
|
8
|
+
/** Texture to use when this one fails to load, relative like `filename`. */
|
|
9
|
+
fallback?: string;
|
|
10
|
+
tags?: string[];
|
|
11
|
+
}
|
|
12
|
+
/** One catalogue category (clouds, moons, skyboxes, …). */
|
|
13
|
+
export interface TextureCategory {
|
|
14
|
+
category?: string;
|
|
15
|
+
basePath?: string;
|
|
16
|
+
defaultTexture?: string;
|
|
17
|
+
defaultSkybox?: string;
|
|
18
|
+
items?: TextureItem[];
|
|
19
|
+
}
|
|
20
|
+
/** The texture catalogue as loaded from the config URL. */
|
|
21
|
+
export interface TextureConfig {
|
|
22
|
+
version?: string;
|
|
23
|
+
lastUpdated?: string;
|
|
24
|
+
baseUrl?: string;
|
|
25
|
+
/** By category name; a category the catalogue does not list is missing. */
|
|
26
|
+
textures: Partial<Record<string, TextureCategory>>;
|
|
27
|
+
}
|
|
28
|
+
/** What `getConfigMetadata` reports about a catalogue. */
|
|
29
|
+
export interface TextureConfigMetadata {
|
|
30
|
+
version: string | undefined;
|
|
31
|
+
lastUpdated: string | undefined;
|
|
32
|
+
categories: string[];
|
|
33
|
+
totalTextures: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Load texture configuration from remote endpoint or use passed config
|
|
37
|
+
* @param {string} configUrl - Override config URL
|
|
38
|
+
* @param {boolean} useCache - Whether to use cached config if available
|
|
39
|
+
* @returns {Promise<TextureConfig>} Texture configuration object
|
|
40
|
+
*/
|
|
41
|
+
export declare function loadTextureConfig(configUrl?: string, useCache?: boolean): Promise<TextureConfig>;
|
|
42
|
+
/**
|
|
43
|
+
* A choice record for one catalogue item.
|
|
44
|
+
*
|
|
45
|
+
* Every catalogue item IS a picture, so a chooser should show it rather than its name. `still` is
|
|
46
|
+
* read straight from the catalogue and never derived from `filename`: the stills do not share one
|
|
47
|
+
* extension, and a wrong guess fails silently (a broken `<img>` just shows nothing).
|
|
48
|
+
*/
|
|
49
|
+
export type TextureChoice = {
|
|
50
|
+
/** Catalogue id. */
|
|
51
|
+
id: string;
|
|
52
|
+
/** Human-readable name. */
|
|
53
|
+
label: string;
|
|
54
|
+
/** Resolved texture URL (the .ktx2 the world renders). */
|
|
55
|
+
url: string;
|
|
56
|
+
/**
|
|
57
|
+
* Resolved URL of the item's picture — a small thumbnail suitable for showing the texture in a
|
|
58
|
+
* picker. `null` when the catalogue names none.
|
|
59
|
+
*/
|
|
60
|
+
still: string | null;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Get the full choice records for a category (clouds, moons, etc.) — label, texture URL, and the
|
|
64
|
+
* picture to show for it. The single resolution point for a category's items; `getTextureOptions`
|
|
65
|
+
* is the flat `{ label: url }` projection of this.
|
|
66
|
+
* @param {string} category - Texture category (clouds, moons)
|
|
67
|
+
* @param {TextureConfig} config - Optional pre-loaded config object
|
|
68
|
+
* @returns {Promise<TextureChoice[]>} Choice records in catalogue order
|
|
69
|
+
*/
|
|
70
|
+
export declare function getTextureChoices(category: string, config?: TextureConfig | null): Promise<TextureChoice[]>;
|
|
71
|
+
/**
|
|
72
|
+
* Get texture options for a specific category (clouds, moons, etc.)
|
|
73
|
+
* @param {string} category - Texture category (clouds, moons, skyboxes)
|
|
74
|
+
* @param {TextureConfig} config - Optional pre-loaded config object
|
|
75
|
+
* @returns {Promise<Record<string, string>>} `{ label: url }`, suitable for dropdown option lists
|
|
76
|
+
*/
|
|
77
|
+
export declare function getTextureOptions(category: string, config?: TextureConfig | null): Promise<Record<string, string>>;
|
|
78
|
+
/**
|
|
79
|
+
* Get default texture for a category
|
|
80
|
+
* @param {string} category - Texture category
|
|
81
|
+
* @param {TextureConfig} config - Optional pre-loaded config object
|
|
82
|
+
* @returns {Promise<string|null>} Default texture path, or null when the category has no texture
|
|
83
|
+
*/
|
|
84
|
+
export declare function getDefaultTexture(category: string, config?: TextureConfig | null): Promise<string | null>;
|
|
85
|
+
/**
|
|
86
|
+
* Return the configured fallback URL for a given texture if defined.
|
|
87
|
+
* @param {string} category - Texture category (clouds, moons, etc.)
|
|
88
|
+
* @param {string} textureUrl - Resolved texture URL
|
|
89
|
+
* @param {TextureConfig|null} config - Optional texture config override
|
|
90
|
+
* @returns {string|null} Resolved fallback texture URL or null
|
|
91
|
+
*/
|
|
92
|
+
export declare function getTextureFallbackUrl(category: string, textureUrl: string, config?: TextureConfig | null): string | null;
|
|
93
|
+
/**
|
|
94
|
+
* Get texture by ID within a category
|
|
95
|
+
* @param {string} category - Texture category
|
|
96
|
+
* @param {string} textureId - Texture ID
|
|
97
|
+
* @param {TextureConfig} config - Optional pre-loaded config object
|
|
98
|
+
* @returns {Promise<TextureItem|null>} Texture definition object
|
|
99
|
+
*/
|
|
100
|
+
export declare function getTextureById(category: string, textureId: string, config?: TextureConfig | null): Promise<TextureItem | null>;
|
|
101
|
+
/**
|
|
102
|
+
* Get full URL for a texture
|
|
103
|
+
* @param {string} category - Texture category
|
|
104
|
+
* @param {string} textureId - Texture ID
|
|
105
|
+
* @param {TextureConfig} config - Optional pre-loaded config object
|
|
106
|
+
* @returns {Promise<string|null>} Full texture URL
|
|
107
|
+
*/
|
|
108
|
+
export declare function getTextureUrl(category: string, textureId: string, config?: TextureConfig | null): Promise<string | null>;
|
|
109
|
+
/**
|
|
110
|
+
* Search textures by tags
|
|
111
|
+
* @param {string} category - Texture category
|
|
112
|
+
* @param {string[]} tags - Tags to search for
|
|
113
|
+
* @param {TextureConfig} config - Optional pre-loaded config object
|
|
114
|
+
* @returns {Promise<TextureItem[]>} Array of matching texture definitions
|
|
115
|
+
*/
|
|
116
|
+
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
|
+
/**
|
|
130
|
+
* Reload configuration (force refresh)
|
|
131
|
+
* @param {string} configUrl - Override config URL
|
|
132
|
+
* @returns {Promise<TextureConfig>} Fresh configuration object
|
|
133
|
+
*/
|
|
134
|
+
export declare function reloadTextureConfig(configUrl?: string): Promise<TextureConfig>;
|
|
135
|
+
/**
|
|
136
|
+
* Get configuration metadata
|
|
137
|
+
* @param {TextureConfig} config - Optional pre-loaded config object
|
|
138
|
+
* @returns {Promise<TextureConfigMetadata>} Configuration metadata
|
|
139
|
+
*/
|
|
140
|
+
export declare function getConfigMetadata(config?: TextureConfig | null): Promise<TextureConfigMetadata>;
|