@scenography/engine 0.5.0 → 0.7.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/index.cjs CHANGED
@@ -1,5 +1,48 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let three = require("three");
3
+ //#region src/loading.ts
4
+ const pending = /* @__PURE__ */ new Set();
5
+ const listeners = /* @__PURE__ */ new Set();
6
+ let state = {
7
+ loaded: 0,
8
+ total: 0,
9
+ done: true
10
+ };
11
+ const notify = () => {
12
+ for (const listener of listeners) listener(state);
13
+ };
14
+ /** Begin tracking a new initial asset set; resets the previous one. */
15
+ const startLoading = (urls) => {
16
+ pending.clear();
17
+ for (const url of urls) pending.add(url);
18
+ state = {
19
+ loaded: 0,
20
+ total: pending.size,
21
+ done: pending.size === 0
22
+ };
23
+ notify();
24
+ };
25
+ /** Mark one initial asset as loaded; unknown URLs (lazy assets) are ignored. */
26
+ const trackAsset = (url) => {
27
+ if (!pending.delete(url)) return;
28
+ state = {
29
+ loaded: state.total - pending.size,
30
+ total: state.total,
31
+ done: pending.size === 0
32
+ };
33
+ notify();
34
+ };
35
+ /** Current loading progress. */
36
+ const loadingState = () => state;
37
+ /** Subscribe to progress; the listener runs immediately with the current state. */
38
+ const onLoading = (listener) => {
39
+ listeners.add(listener);
40
+ listener(state);
41
+ return () => {
42
+ listeners.delete(listener);
43
+ };
44
+ };
45
+ //#endregion
3
46
  //#region src/environment/EnvironmentController.ts
4
47
  /** Load an equirectangular HDRI, picking the loader by extension. */
5
48
  const loadHDRI = (src, onLoad) => {
@@ -31,6 +74,7 @@ var EnvironmentController = class {
31
74
  this.clear();
32
75
  if (!env) return;
33
76
  loadHDRI(env.src, (tex) => {
77
+ trackAsset(env.src);
34
78
  if (this.hash !== hash) {
35
79
  tex.dispose();
36
80
  return;
@@ -60,6 +104,189 @@ var EnvironmentController = class {
60
104
  }
61
105
  };
62
106
  //#endregion
107
+ //#region src/builders/ktx2.ts
108
+ let loader = null;
109
+ /**
110
+ * Configure the shared KTX2 loader. Called by the kernel once the renderer
111
+ * exists; `.ktx2` textures and glTF models then decode through it.
112
+ */
113
+ const configureKtx2 = (renderer, path) => {
114
+ loader = import("three/examples/jsm/loaders/KTX2Loader.js").then(({ KTX2Loader }) => {
115
+ const instance = new KTX2Loader();
116
+ instance.setTranscoderPath(path);
117
+ instance.detectSupport(renderer);
118
+ return instance;
119
+ });
120
+ };
121
+ /** The configured loader, or null when the gallery did not set `ktx2Path`. */
122
+ const getKtx2Loader = () => loader;
123
+ //#endregion
124
+ //#region src/builders/textureCache.ts
125
+ /** Two surfaces can share a texture only at the same tiling and color space. */
126
+ const textureKey = (src, repeat, srgb) => `${src}|${repeat[0]}x${repeat[1]}|${srgb ? "srgb" : "linear"}`;
127
+ /** Tiling and color space of a texture; the image arrives later from the loader. */
128
+ const configureTexture = (texture, repeat, srgb) => {
129
+ texture.wrapS = three.RepeatWrapping;
130
+ texture.wrapT = three.RepeatWrapping;
131
+ texture.repeat.set(repeat[0], repeat[1]);
132
+ texture.colorSpace = srgb ? three.SRGBColorSpace : three.NoColorSpace;
133
+ };
134
+ /**
135
+ * Shares textures between materials: the same source at the same tiling and
136
+ * color space becomes one texture, loaded once and uploaded to the GPU once.
137
+ * Reference counted, so releasing a material keeps the texture alive while
138
+ * another one still samples it.
139
+ */
140
+ var TextureCache = class {
141
+ source;
142
+ entries = /* @__PURE__ */ new Map();
143
+ owners = /* @__PURE__ */ new Map();
144
+ constructor(source) {
145
+ this.source = source;
146
+ }
147
+ /** The shared texture for a source, created on first use. */
148
+ acquire(src, repeat, srgb) {
149
+ const key = textureKey(src, repeat, srgb);
150
+ const existing = this.entries.get(key);
151
+ if (existing) {
152
+ existing.refs += 1;
153
+ return existing.texture;
154
+ }
155
+ const texture = this.source(src);
156
+ configureTexture(texture, repeat, srgb);
157
+ const entry = {
158
+ key,
159
+ texture,
160
+ refs: 1
161
+ };
162
+ this.entries.set(key, entry);
163
+ this.owners.set(texture, entry);
164
+ return texture;
165
+ }
166
+ /** Drop one reference; the texture is disposed when the last one goes. */
167
+ release(texture) {
168
+ const entry = this.owners.get(texture);
169
+ if (!entry) {
170
+ texture.dispose();
171
+ return;
172
+ }
173
+ entry.refs -= 1;
174
+ if (entry.refs > 0) return;
175
+ this.entries.delete(entry.key);
176
+ this.owners.delete(texture);
177
+ texture.dispose();
178
+ }
179
+ /** Textures currently shared. */
180
+ get size() {
181
+ return this.entries.size;
182
+ }
183
+ };
184
+ //#endregion
185
+ //#region src/builders/texture.ts
186
+ const textureLoader = new three.TextureLoader();
187
+ const cache = new TextureCache((src) => textureLoader.load(src, () => trackAsset(src)));
188
+ const ktx2Entries = /* @__PURE__ */ new Map();
189
+ const ktx2Owners = /* @__PURE__ */ new Map();
190
+ /**
191
+ * KTX2 textures resolve asynchronously, so the material slot is filled through
192
+ * a waiter list. One entry per source and tiling is shared by every material
193
+ * that samples it, just like the image cache.
194
+ */
195
+ const loadKtx2 = (material, slot, src, repeat, srgb) => {
196
+ const loader = getKtx2Loader();
197
+ if (!loader) {
198
+ console.warn(`KTX2 texture "${src}" needs the gallery's ktx2Path option.`);
199
+ return;
200
+ }
201
+ const key = textureKey(src, repeat, srgb);
202
+ let entry = ktx2Entries.get(key);
203
+ if (!entry) {
204
+ entry = {
205
+ key,
206
+ refs: 0,
207
+ waiters: []
208
+ };
209
+ ktx2Entries.set(key, entry);
210
+ const created = entry;
211
+ loader.then((ktx2) => ktx2.load(src, (texture) => {
212
+ configureTexture(texture, repeat, srgb);
213
+ texture.needsUpdate = true;
214
+ trackAsset(src);
215
+ created.texture = texture;
216
+ ktx2Owners.set(texture, created);
217
+ for (const assign of created.waiters) assign(texture);
218
+ created.waiters = [];
219
+ }, void 0, (error) => console.error(`Failed to load KTX2 texture "${src}".`, error)));
220
+ }
221
+ entry.refs += 1;
222
+ if (entry.texture) {
223
+ material[slot] = entry.texture;
224
+ material.needsUpdate = true;
225
+ } else entry.waiters.push((texture) => {
226
+ material[slot] = texture;
227
+ material.needsUpdate = true;
228
+ });
229
+ };
230
+ /**
231
+ * Load one map into `material[slot]`. Textures are shared: every material that
232
+ * samples the same source at the same tiling reuses one texture object. `.exr`
233
+ * maps go through EXRLoader, imported on demand, and are not shared because
234
+ * they resolve asynchronously; `.ktx2` maps share through their own cache.
235
+ */
236
+ const loadMap = (material, slot, src, repeat, srgb) => {
237
+ if (src.toLowerCase().endsWith(".ktx2")) {
238
+ loadKtx2(material, slot, src, repeat, srgb);
239
+ return;
240
+ }
241
+ if (src.toLowerCase().endsWith(".exr")) {
242
+ import("three/examples/jsm/loaders/EXRLoader.js").then(({ EXRLoader }) => {
243
+ new EXRLoader().load(src, (texture) => {
244
+ configureTexture(texture, repeat, srgb);
245
+ texture.needsUpdate = true;
246
+ trackAsset(src);
247
+ material[slot] = texture;
248
+ material.needsUpdate = true;
249
+ });
250
+ });
251
+ return;
252
+ }
253
+ material[slot] = cache.acquire(src, repeat, srgb);
254
+ };
255
+ /**
256
+ * Apply a `SurfaceTexture` to a standard material. Returns whether a second UV
257
+ * set (`uv1`) is needed — three samples `aoMap` from it, so the geometry must
258
+ * provide one.
259
+ */
260
+ const applyTexture = (material, texture) => {
261
+ const repeat = texture.repeat ?? [1, 1];
262
+ loadMap(material, "map", texture.src, repeat, true);
263
+ if (texture.normal) {
264
+ loadMap(material, "normalMap", texture.normal, repeat, false);
265
+ const scale = texture.normalScale ?? 1;
266
+ material.normalScale = new three.Vector2(scale, scale);
267
+ }
268
+ if (texture.roughness) loadMap(material, "roughnessMap", texture.roughness, repeat, false);
269
+ if (texture.ao) loadMap(material, "aoMap", texture.ao, repeat, false);
270
+ material.needsUpdate = true;
271
+ return texture.ao !== void 0;
272
+ };
273
+ /**
274
+ * Release one reference to a texture assigned by `applyTexture`. Shared
275
+ * textures stay alive while another material still samples them.
276
+ */
277
+ const releaseTexture = (texture) => {
278
+ const ktx2 = ktx2Owners.get(texture);
279
+ if (!ktx2) {
280
+ cache.release(texture);
281
+ return;
282
+ }
283
+ ktx2.refs -= 1;
284
+ if (ktx2.refs > 0) return;
285
+ ktx2Owners.delete(texture);
286
+ ktx2Entries.delete(ktx2.key);
287
+ texture.dispose();
288
+ };
289
+ //#endregion
63
290
  //#region src/interaction/InteractionController.ts
64
291
  const HIGHLIGHT_COLOR = "#ef4444";
65
292
  const HIGHLIGHT_THICKNESS = 1.5;
@@ -503,116 +730,6 @@ const EDGES = "edges";
503
730
  */
504
731
  const EYE_HEIGHT = "eyeHeight";
505
732
  //#endregion
506
- //#region src/builders/textureCache.ts
507
- /** Two surfaces can share a texture only at the same tiling and color space. */
508
- const textureKey = (src, repeat, srgb) => `${src}|${repeat[0]}x${repeat[1]}|${srgb ? "srgb" : "linear"}`;
509
- /** Tiling and color space of a texture; the image arrives later from the loader. */
510
- const configureTexture = (texture, repeat, srgb) => {
511
- texture.wrapS = three.RepeatWrapping;
512
- texture.wrapT = three.RepeatWrapping;
513
- texture.repeat.set(repeat[0], repeat[1]);
514
- texture.colorSpace = srgb ? three.SRGBColorSpace : three.NoColorSpace;
515
- };
516
- /**
517
- * Shares textures between materials: the same source at the same tiling and
518
- * color space becomes one texture, loaded once and uploaded to the GPU once.
519
- * Reference counted, so releasing a material keeps the texture alive while
520
- * another one still samples it.
521
- */
522
- var TextureCache = class {
523
- source;
524
- entries = /* @__PURE__ */ new Map();
525
- owners = /* @__PURE__ */ new Map();
526
- constructor(source) {
527
- this.source = source;
528
- }
529
- /** The shared texture for a source, created on first use. */
530
- acquire(src, repeat, srgb) {
531
- const key = textureKey(src, repeat, srgb);
532
- const existing = this.entries.get(key);
533
- if (existing) {
534
- existing.refs += 1;
535
- return existing.texture;
536
- }
537
- const texture = this.source(src);
538
- configureTexture(texture, repeat, srgb);
539
- const entry = {
540
- key,
541
- texture,
542
- refs: 1
543
- };
544
- this.entries.set(key, entry);
545
- this.owners.set(texture, entry);
546
- return texture;
547
- }
548
- /** Drop one reference; the texture is disposed when the last one goes. */
549
- release(texture) {
550
- const entry = this.owners.get(texture);
551
- if (!entry) {
552
- texture.dispose();
553
- return;
554
- }
555
- entry.refs -= 1;
556
- if (entry.refs > 0) return;
557
- this.entries.delete(entry.key);
558
- this.owners.delete(texture);
559
- texture.dispose();
560
- }
561
- /** Textures currently shared. */
562
- get size() {
563
- return this.entries.size;
564
- }
565
- };
566
- //#endregion
567
- //#region src/builders/texture.ts
568
- const textureLoader = new three.TextureLoader();
569
- const cache = new TextureCache((src) => textureLoader.load(src));
570
- /**
571
- * Load one map into `material[slot]`. Textures are shared: every material that
572
- * samples the same source at the same tiling reuses one texture object. `.exr`
573
- * maps go through EXRLoader, imported on demand, and are not shared because
574
- * they resolve asynchronously.
575
- */
576
- const loadMap = (material, slot, src, repeat, srgb) => {
577
- if (src.toLowerCase().endsWith(".exr")) {
578
- import("three/examples/jsm/loaders/EXRLoader.js").then(({ EXRLoader }) => {
579
- new EXRLoader().load(src, (texture) => {
580
- configureTexture(texture, repeat, srgb);
581
- texture.needsUpdate = true;
582
- material[slot] = texture;
583
- material.needsUpdate = true;
584
- });
585
- });
586
- return;
587
- }
588
- material[slot] = cache.acquire(src, repeat, srgb);
589
- };
590
- /**
591
- * Apply a `SurfaceTexture` to a standard material. Returns whether a second UV
592
- * set (`uv1`) is needed — three samples `aoMap` from it, so the geometry must
593
- * provide one.
594
- */
595
- const applyTexture = (material, texture) => {
596
- const repeat = texture.repeat ?? [1, 1];
597
- loadMap(material, "map", texture.src, repeat, true);
598
- if (texture.normal) {
599
- loadMap(material, "normalMap", texture.normal, repeat, false);
600
- const scale = texture.normalScale ?? 1;
601
- material.normalScale = new three.Vector2(scale, scale);
602
- }
603
- if (texture.roughness) loadMap(material, "roughnessMap", texture.roughness, repeat, false);
604
- if (texture.ao) loadMap(material, "aoMap", texture.ao, repeat, false);
605
- material.needsUpdate = true;
606
- return texture.ao !== void 0;
607
- };
608
- /**
609
- * Release one reference to a texture assigned by `applyTexture`. Shared
610
- * textures stay alive while another material still samples them.
611
- */
612
- const releaseTexture = (texture) => {
613
- cache.release(texture);
614
- };
615
- //#endregion
616
733
  //#region src/SceneManager.ts
617
734
  const TEXTURE_SLOTS = [
618
735
  "map",
@@ -656,12 +773,24 @@ var SceneManager = class {
656
773
  root;
657
774
  plugins;
658
775
  capabilities;
776
+ view;
659
777
  /** Mounted plugin elements, per kind, keyed by element id ("" for singletons). */
660
778
  pluginElements = /* @__PURE__ */ new Map();
661
- constructor(root, plugins = [], capabilities = new CapabilityRegistry()) {
779
+ constructor(root, plugins = [], capabilities = new CapabilityRegistry(), view = {
780
+ camera: new three.PerspectiveCamera(),
781
+ renderer: {}
782
+ }) {
662
783
  this.root = root;
663
784
  this.plugins = plugins;
664
785
  this.capabilities = capabilities;
786
+ this.view = view;
787
+ }
788
+ /** The build context handed to every plugin. */
789
+ context() {
790
+ return {
791
+ capabilities: this.capabilities,
792
+ ...this.view
793
+ };
665
794
  }
666
795
  reconcile(scene) {
667
796
  const eye = scene.camera.start.position[1];
@@ -694,7 +823,7 @@ var SceneManager = class {
694
823
  const existing = mounted.get(key);
695
824
  if (existing?.hash === hash) continue;
696
825
  if (existing) this.removePluginElement(mounted, key, existing);
697
- const built = plugin.build(value, { capabilities: this.capabilities });
826
+ const built = plugin.build(value, this.context());
698
827
  if (built.object) this.root.add(built.object);
699
828
  mounted.set(key, {
700
829
  ...built,
@@ -702,7 +831,7 @@ var SceneManager = class {
702
831
  });
703
832
  }
704
833
  for (const [key, entry] of mounted) if (!seen.has(key)) this.removePluginElement(mounted, key, entry);
705
- plugin.provide?.(items.map(([, value]) => value), { capabilities: this.capabilities });
834
+ plugin.provide?.(items.map(([, value]) => value), this.context());
706
835
  }
707
836
  }
708
837
  removePluginElement(mounted, key, entry) {
@@ -793,7 +922,20 @@ function createGallery(scene, options) {
793
922
  const systemPlugins = allPlugins.filter(isSystemPlugin);
794
923
  const elementPlugins = allPlugins.filter((p) => !isSystemPlugin(p));
795
924
  const capabilities = new CapabilityRegistry();
796
- const manager = new SceneManager(threeScene, elementPlugins, capabilities);
925
+ if (options.ktx2Path) configureKtx2(renderer, options.ktx2Path);
926
+ const assets = /* @__PURE__ */ new Set();
927
+ for (const plugin of elementPlugins) {
928
+ const { kind, cardinality } = plugin.data;
929
+ const raw = scene[kind];
930
+ const values = cardinality === "many" ? Array.isArray(raw) ? raw : [] : raw !== void 0 ? [raw] : [];
931
+ for (const value of values) for (const url of plugin.data.assets?.(value) ?? []) assets.add(url);
932
+ }
933
+ if (scene.environment?.src) assets.add(scene.environment.src);
934
+ startLoading(assets);
935
+ const manager = new SceneManager(threeScene, elementPlugins, capabilities, {
936
+ camera,
937
+ renderer
938
+ });
797
939
  manager.reconcile(scene);
798
940
  const input = new InputBus();
799
941
  const controlSettings = new ControlSettings(loadControlFlags() ?? (options.cardboard ? MODE_PRESETS.cardboard : {}));
@@ -1012,6 +1154,10 @@ function createGallery(scene, options) {
1012
1154
  renderer
1013
1155
  }
1014
1156
  },
1157
+ loading: {
1158
+ get: loadingState,
1159
+ subscribe: onLoading
1160
+ },
1015
1161
  setScene(next) {
1016
1162
  manager.reconcile(next);
1017
1163
  interaction.setRadii(radiiOf(next));
@@ -1108,7 +1254,9 @@ exports.TERRAIN = TERRAIN;
1108
1254
  exports.applyMode = applyMode;
1109
1255
  exports.applyTexture = applyTexture;
1110
1256
  exports.createGallery = createGallery;
1257
+ exports.getKtx2Loader = getKtx2Loader;
1111
1258
  exports.isSystemPlugin = isSystemPlugin;
1112
1259
  exports.modeOf = modeOf;
1260
+ exports.trackAsset = trackAsset;
1113
1261
 
1114
1262
  //# sourceMappingURL=index.cjs.map