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.
Files changed (126) hide show
  1. package/README.md +3 -2
  2. package/dist/clock/Transport.d.ts +8 -2
  3. package/dist/clock/Transport.js +46 -7
  4. package/dist/clock/musical.js +13 -8
  5. package/dist/clock/sync.js +15 -2
  6. package/dist/config/env.d.ts +8 -0
  7. package/dist/config/env.js +36 -0
  8. package/dist/config/index.d.ts +3 -0
  9. package/dist/config/index.js +4 -0
  10. package/dist/config/textureConfig.d.ts +140 -0
  11. package/dist/config/textureConfig.js +318 -0
  12. package/dist/core/index.d.ts +2 -0
  13. package/dist/core/index.js +3 -0
  14. package/dist/core/lightFresnel.d.ts +6 -0
  15. package/dist/core/lightFresnel.js +63 -0
  16. package/dist/core/lights.d.ts +48 -0
  17. package/dist/core/lights.js +79 -0
  18. package/dist/index.d.ts +6 -0
  19. package/dist/index.js +7 -0
  20. package/dist/loaders/bodyTextureLoader.d.ts +10 -0
  21. package/dist/loaders/bodyTextureLoader.js +28 -0
  22. package/dist/loaders/cubeMapLoader.d.ts +58 -0
  23. package/dist/loaders/cubeMapLoader.js +291 -0
  24. package/dist/loaders/glbLoader.d.ts +24 -0
  25. package/dist/loaders/glbLoader.js +85 -0
  26. package/dist/loaders/index.d.ts +5 -0
  27. package/{src → dist}/loaders/index.js +0 -2
  28. package/dist/{types/loaders → loaders}/ktx2.d.ts +4 -6
  29. package/dist/loaders/ktx2.js +48 -0
  30. package/dist/moons/index.d.ts +3 -0
  31. package/dist/moons/index.js +3 -0
  32. package/dist/moons/moonsGeneration.d.ts +64 -0
  33. package/dist/moons/moonsGeneration.js +238 -0
  34. package/dist/moons/moonsManager.d.ts +43 -0
  35. package/dist/moons/moonsManager.js +93 -0
  36. package/dist/{types/render → render}/frameBudget.d.ts +18 -17
  37. package/dist/render/frameBudget.js +42 -0
  38. package/dist/render/index.d.ts +4 -0
  39. package/dist/render/viewportCompositor.d.ts +124 -0
  40. package/dist/render/viewportCompositor.js +643 -0
  41. package/dist/studio/headerBroadcast.d.ts +10 -1
  42. package/dist/studio/hostTransport.d.ts +2 -2
  43. package/dist/studio/index.d.ts +1 -1
  44. package/dist/utils/dispose.d.ts +5 -0
  45. package/dist/utils/dispose.js +50 -0
  46. package/dist/utils/index.d.ts +3 -0
  47. package/{src → dist}/utils/index.js +1 -1
  48. package/dist/utils/materials.d.ts +10 -0
  49. package/dist/utils/materials.js +36 -0
  50. package/dist/world/cloudShell.d.ts +16 -0
  51. package/dist/world/cloudShell.js +73 -0
  52. package/dist/world/index.d.ts +6 -0
  53. package/{src → dist}/world/index.js +0 -2
  54. package/dist/world/worldGLB.d.ts +17 -0
  55. package/dist/world/worldGLB.js +22 -0
  56. package/dist/world/worldManager.d.ts +57 -0
  57. package/dist/world/worldManager.js +125 -0
  58. package/dist/world/worldNormal.d.ts +30 -0
  59. package/dist/world/worldNormal.js +58 -0
  60. package/dist/world/worldTexture.d.ts +56 -0
  61. package/dist/world/worldTexture.js +299 -0
  62. package/package.json +29 -39
  63. package/src/clock/Transport.ts +37 -6
  64. package/src/clock/musical.ts +18 -8
  65. package/src/clock/sync.ts +15 -2
  66. package/src/config/env.ts +48 -0
  67. package/src/config/{index.js → index.ts} +7 -0
  68. package/src/config/{textureConfig.js → textureConfig.ts} +126 -60
  69. package/src/core/{lightFresnel.js → lightFresnel.ts} +1 -1
  70. package/src/core/{lights.js → lights.ts} +21 -21
  71. package/src/loaders/{bodyTextureLoader.js → bodyTextureLoader.ts} +10 -7
  72. package/src/loaders/{cubeMapLoader.js → cubeMapLoader.ts} +83 -49
  73. package/src/loaders/glbLoader.ts +110 -0
  74. package/src/loaders/index.ts +6 -0
  75. package/src/loaders/{ktx2.js → ktx2.ts} +5 -7
  76. package/src/moons/{index.js → index.ts} +1 -0
  77. package/src/moons/{moonsGeneration.js → moonsGeneration.ts} +45 -32
  78. package/src/moons/{moonsManager.js → moonsManager.ts} +37 -11
  79. package/src/render/{frameBudget.js → frameBudget.ts} +15 -8
  80. package/src/render/index.ts +14 -0
  81. package/src/render/{viewportCompositor.js → viewportCompositor.ts} +121 -59
  82. package/src/studio/headerBroadcast.ts +14 -1
  83. package/src/studio/hostTransport.ts +2 -1
  84. package/src/studio/index.ts +1 -1
  85. package/src/utils/dispose.ts +47 -0
  86. package/src/utils/index.ts +4 -0
  87. package/src/utils/materials.ts +44 -0
  88. package/src/world/{cloudShell.js → cloudShell.ts} +19 -12
  89. package/src/world/index.ts +7 -0
  90. package/src/world/worldGLB.ts +41 -0
  91. package/src/world/{worldManager.js → worldManager.ts} +67 -32
  92. package/src/world/{worldNormal.js → worldNormal.ts} +25 -20
  93. package/src/world/{worldTexture.js → worldTexture.ts} +70 -51
  94. package/dist/types/config/env.d.ts +0 -12
  95. package/dist/types/config/index.d.ts +0 -2
  96. package/dist/types/config/textureConfig.d.ts +0 -121
  97. package/dist/types/core/index.d.ts +0 -2
  98. package/dist/types/core/lightFresnel.d.ts +0 -5
  99. package/dist/types/core/lights.d.ts +0 -58
  100. package/dist/types/index.d.ts +0 -6
  101. package/dist/types/loaders/bodyTextureLoader.d.ts +0 -12
  102. package/dist/types/loaders/cubeMapLoader.d.ts +0 -70
  103. package/dist/types/loaders/glbLoader.d.ts +0 -24
  104. package/dist/types/loaders/index.d.ts +0 -5
  105. package/dist/types/moons/index.d.ts +0 -2
  106. package/dist/types/moons/moonsGeneration.d.ts +0 -72
  107. package/dist/types/moons/moonsManager.d.ts +0 -27
  108. package/dist/types/render/index.d.ts +0 -2
  109. package/dist/types/render/viewportCompositor.d.ts +0 -58
  110. package/dist/types/utils/dispose.d.ts +0 -2
  111. package/dist/types/utils/index.d.ts +0 -2
  112. package/dist/types/utils/materials.d.ts +0 -2
  113. package/dist/types/world/cloudShell.d.ts +0 -25
  114. package/dist/types/world/index.d.ts +0 -6
  115. package/dist/types/world/worldGLB.d.ts +0 -20
  116. package/dist/types/world/worldManager.d.ts +0 -63
  117. package/dist/types/world/worldNormal.d.ts +0 -43
  118. package/dist/types/world/worldTexture.d.ts +0 -80
  119. package/src/config/env.js +0 -38
  120. package/src/loaders/glbLoader.js +0 -78
  121. package/src/utils/dispose.js +0 -39
  122. package/src/utils/materials.js +0 -31
  123. package/src/world/worldGLB.js +0 -19
  124. /package/{src → dist}/render/index.js +0 -0
  125. /package/src/core/{index.js → index.ts} +0 -0
  126. /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 and three-stdlib as peer dependencies:
149
+ This library requires Three.js as a peer dependency:
149
150
 
150
151
  ```bash
151
- npm install three three-stdlib
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
- applyAnchoredState(s: AnchoredState): void;
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
- scheduleStateChange(atAudioSec: number, state: AnchoredState, meta?: ScheduleStateMeta): void;
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;
@@ -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 <= 0)
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
- if (p.onApply)
196
- p.onApply(); // control rides WITH the state, applied atomically here (fix #5)
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 <= 0)
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;
@@ -14,20 +14,25 @@ export class MusicalProjection {
14
14
  _sig;
15
15
  constructor(spec = {}) {
16
16
  this.ppq = spec.ppq ?? DEFAULT_PPQ;
17
- const tempo = (spec.tempoMap && spec.tempoMap.length ? spec.tempoMap : [{ startSec: 0, bpm: 120 }])
18
- .slice()
19
- .sort((a, b) => a.startSec - b.startSec);
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
- const sig = (spec.signatureMap && spec.signatureMap.length
27
- ? spec.signatureMap
28
- : [{ startBar: 0, numerator: 4, denominator: 4 }])
29
- .slice()
30
- .sort((a, b) => a.startBar - b.startBar);
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++) {
@@ -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
- this.relay.send(this.id, { kind: 'beat:transport', id: this.id, playing, atSessionMs: this.sessionNowMs() });
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
- if (newerTs(msg.ts, this._ts)) {
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>;