@preference-sl/pref-viewer 2.10.0-beta.3 → 2.10.0-beta.30

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/src/index.js CHANGED
@@ -22,7 +22,7 @@
22
22
  * style="width:800px; height:600px;">
23
23
  * </pref-viewer>
24
24
  * ```
25
- *
25
+ *
26
26
  * Load scene a URL:
27
27
  * ```html
28
28
  * <pref-viewer
@@ -39,28 +39,110 @@
39
39
  * </pref-viewer>
40
40
  * ```
41
41
  */
42
- import { Engine, Scene, ArcRotateCamera, Vector3, Color4, HemisphericLight, DirectionalLight, PointLight, ShadowGenerator, LoadAssetContainerAsync, Tools, WebXRSessionManager, WebXRDefaultExperience, MeshBuilder, WebXRFeatureName } from "@babylonjs/core";
42
+ import { Engine, Scene, ArcRotateCamera, Vector3, Color4, HemisphericLight, DirectionalLight, PointLight, ShadowGenerator, LoadAssetContainerAsync, Tools, WebXRSessionManager, WebXRDefaultExperience, MeshBuilder, WebXRFeatureName, HDRCubeTexture, IblShadowsRenderPipeline } from "@babylonjs/core";
43
43
  import "@babylonjs/loaders";
44
44
  import { USDZExportAsync, GLTF2Export } from "@babylonjs/serializers";
45
45
  import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression";
46
46
  import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompression";
47
47
  import { initDb, loadModel } from "./gltf-storage.js";
48
48
 
49
- class PrefViewer extends HTMLElement {
50
- #initialized = false;
49
+ class PrefViewerTask {
50
+ static Types = Object.freeze({
51
+ Config: "config",
52
+ Environment: "environment",
53
+ Materials: "materials",
54
+ Model: "model",
55
+ Options: "options",
56
+ });
51
57
 
52
- #model = {
53
- container: null,
54
- show: true, // Show model by default
55
- storage: null,
56
- visible: false,
57
- };
58
+ /**
59
+ * value: any payload for the task
60
+ * type: must match one of PrefViewerTask.Types values (case-insensitive)
61
+ */
62
+ constructor(value, type) {
63
+ this.value = value;
64
+
65
+ const t = typeof type === "string" ? type.toLowerCase() : String(type).toLowerCase();
66
+ const allowed = Object.values(PrefViewerTask.Types);
67
+ if (!allowed.includes(t)) {
68
+ throw new TypeError(
69
+ `PrefViewerTask: invalid type "${type}". Allowed types: ${allowed.join(", ")}`
70
+ );
71
+ }
72
+ this.type = t;
58
73
 
59
- #environment = {
60
- container: null,
61
- show: true, // Show environment by default
62
- storage: null,
63
- visible: false,
74
+ Object.freeze(this);
75
+ }
76
+ }
77
+
78
+ class PrefViewer extends HTMLElement {
79
+ initialized = false;
80
+ loaded = false;
81
+ loading = false;
82
+ #taskQueue = [];
83
+
84
+ #data = {
85
+ containers: {
86
+ model: {
87
+ name: "model",
88
+ assetContainer: null,
89
+ show: true,
90
+ storage: null,
91
+ visible: false,
92
+ size: null,
93
+ timeStamp: null,
94
+ changed: { pending: false, success: false },
95
+ },
96
+ environment: {
97
+ name: "environment",
98
+ assetContainer: null,
99
+ show: true,
100
+ storage: null,
101
+ visible: false,
102
+ size: null,
103
+ timeStamp: null,
104
+ changed: { pending: false, success: false },
105
+ },
106
+ materials: {
107
+ name: "materials",
108
+ assetContainer: null,
109
+ storage: null,
110
+ show: true,
111
+ visible: false,
112
+ size: null,
113
+ timeStamp: null,
114
+ changed: { pending: false, success: false },
115
+ },
116
+ },
117
+ options: {
118
+ camera: {
119
+ value: null,
120
+ locked: true,
121
+ changed: { pending: false, success: false },
122
+ },
123
+ materials: {
124
+ innerWall: {
125
+ value: null,
126
+ prefix: "innerWall",
127
+ changed: { pending: false, success: false },
128
+ },
129
+ outerWall: {
130
+ value: null,
131
+ prefix: "outerWall",
132
+ changed: { pending: false, success: false },
133
+ },
134
+ innerFloor: {
135
+ value: null,
136
+ prefix: "innerFloor",
137
+ changed: { pending: false, success: false },
138
+ },
139
+ outerFloor: {
140
+ value: null,
141
+ prefix: "outerFloor",
142
+ changed: { pending: false, success: false },
143
+ },
144
+ },
145
+ },
64
146
  };
65
147
 
66
148
  // DOM elements
@@ -110,20 +192,26 @@ class PrefViewer extends HTMLElement {
110
192
  case "scene":
111
193
  this.loadScene(value);
112
194
  break;
195
+ case "materials":
196
+ this.loadMaterials(value);
197
+ break;
198
+ case "options":
199
+ this.setOptions(value);
200
+ break;
113
201
  case "show-model":
114
202
  data = value.toLowerCase?.() === "true";
115
- if (this.#initialized) {
203
+ if (this.initialized) {
116
204
  data ? this.showModel() : this.hideModel();
117
205
  } else {
118
- this.#model.show = data;
206
+ this.#data.containers.model.show = data;
119
207
  }
120
208
  break;
121
209
  case "show-scene":
122
210
  data = value.toLowerCase?.() === "true";
123
- if (this.#initialized) {
211
+ if (this.initialized) {
124
212
  data ? this.showScene() : this.hideScene();
125
213
  } else {
126
- this.#environment.show = data;
214
+ this.#data.containers.environment.show = data;
127
215
  }
128
216
  break;
129
217
  }
@@ -134,18 +222,19 @@ class PrefViewer extends HTMLElement {
134
222
  const error = 'PrefViewer: provide "models" as array of model and environment';
135
223
  console.error(error);
136
224
  this.dispatchEvent(
137
- new CustomEvent("model-error", {
138
- detail: { error: new Error(error) },
225
+ new CustomEvent("scene-error", {
139
226
  bubbles: true,
227
+ cancelable: false,
140
228
  composed: true,
229
+ detail: { error: new Error(error) },
141
230
  })
142
231
  );
143
232
  return false;
144
233
  }
145
234
 
146
235
  this.#initializeBabylon();
147
- this.#loadContainers(true, true);
148
- this.#initialized = true;
236
+ this.initialized = true;
237
+ this.#processNextTask();
149
238
  }
150
239
 
151
240
  disconnectedCallback() {
@@ -174,24 +263,155 @@ class PrefViewer extends HTMLElement {
174
263
  this.#wrapper.appendChild(this.#canvas);
175
264
  this.shadowRoot.append(this.#wrapper);
176
265
  }
177
-
178
- // Bbylon.js
266
+
267
+ #setStatusLoading() {
268
+ this.loaded = false;
269
+ this.loading = true;
270
+ if (this.hasAttribute("loaded")) {
271
+ this.removeAttribute("loaded");
272
+ }
273
+ this.setAttribute("loading", "");
274
+ this.dispatchEvent(
275
+ new CustomEvent("scene-loading", {
276
+ bubbles: true,
277
+ cancelable: false,
278
+ composed: true,
279
+ })
280
+ );
281
+ this.#engine.stopRenderLoop(this.#renderLoop);
282
+ }
283
+
284
+ async #setStatusLoaded() {
285
+ const toLoadDetail = {
286
+ container_model: !!this.#data.containers.model.changed.pending,
287
+ container_environment: !!this.#data.containers.environment.changed.pending,
288
+ container_materials: !!this.#data.containers.materials.changed.pending,
289
+ options_camera: !!this.#data.options.camera.changed.pending,
290
+ options_innerWallMaterial: !!this.#data.options.materials.innerWall.changed.pending,
291
+ options_outerWallMaterial: !!this.#data.options.materials.outerWall.changed.pending,
292
+ options_innerFloorMaterial: !!this.#data.options.materials.innerFloor.changed.pending,
293
+ options_outerFloorMaterial: !!this.#data.options.materials.outerFloor.changed.pending,
294
+ };
295
+ const loadedDetail = {
296
+ container_model: !!this.#data.containers.model.changed.success,
297
+ container_environment: !!this.#data.containers.environment.changed.success,
298
+ container_materials: !!this.#data.containers.materials.changed.success,
299
+ options_camera: !!this.#data.options.camera.changed.success,
300
+ options_innerWallMaterial: !!this.#data.options.materials.innerWall.changed.success,
301
+ options_outerWallMaterial: !!this.#data.options.materials.outerWall.changed.success,
302
+ options_innerFloorMaterial: !!this.#data.options.materials.innerFloor.changed.success,
303
+ options_outerFloorMaterial: !!this.#data.options.materials.outerFloor.changed.success,
304
+ };
305
+ const detail = {
306
+ tried: toLoadDetail,
307
+ success: loadedDetail,
308
+ };
309
+
310
+ this.dispatchEvent(
311
+ new CustomEvent("scene-loaded", {
312
+ bubbles: true,
313
+ cancelable: false,
314
+ composed: true,
315
+ detail: detail,
316
+ })
317
+ );
318
+
319
+ await this.#scene.whenReadyAsync();
320
+ this.#engine.runRenderLoop(this.#renderLoop);
321
+
322
+ this.#resetChangedFlags();
323
+
324
+ if (this.hasAttribute("loading")) {
325
+ this.removeAttribute("loading");
326
+ }
327
+ this.setAttribute("loaded", "");
328
+
329
+ this.loaded = true;
330
+ this.loading = false;
331
+
332
+ this.#processNextTask();
333
+ }
334
+
335
+ // Data
336
+ #checkCameraChanged(options) {
337
+ if (!options || !options.camera) {
338
+ return false;
339
+ }
340
+ const prev = this.#data.options.camera.value;
341
+ const changed = options.camera !== prev;
342
+
343
+ this.#data.options.camera.changed.pending = changed;
344
+ this.#data.options.camera.changed.success = false;
345
+ if (changed) {
346
+ this.#data.options.camera.changed.value = prev;
347
+ this.#data.options.camera.changed.locked = this.#data.options.camera.locked;
348
+ this.#data.options.camera.value = options.camera;
349
+ }
350
+ return changed;
351
+ }
352
+
353
+ #checkMaterialsChanged(options) {
354
+ if (!options) {
355
+ return false;
356
+ }
357
+ let someChanged = false;
358
+ Object.keys(this.#data.options.materials).forEach((material) => {
359
+ const key = `${material}Material`;
360
+ const state = this.#data.options.materials[material];
361
+ const prev = state.value;
362
+ const incoming = options[key];
363
+ const changed = !!incoming && incoming !== prev;
364
+
365
+ state.changed.pending = changed;
366
+ state.changed.success = false;
367
+ if (changed) {
368
+ state.changed.value = prev;
369
+ state.value = incoming;
370
+ }
371
+ someChanged = someChanged || changed;
372
+ });
373
+ return someChanged;
374
+ }
375
+
376
+ #storeChangedFlagsForContainer(container, success) {
377
+ if (success) {
378
+ container.timeStamp = container.changed.timeStamp;
379
+ container.size = container.changed.size;
380
+ container.changed.success = true;
381
+ } else {
382
+ container.changed.success = false;
383
+ }
384
+ }
385
+
386
+ #resetChangedFlags() {
387
+ const reset = (node) => {
388
+ node.changed = { pending: false, success: false };
389
+ };
390
+ Object.values(this.#data.containers).forEach(reset);
391
+ Object.values(this.#data.options.materials).forEach(reset);
392
+ reset(this.#data.options.camera);
393
+ }
394
+
395
+ // Babylon.js
179
396
  async #initializeBabylon() {
180
397
  this.#engine = new Engine(this.#canvas, true, { alpha: true });
398
+ this.#engine.disableUniformBuffers = true;
181
399
  this.#scene = new Scene(this.#engine);
182
400
  this.#scene.clearColor = new Color4(1, 1, 1, 1);
183
401
  this.#createCamera();
184
402
  this.#createLights();
185
403
  this.#setupInteraction();
186
-
187
- this.#engine.runRenderLoop(() => this.#scene && this.#scene.render());
188
- this.#canvasResizeObserver.observe(this.#canvas);
189
-
190
404
  await this.#createXRExperience();
191
-
405
+ this.#engine.runRenderLoop(this.#renderLoop);
406
+ this.#canvasResizeObserver.observe(this.#canvas);
192
407
  }
193
408
 
194
- addStylesToARButton(){
409
+ // If this function is defined as '#renderLoop() {}' it is not executed in 'this.#engine.runRenderLoop(this.#renderLoop)'
410
+ #renderLoop = () => {
411
+ this.#scene && this.#scene.render();
412
+ };
413
+
414
+ #addStylesToARButton() {
195
415
  const css = '.babylonVRicon { color: #868686; border-color: #868686; border-style: solid; margin-left: 10px; height: 50px; width: 80px; background-color: rgba(51,51,51,0.7); background-image: url(data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%222048%22%20height%3D%221152%22%20viewBox%3D%220%200%202048%201152%22%20version%3D%221.1%22%3E%3Cpath%20transform%3D%22rotate%28180%201024%2C576.0000000000001%29%22%20d%3D%22m1109%2C896q17%2C0%2030%2C-12t13%2C-30t-12.5%2C-30.5t-30.5%2C-12.5l-170%2C0q-18%2C0%20-30.5%2C12.5t-12.5%2C30.5t13%2C30t30%2C12l170%2C0zm-85%2C256q59%2C0%20132.5%2C-1.5t154.5%2C-5.5t164.5%2C-11.5t163%2C-20t150%2C-30t124.5%2C-41.5q23%2C-11%2042%2C-24t38%2C-30q27%2C-25%2041%2C-61.5t14%2C-72.5l0%2C-257q0%2C-123%20-47%2C-232t-128%2C-190t-190%2C-128t-232%2C-47l-81%2C0q-37%2C0%20-68.5%2C14t-60.5%2C34.5t-55.5%2C45t-53%2C45t-53%2C34.5t-55.5%2C14t-55.5%2C-14t-53%2C-34.5t-53%2C-45t-55.5%2C-45t-60.5%2C-34.5t-68.5%2C-14l-81%2C0q-123%2C0%20-232%2C47t-190%2C128t-128%2C190t-47%2C232l0%2C257q0%2C68%2038%2C115t97%2C73q54%2C24%20124.5%2C41.5t150%2C30t163%2C20t164.5%2C11.5t154.5%2C5.5t132.5%2C1.5zm939%2C-298q0%2C39%20-24.5%2C67t-58.5%2C42q-54%2C23%20-122%2C39.5t-143.5%2C28t-155.5%2C19t-157%2C11t-148.5%2C5t-129.5%2C1.5q-59%2C0%20-130%2C-1.5t-148%2C-5t-157%2C-11t-155.5%2C-19t-143.5%2C-28t-122%2C-39.5q-34%2C-14%20-58.5%2C-42t-24.5%2C-67l0%2C-257q0%2C-106%2040.5%2C-199t110%2C-162.5t162.5%2C-109.5t199%2C-40l81%2C0q27%2C0%2052%2C14t50%2C34.5t51%2C44.5t55.5%2C44.5t63.5%2C34.5t74%2C14t74%2C-14t63.5%2C-34.5t55.5%2C-44.5t51%2C-44.5t50%2C-34.5t52%2C-14l14%2C0q37%2C0%2070%2C0.5t64.5%2C4.5t63.5%2C12t68%2C23q71%2C30%20128.5%2C78.5t98.5%2C110t63.5%2C133.5t22.5%2C149l0%2C257z%22%20fill%3D%22white%22%20/%3E%3C/svg%3E%0A); background-size: 80%; background-repeat:no-repeat; background-position: center; border: none; outline: none; transition: transform 0.125s ease-out } .babylonVRicon:hover { transform: scale(1.05) } .babylonVRicon:active {background-color: rgba(51,51,51,1) } .babylonVRicon:focus {background-color: rgba(51,51,51,1) }.babylonVRicon.vrdisplaypresenting { background-image: none;} .vrdisplaypresenting::after { content: "EXIT"} .xr-error::after { content: "ERROR"}';
196
416
  const style = document.createElement("style");
197
417
  style.appendChild(document.createTextNode(css));
@@ -202,14 +422,14 @@ class PrefViewer extends HTMLElement {
202
422
  if (this.#XRExperience) {
203
423
  return true;
204
424
  }
205
-
425
+
206
426
  const sessionMode = "immersive-ar";
207
427
  const sessionSupported = await WebXRSessionManager.IsSessionSupportedAsync(sessionMode);
208
428
  if (!sessionSupported) {
209
429
  console.info("PrefViewer: WebXR in mode AR is not supported");
210
430
  return false;
211
431
  }
212
-
432
+
213
433
  try {
214
434
  const ground = MeshBuilder.CreateGround("ground", { width: 1000, height: 1000 }, this.#scene);
215
435
  ground.isVisible = false;
@@ -231,7 +451,6 @@ class PrefViewer extends HTMLElement {
231
451
  xrInput: this.#XRExperience.input,
232
452
  floorMeshes: [ground],
233
453
  timeToTeleport: 1500,
234
- useMainComponentOnly: true,
235
454
  });
236
455
 
237
456
  this.#XRExperience.baseExperience.sessionManager.onXRReady.add(() => {
@@ -241,7 +460,7 @@ class PrefViewer extends HTMLElement {
241
460
  this.#XRExperience.baseExperience.onInitialXRPoseSetObservable.notifyObservers(this.#XRExperience.baseExperience.camera);
242
461
  });
243
462
 
244
- this.addStylesToARButton();
463
+ this.#addStylesToARButton();
245
464
  } catch (error) {
246
465
  console.warn("PrefViewer: failed to create WebXR experience", error);
247
466
  this.#XRExperience = null;
@@ -251,15 +470,23 @@ class PrefViewer extends HTMLElement {
251
470
  #canvasResizeObserver = new ResizeObserver(() => this.#engine && this.#engine.resize());
252
471
 
253
472
  #createCamera() {
254
- this.#camera = new ArcRotateCamera("camera", 3 * Math.PI / 2, Math.PI * 0.47, 10, Vector3.Zero(), this.#scene);
473
+ this.#camera = new ArcRotateCamera("camera", (3 * Math.PI) / 2, Math.PI * 0.47, 10, Vector3.Zero(), this.#scene);
255
474
  this.#camera.upperBetaLimit = Math.PI * 0.48;
256
475
  this.#camera.lowerBetaLimit = Math.PI * 0.25;
257
476
  this.#camera.lowerRadiusLimit = 5;
258
477
  this.#camera.upperRadiusLimit = 20;
478
+ this.#camera.metadata = { locked: false };
259
479
  this.#camera.attachControl(this.#canvas, true);
480
+ this.#scene.activeCamera = this.#camera;
260
481
  }
261
482
 
262
483
  #createLights() {
484
+ this.#initEnvironmentTexture();
485
+
486
+ if (this.#scene.environmentTexture) {
487
+ return true;
488
+ }
489
+
263
490
  // 1) Stronger ambient fill
264
491
  this.#hemiLight = new HemisphericLight("hemiLight", new Vector3(-10, 10, -10), this.#scene);
265
492
  this.#hemiLight.intensity = 0.6;
@@ -269,7 +496,7 @@ class PrefViewer extends HTMLElement {
269
496
  this.#dirLight.position = new Vector3(5, 4, 5); // light is IN FRONT + ABOVE + to the RIGHT
270
497
  this.#dirLight.intensity = 0.6;
271
498
 
272
- // 3) Soft shadows
499
+ // // 3) Soft shadows
273
500
  this.#shadowGen = new ShadowGenerator(1024, this.#dirLight);
274
501
  this.#shadowGen.useBlurExponentialShadowMap = true;
275
502
  this.#shadowGen.blurKernel = 16;
@@ -281,12 +508,104 @@ class PrefViewer extends HTMLElement {
281
508
  this.#cameraLight.intensity = 0.3;
282
509
  }
283
510
 
511
+ #initEnvironmentTexture() {
512
+ return false;
513
+ if (this.#scene.environmentTexture) {
514
+ return true;
515
+ }
516
+ const hdrTextureURI = "../src/environments/noon_grass.hdr";
517
+ const hdrTexture = new HDRCubeTexture(hdrTextureURI, this.#scene, 128);
518
+ hdrTexture.gammaSpace = true;
519
+ hdrTexture._noMipmap = false;
520
+ hdrTexture.level = 2.0;
521
+ this.#scene.environmentTexture = hdrTexture;
522
+ }
523
+
524
+ #initIBLShadows() {
525
+ if (!this.#scene.environmentTexture) {
526
+ return false;
527
+ }
528
+
529
+ let createIBLShadowPipeline = function (scene) {
530
+ const pipeline = new IblShadowsRenderPipeline(
531
+ "iblShadowsPipeline",
532
+ scene,
533
+ {
534
+ resolutionExp: 7,
535
+ sampleDirections: 2,
536
+ ssShadowsEnabled: true,
537
+ shadowRemanence: 0.8,
538
+ triPlanarVoxelization: true,
539
+ shadowOpacity: 0.8,
540
+ },
541
+ [scene.activeCamera]
542
+ );
543
+ pipeline.allowDebugPasses = false;
544
+ pipeline.gbufferDebugEnabled = true;
545
+ pipeline.importanceSamplingDebugEnabled = false;
546
+ pipeline.voxelDebugEnabled = false;
547
+ pipeline.voxelDebugDisplayMip = 1;
548
+ pipeline.voxelDebugAxis = 2;
549
+ pipeline.voxelTracingDebugEnabled = false;
550
+ pipeline.spatialBlurPassDebugEnabled = false;
551
+ pipeline.accumulationPassDebugEnabled = false;
552
+ return pipeline;
553
+ };
554
+
555
+ let iblShadowsPipeline = createIBLShadowPipeline(this.#scene);
556
+
557
+ this.#scene.meshes.forEach((mesh) => {
558
+ if (mesh.id.startsWith("__root__") || mesh.name === "hdri") {
559
+ return false;
560
+ }
561
+ iblShadowsPipeline.addShadowCastingMesh(mesh);
562
+ iblShadowsPipeline.updateSceneBounds();
563
+ });
564
+
565
+ this.#scene.materials.forEach((material) => {
566
+ iblShadowsPipeline.addShadowReceivingMaterial(material);
567
+ });
568
+ }
569
+
570
+ #initShadows() {
571
+ if (!this.#scene.environmentTexture) {
572
+ this.#initIBLShadows();
573
+ return true;
574
+ }
575
+
576
+ this.#scene.meshes.forEach((mesh) => {
577
+ if (mesh.id.startsWith("__root__")) {
578
+ return false;
579
+ }
580
+ mesh.receiveShadows = true;
581
+ if (!mesh.name === "hdri") {
582
+ this.#shadowGen.addShadowCaster(mesh, true);
583
+ }
584
+ });
585
+ }
586
+
587
+ #setMaxSimultaneousLights() {
588
+ let lightsNumber = 1; // Como mínimo una luz correspondiente a la textura de environmentTexture
589
+ this.#scene.lights.forEach((light) => {
590
+ if (light.isEnabled()) {
591
+ ++lightsNumber;
592
+ }
593
+ });
594
+ if (this.#scene.materials) {
595
+ this.#scene.materials.forEach((material) => (material.maxSimultaneousLights = lightsNumber));
596
+ }
597
+ }
598
+
284
599
  #setupInteraction() {
285
600
  this.#canvas.addEventListener("wheel", (event) => {
286
- if (!this.#scene || !this.#camera) return;
287
- const pick = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
601
+ if (!this.#scene || !this.#camera) {
602
+ return false;
603
+ }
604
+ //const pick = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
288
605
  //this.#camera.target = pick.hit ? pick.pickedPoint.clone() : this.#camera.target;
289
- this.#camera.inertialRadiusOffset -= event.deltaY * this.#camera.wheelPrecision * 0.001;
606
+ if (!this.#scene.activeCamera.metadata?.locked) {
607
+ this.#scene.activeCamera.inertialRadiusOffset -= event.deltaY * this.#scene.activeCamera.wheelPrecision * 0.001;
608
+ }
290
609
  event.preventDefault();
291
610
  });
292
611
  }
@@ -300,6 +619,27 @@ class PrefViewer extends HTMLElement {
300
619
  }
301
620
 
302
621
  // Utility methods for loading gltf/glb
622
+ async #getServerFileDataHeader(uri) {
623
+ return new Promise((resolve) => {
624
+ const xhr = new XMLHttpRequest();
625
+ xhr.open("HEAD", uri, true);
626
+ xhr.responseType = "blob";
627
+ xhr.onload = () => {
628
+ if (xhr.status === 200) {
629
+ const size = parseInt(xhr.getResponseHeader("Content-Length"));
630
+ const timeStamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
631
+ resolve([size, timeStamp]);
632
+ } else {
633
+ resolve([0, null]);
634
+ }
635
+ };
636
+ xhr.onerror = () => {
637
+ resolve([0, null]);
638
+ };
639
+ xhr.send();
640
+ });
641
+ }
642
+
303
643
  #transformUrl(url) {
304
644
  return new Promise((resolve) => {
305
645
  resolve(url.replace(/^blob:[^/]+\//i, "").replace(/\\/g, "/"));
@@ -312,21 +652,22 @@ class PrefViewer extends HTMLElement {
312
652
  let decoded = "";
313
653
  let blob = null;
314
654
  let extension = null;
655
+ let size = raw.length;
315
656
  try {
316
657
  decoded = atob(raw);
317
658
  } catch {
318
- return { blob, extension };
659
+ return { blob, extension, size };
319
660
  }
320
661
  let isJson = false;
321
662
  try {
322
663
  JSON.parse(decoded);
323
664
  isJson = true;
324
- } catch {}
665
+ } catch { }
325
666
  extension = isJson ? ".gltf" : ".glb";
326
667
  const type = isJson ? "model/gltf+json" : "model/gltf-binary";
327
668
  const array = Uint8Array.from(decoded, (c) => c.charCodeAt(0));
328
669
  blob = new Blob([array], { type });
329
- return { blob, extension };
670
+ return { blob, extension, size };
330
671
  }
331
672
 
332
673
  async #initStorage(db, table) {
@@ -338,41 +679,133 @@ class PrefViewer extends HTMLElement {
338
679
 
339
680
  // Methods for managing Asset Containers
340
681
  #setVisibilityOfWallAndFloorInModel(show) {
341
- if (this.#model.container && this.#model.visible) {
342
- const names = ["outer_0", "inner_1", "outerFloor", "innerFloor"];
343
- const nodes = this.#model.container.getNodes();
344
- this.#model.container
345
- .getNodes()
346
- .filter((filter) => names.includes(filter.name))
347
- .forEach((node) => node.setEnabled(show !== undefined ? show : this.#environment.show));
682
+ if (!this.#data.containers.model.assetContainer || !this.#data.containers.model.visible) {
683
+ return false;
348
684
  }
685
+ show = show !== undefined ? show : this.#data.containers.environment.visible;
686
+ const prefixes = Object.values(this.#data.options.materials).map((material) => material.prefix);
687
+ this.#data.containers.model.assetContainer.meshes.filter((meshToFilter) => prefixes.some((prefix) => meshToFilter.name.startsWith(prefix))).forEach((mesh) => mesh.setEnabled(show));
349
688
  }
350
689
 
351
- #addContainer(group) {
352
- if (group.container && !group.visible && group.show) {
353
- group.container.addAllToScene();
354
- group.visible = true;
690
+ #setOptionsMaterial(optionMaterial) {
691
+ if (!optionMaterial || !optionMaterial.prefix || !optionMaterial.value) {
692
+ return false;
355
693
  }
356
- }
357
694
 
358
- #removeContainer(group) {
359
- if (group.container && group.visible) {
360
- group.container.removeAllFromScene();
361
- group.visible = false;
695
+ const material = this.#data.containers.materials.assetContainer?.materials.find((mat) => mat.name === optionMaterial.value) || null;
696
+ if (!material) {
697
+ return false;
362
698
  }
699
+
700
+ const containers = [];
701
+ if (this.#data.containers.model.assetContainer && (this.#data.containers.model.changed.pending || this.#data.containers.materials.changed.pending || optionMaterial.changed.pending)) {
702
+ containers.push(this.#data.containers.model.assetContainer);
703
+ }
704
+ if (this.#data.containers.environment.assetContainer && (this.#data.containers.environment.changed.pending || this.#data.containers.materials.changed.pending || optionMaterial.changed.pending)) {
705
+ containers.push(this.#data.containers.environment.assetContainer);
706
+ }
707
+ if (containers.length === 0) {
708
+ return false;
709
+ }
710
+
711
+ let someSetted = false;
712
+ containers.forEach((container) =>
713
+ container.meshes
714
+ .filter((meshToFilter) => meshToFilter.name.startsWith(optionMaterial.prefix))
715
+ .forEach((mesh) => {
716
+ mesh.material = material;
717
+ someSetted = true;
718
+ })
719
+ );
720
+
721
+ if (someSetted) {
722
+ optionMaterial.changed.success = true;
723
+ } else if (optionMaterial.changed.pending) {
724
+ optionMaterial.value = optionMaterial.changed.value;
725
+ optionMaterial.changed.success = false;
726
+ }
727
+
728
+ return someSetted;
363
729
  }
364
730
 
365
- #replaceContainer(group, newContainer) {
366
- this.#removeContainer(group);
367
- group.container = newContainer;
368
- group.container.meshes.forEach((mesh) => {
369
- mesh.receiveShadows = true;
370
- this.#shadowGen.addShadowCaster(mesh, true);
731
+ #setOptionsMaterials() {
732
+ let someSetted = false;
733
+ Object.values(this.#data.options.materials).forEach((material) => {
734
+ let settedMaterial = this.#setOptionsMaterial(material);
735
+ someSetted = someSetted || settedMaterial;
371
736
  });
372
- this.#addContainer(group);
737
+ return someSetted;
373
738
  }
374
739
 
375
- async #loadAssetContainer(storage) {
740
+ #setOptionsCamera() {
741
+ if (!this.#data.options.camera.value && !this.#data.options.camera.changed.pending && !this.#data.containers.model.changed.pending && !this.#data.containers.environment.changed.pending) {
742
+ return false;
743
+ }
744
+
745
+ let camera = this.#data.containers.model.assetContainer?.cameras.find((thisCamera) => thisCamera.name === this.#data.options.camera.value) || this.#data.containers.environment.assetContainer?.cameras.find((thisCamera) => thisCamera.name === this.#data.options.camera.value) || null;
746
+ if (!camera) {
747
+ if (this.#data.options.camera.changed.value && this.#data.options.camera.changed.value !== this.#data.options.camera.value) {
748
+ camera = this.#data.containers.model.assetContainer?.cameras.find((thisCamera) => thisCamera.name === this.#data.options.camera.changed.value) || this.#data.containers.environment.assetContainer?.cameras.find((thisCamera) => thisCamera.name === this.#data.options.camera.changed.value) || null;
749
+ }
750
+ if (camera) {
751
+ camera.metadata = { locked: this.#data.options.camera.changed.locked };
752
+ this.#data.options.camera.value = this.#data.options.camera.changed.value;
753
+ this.#data.options.camera.locked = this.#data.options.camera.changed.locked;
754
+ this.#data.options.camera.changed.success = false;
755
+ } else {
756
+ camera = this.#camera;
757
+ this.#data.options.camera.value = null;
758
+ this.#data.options.camera.locked = this.#camera.metadata.locked;
759
+ this.#data.options.camera.changed.success = false;
760
+ }
761
+ } else {
762
+ camera.metadata = { locked: this.#data.options.camera.locked };
763
+ if (this.#data.options.camera.changed.pending) {
764
+ this.#data.options.camera.changed.success = true;
765
+ }
766
+ }
767
+ if (!this.#data.options.camera.locked && this.#data.options.camera.value !== null) {
768
+ camera.attachControl(this.#canvas, true);
769
+ }
770
+ this.#scene.activeCamera = camera;
771
+ return true;
772
+ }
773
+
774
+ #addContainer(container) {
775
+ if (!container.assetContainer || container.visible || !container.show) {
776
+ return false;
777
+ }
778
+
779
+ container.assetContainer.addAllToScene();
780
+ container.visible = true;
781
+ return true;
782
+ }
783
+
784
+ #removeContainer(container) {
785
+ if (!container.assetContainer || !container.visible) {
786
+ return false;
787
+ }
788
+
789
+ container.assetContainer.removeAllFromScene();
790
+ container.visible = false;
791
+ return true;
792
+ }
793
+
794
+ #replaceContainer(container, newAssetContainer) {
795
+ if (container.assetContainer) {
796
+ this.#removeContainer(container);
797
+ container.assetContainer.dispose();
798
+ container.assetContainer = null;
799
+ }
800
+ this.#scene.getEngine().releaseEffects();
801
+ container.assetContainer = newAssetContainer;
802
+ this.#addContainer(container);
803
+ return true;
804
+ }
805
+
806
+ async #loadAssetContainer(container) {
807
+ let storage = container?.storage;
808
+
376
809
  if (!storage) {
377
810
  return false;
378
811
  }
@@ -383,6 +816,12 @@ class PrefViewer extends HTMLElement {
383
816
  await this.#initStorage(storage.db, storage.table);
384
817
  const object = await loadModel(storage.id, storage.table);
385
818
  source = object.data;
819
+ if (object.timeStamp === container.timeStamp) {
820
+ container.changed = { pending: false, success: false };
821
+ return false;
822
+ } else {
823
+ container.changed = { pending: true, size: object.size, success: false, timeStamp: object.timeStamp };
824
+ }
386
825
  }
387
826
 
388
827
  if (!source) {
@@ -391,20 +830,39 @@ class PrefViewer extends HTMLElement {
391
830
 
392
831
  let file = null;
393
832
 
394
- let { blob, extension } = this.#decodeBase64(source);
833
+ let { blob, extension, size } = this.#decodeBase64(source);
395
834
  if (blob && extension) {
396
- file = new File([blob], `model${extension}`, {
835
+ file = new File([blob], `${container.name}${extension}`, {
397
836
  type: blob.type,
398
837
  });
838
+ if (!container.changed.pending) {
839
+ if (container.timeStamp === null && container.size === size) {
840
+ container.changed = { pending: false, success: false };
841
+ return false;
842
+ } else {
843
+ container.changed = { pending: true, size: size, success: false, timeStamp: null };
844
+ }
845
+ }
399
846
  } else {
400
847
  const extMatch = source.match(/\.(gltf|glb)(\?|#|$)/i);
401
848
  extension = extMatch ? `.${extMatch[1].toLowerCase()}` : ".gltf";
849
+ const [fileSize, fileTimeStamp] = await this.#getServerFileDataHeader(source);
850
+ if (container.size === fileSize && container.timeStamp === fileTimeStamp) {
851
+ container.changed = { pending: false, success: false };
852
+ return false;
853
+ } else {
854
+ container.changed = { pending: true, size: fileSize, success: false, timeStamp: fileTimeStamp };
855
+ }
402
856
  }
403
857
 
858
+ // https://doc.babylonjs.com/typedoc/interfaces/BABYLON.LoadAssetContainerOptions
404
859
  let options = {
405
860
  pluginExtension: extension,
406
861
  pluginOptions: {
407
862
  gltf: {
863
+ compileMaterials: true,
864
+ loadAllMaterials: true,
865
+ loadOnlyMaterials: container.name === "materials",
408
866
  preprocessUrlAsync: this.#transformUrl,
409
867
  },
410
868
  },
@@ -413,58 +871,198 @@ class PrefViewer extends HTMLElement {
413
871
  return LoadAssetContainerAsync(file || source, this.#scene, options);
414
872
  }
415
873
 
416
- async #loadContainers(loadModel = true, loadEnvironment = true) {
417
- const promiseArray = [];
874
+ async #loadContainers(loadModel = true, loadEnvironment = true, loadMaterials = true) {
875
+ this.#engine.stopRenderLoop(this.#renderLoop);
418
876
 
419
- promiseArray.push(loadModel ? this.#loadAssetContainer(this.#model.storage) : false);
420
- promiseArray.push(loadEnvironment ? this.#loadAssetContainer(this.#environment.storage) : false);
877
+ const promiseArray = [];
878
+ promiseArray.push(loadModel ? this.#loadAssetContainer(this.#data.containers.model) : false);
879
+ promiseArray.push(loadEnvironment ? this.#loadAssetContainer(this.#data.containers.environment) : false);
880
+ promiseArray.push(loadMaterials ? this.#loadAssetContainer(this.#data.containers.materials) : false);
421
881
 
422
882
  Promise.allSettled(promiseArray)
423
- .then(async (values) => {
883
+ .then((values) => {
424
884
  const modelContainer = values[0];
425
885
  const environmentContainer = values[1];
886
+ const materialsContainer = values[2];
426
887
 
427
888
  if (modelContainer.status === "fulfilled" && modelContainer.value) {
428
- this.#replaceContainer(this.#model, modelContainer.value);
889
+ modelContainer.value.lights = [];
890
+ this.#replaceContainer(this.#data.containers.model, modelContainer.value);
891
+ this.#storeChangedFlagsForContainer(this.#data.containers.model, true);
892
+ } else {
893
+ if (this.#data.containers.model.assetContainer && this.#data.containers.model.show !== this.#data.containers.model.visible) {
894
+ this.#data.containers.model.show ? this.#addContainer(this.#data.containers.model) : this.#removeContainer(this.#data.containers.model);
895
+ }
896
+ this.#storeChangedFlagsForContainer(this.#data.containers.model, false);
429
897
  }
430
898
 
431
899
  if (environmentContainer.status === "fulfilled" && environmentContainer.value) {
432
- this.#replaceContainer(this.#environment, environmentContainer.value);
900
+ this.#replaceContainer(this.#data.containers.environment, environmentContainer.value);
901
+ this.#storeChangedFlagsForContainer(this.#data.containers.environment, true);
902
+ } else {
903
+ if (this.#data.containers.environment.assetContainer && this.#data.containers.environment.show !== this.#data.containers.environment.visible) {
904
+ this.#data.containers.environment.show ? this.#addContainer(this.#data.containers.environment) : this.#removeContainer(this.#data.containers.environment);
905
+ }
906
+ this.#storeChangedFlagsForContainer(this.#data.containers.environment, false);
433
907
  }
434
908
 
909
+ if (materialsContainer.status === "fulfilled" && materialsContainer.value) {
910
+ this.#replaceContainer(this.#data.containers.materials, materialsContainer.value);
911
+ this.#storeChangedFlagsForContainer(this.#data.containers.materials, true);
912
+ } else {
913
+ this.#storeChangedFlagsForContainer(this.#data.containers.materials, false);
914
+ }
915
+
916
+ this.#setOptionsMaterials();
917
+ this.#setOptionsCamera();
435
918
  this.#setVisibilityOfWallAndFloorInModel();
436
-
437
- this.dispatchEvent(
438
- new CustomEvent("model-loaded", {
439
- detail: { success: "" },
440
- bubbles: true,
441
- composed: true,
442
- })
443
- );
444
919
  })
445
920
  .catch((error) => {
921
+ this.loaded = true;
446
922
  console.error("PrefViewer: failed to load model", error);
447
923
  this.dispatchEvent(
448
- new CustomEvent("model-error", {
449
- detail: { error: error },
924
+ new CustomEvent("scene-error", {
450
925
  bubbles: true,
926
+ cancelable: false,
451
927
  composed: true,
928
+ detail: { error: error },
452
929
  })
453
930
  );
931
+ })
932
+ .finally(async () => {
933
+ this.#setMaxSimultaneousLights();
934
+ this.#initShadows();
935
+ await this.#setStatusLoaded();
454
936
  });
455
937
  }
456
938
 
939
+ // Tasks
940
+ #addTaskToQueue(value, type) {
941
+ this.#taskQueue.push(new PrefViewerTask(value, type));
942
+ if (this.initialized && !this.loading) {
943
+ this.#processNextTask();
944
+ }
945
+ }
946
+
947
+ #processNextTask() {
948
+ if (!this.#taskQueue.length) {
949
+ return false;
950
+ }
951
+ const task = this.#taskQueue[0];
952
+ this.#taskQueue.shift();
953
+ switch (task.type) {
954
+ case PrefViewerTask.Types.Config:
955
+ this.#processConfig(task.value);
956
+ break;
957
+ case PrefViewerTask.Types.Model:
958
+ this.#processModel(task.value);
959
+ break;
960
+ case PrefViewerTask.Types.Environment:
961
+ this.#processEnvironment(task.value);
962
+ break;
963
+ case PrefViewerTask.Types.Materials:
964
+ this.#processMaterials(task.value);
965
+ break;
966
+ case PrefViewerTask.Types.Options:
967
+ this.#processOptions(task.value);
968
+ break;
969
+ }
970
+ }
971
+
972
+ #processConfig(config) {
973
+ this.#setStatusLoading();
974
+
975
+ // Containers
976
+ const loadModel = !!config.model?.storage;
977
+ this.#data.containers.model.changed.pending = loadModel;
978
+ this.#data.containers.model.changed.success = false;
979
+ this.#data.containers.model.changed.storage = this.#data.containers.model.storage;
980
+ this.#data.containers.model.storage = loadModel ? config.model.storage : this.#data.containers.model.storage;
981
+ this.#data.containers.model.show = config.model?.visible !== undefined ? config.model.visible : this.#data.containers.model.show;
982
+
983
+ const loadEnvironment = !!config.scene?.storage;
984
+ this.#data.containers.environment.changed.pending = loadEnvironment;
985
+ this.#data.containers.environment.changed.success = false;
986
+ this.#data.containers.environment.changed.storage = this.#data.containers.environment.storage;
987
+ this.#data.containers.environment.storage = loadEnvironment ? config.scene.storage : this.#data.containers.environment.storage;
988
+ this.#data.containers.environment.show = config.scene?.visible !== undefined ? config.scene.visible : this.#data.containers.environment.show;
989
+
990
+ const loadMaterials = !!config.materials?.storage;
991
+ this.#data.containers.materials.changed.pending = loadMaterials;
992
+ this.#data.containers.materials.changed.success = false;
993
+ this.#data.containers.materials.changed.storage = this.#data.containers.materials.storage;
994
+ this.#data.containers.materials.storage = loadMaterials ? config.materials.storage : this.#data.containers.materials.storage;
995
+
996
+ // Options
997
+ if (config.options) {
998
+ this.#checkCameraChanged(config.options);
999
+ this.#checkMaterialsChanged(config.options);
1000
+ }
1001
+
1002
+ this.#loadContainers(loadModel, loadEnvironment, loadMaterials);
1003
+ }
1004
+
1005
+ #processModel(model) {
1006
+ this.#setStatusLoading();
1007
+
1008
+ const loadModel = !!model.storage;
1009
+ this.#data.containers.model.changed.pending = loadModel;
1010
+ this.#data.containers.model.changed.success = false;
1011
+ this.#data.containers.model.changed.storage = this.#data.containers.model.storage;
1012
+ this.#data.containers.model.storage = loadModel ? model.storage : this.#data.containers.model.storage;
1013
+ this.#data.containers.model.show = model.visible !== undefined ? model.visible : this.#data.containers.model.show;
1014
+
1015
+ this.initialized && this.#loadContainers(loadModel, false, false);
1016
+ }
1017
+
1018
+ #processEnvironment(environment) {
1019
+ this.#setStatusLoading();
1020
+
1021
+ const loadEnvironment = !!environment.storage;
1022
+ this.#data.containers.environment.changed.pending = loadEnvironment;
1023
+ this.#data.containers.environment.changed.success = false;
1024
+ this.#data.containers.environment.changed.storage = this.#data.containers.environment.storage;
1025
+ this.#data.containers.environment.storage = loadEnvironment ? environment.storage : this.#data.containers.environment.storage;
1026
+ this.#data.containers.environment.show = environment.visible !== undefined ? environment.visible : this.#data.containers.environment.show;
1027
+
1028
+ this.#loadContainers(false, loadEnvironment, false);
1029
+ }
1030
+
1031
+ #processMaterials(materials) {
1032
+ this.#setStatusLoading();
1033
+
1034
+ const loadMaterials = !!materials.storage;
1035
+ this.#data.containers.materials.changed.pending = loadMaterials;
1036
+ this.#data.containers.materials.changed.success = false;
1037
+ this.#data.containers.materials.changed.storage = this.#data.containers.materials.storage;
1038
+ this.#data.containers.materials.storage = loadMaterials ? materials.storage : this.#data.containers.materials.storage;
1039
+
1040
+ this.#loadContainers(false, false, loadMaterials);
1041
+ }
1042
+
1043
+ async #processOptions(options) {
1044
+ this.#setStatusLoading();
1045
+
1046
+ let someSetted = false;
1047
+ if (this.#checkCameraChanged(options)) {
1048
+ someSetted = someSetted || this.#setOptionsCamera();
1049
+ }
1050
+ if (this.#checkMaterialsChanged(options)) {
1051
+ someSetted = someSetted || this.#setOptionsMaterials();
1052
+ }
1053
+
1054
+ await this.#setStatusLoaded();
1055
+
1056
+ return someSetted;
1057
+ }
1058
+
457
1059
  // Public Methods
458
1060
  loadConfig(config) {
459
1061
  config = typeof config === "string" ? JSON.parse(config) : config;
460
1062
  if (!config) {
461
1063
  return false;
462
1064
  }
463
- this.#model.storage = config.model?.storage || null;
464
- this.#model.show = config.model?.visible !== undefined ? config.model.visible : this.#model.show;
465
- this.#environment.storage = config.scene?.storage || null;
466
- this.#environment.show = config.scene?.visible !== undefined ? config.scene.visible : this.#environment.show;
467
- this.#initialized && this.#loadContainers(true, true);
1065
+ this.#addTaskToQueue(config, "config");
468
1066
  }
469
1067
 
470
1068
  loadModel(model) {
@@ -472,9 +1070,7 @@ class PrefViewer extends HTMLElement {
472
1070
  if (!model) {
473
1071
  return false;
474
1072
  }
475
- this.#model.storage = model.storage || null;
476
- this.#model.show = model.visible !== undefined ? model.visible : this.#model.show;
477
- this.#initialized && this.#loadContainers(true, false);
1073
+ this.#addTaskToQueue(model, "model");
478
1074
  }
479
1075
 
480
1076
  loadScene(scene) {
@@ -482,43 +1078,55 @@ class PrefViewer extends HTMLElement {
482
1078
  if (!scene) {
483
1079
  return false;
484
1080
  }
485
- this.#environment.storage = scene.storage || null;
486
- this.#environment.show = scene.visible !== undefined ? scene.visible : this.#environment.show;
487
- this.#initialized && this.#loadContainers(false, true);
1081
+ this.#addTaskToQueue(scene, "environment");
1082
+ }
1083
+
1084
+ loadMaterials(materials) {
1085
+ materials = typeof materials === "string" ? JSON.parse(materials) : materials;
1086
+ if (!materials) {
1087
+ return false;
1088
+ }
1089
+ this.#addTaskToQueue(materials, "materials");
1090
+ }
1091
+
1092
+ setOptions(options) {
1093
+ options = typeof options === "string" ? JSON.parse(options) : options;
1094
+ if (!options) {
1095
+ return false;
1096
+ }
1097
+ this.#addTaskToQueue(options, "options");
488
1098
  }
489
1099
 
490
1100
  showModel() {
491
- this.#model.show = true;
492
- this.#addContainer(this.#model);
1101
+ this.#data.containers.model.show = true;
1102
+ this.#addContainer(this.#data.containers.model);
493
1103
  }
494
1104
 
495
1105
  hideModel() {
496
- this.#model.show = false;
497
- this.#removeContainer(this.#model);
1106
+ this.#data.containers.model.show = false;
1107
+ this.#removeContainer(this.#data.containers.model);
498
1108
  }
499
1109
 
500
1110
  showScene() {
501
- this.#environment.show = true;
502
- this.#addContainer(this.#environment);
1111
+ this.#data.containers.environment.show = true;
1112
+ this.#addContainer(this.#data.containers.environment);
503
1113
  this.#setVisibilityOfWallAndFloorInModel();
504
1114
  }
505
1115
 
506
1116
  hideScene() {
507
- this.#environment.show = false;
508
- this.#removeContainer(this.#environment);
1117
+ this.#data.containers.environment.show = false;
1118
+ this.#removeContainer(this.#data.containers.environment);
509
1119
  this.#setVisibilityOfWallAndFloorInModel();
510
1120
  }
511
1121
 
512
1122
  downloadModelGLB() {
513
1123
  const fileName = "model";
514
- GLTF2Export.GLBAsync(this.#model.container, fileName, { exportWithoutWaitingForScene: true }).then((glb) => {
515
- glb.downloadFiles();
516
- });
1124
+ GLTF2Export.GLBAsync(this.#data.containers.model.assetContainer, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
517
1125
  }
518
1126
 
519
1127
  downloadModelUSDZ() {
520
1128
  const fileName = "model";
521
- USDZExportAsync(this.#model.container).then((response) => {
1129
+ USDZExportAsync(this.#data.containers.model.assetContainer).then((response) => {
522
1130
  if (response) {
523
1131
  Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
524
1132
  }
@@ -536,9 +1144,7 @@ class PrefViewer extends HTMLElement {
536
1144
 
537
1145
  downloadModelAndSceneGLB() {
538
1146
  const fileName = "scene";
539
- GLTF2Export.GLBAsync(this.#scene, fileName, { exportWithoutWaitingForScene: true }).then((glb) => {
540
- glb.downloadFiles();
541
- });
1147
+ GLTF2Export.GLBAsync(this.#scene, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
542
1148
  }
543
1149
  }
544
1150