@preference-sl/pref-viewer 2.10.0-beta.2 → 2.10.0-beta.4

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +327 -89
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.10.0-beta.2",
3
+ "version": "2.10.0-beta.4",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "scripts": {
package/src/index.js CHANGED
@@ -39,7 +39,7 @@
39
39
  * </pref-viewer>
40
40
  * ```
41
41
  */
42
- import { Engine, Scene, ArcRotateCamera, Vector3, Color4, HemisphericLight, DirectionalLight, PointLight, ShadowGenerator, LoadAssetContainerAsync, Tools, WebXRSessionManager, WebXRDefaultExperience } from "@babylonjs/core";
42
+ import { Engine, Scene, ArcRotateCamera, Vector3, Color4, HemisphericLight, DirectionalLight, PointLight, ShadowGenerator, LoadAssetContainerAsync, Tools, WebXRSessionManager, WebXRDefaultExperience, MeshBuilder, WebXRFeatureName } 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";
@@ -49,20 +49,72 @@ import { initDb, loadModel } from "./gltf-storage.js";
49
49
  class PrefViewer extends HTMLElement {
50
50
  #initialized = false;
51
51
 
52
- #model = {
53
- container: null,
54
- show: true, // Show model by default
55
- storage: null,
56
- visible: false,
57
- };
58
-
59
- #environment = {
60
- container: null,
61
- show: true, // Show environment by default
62
- storage: null,
63
- visible: false,
52
+ #data = {
53
+ containers: {
54
+ model: {
55
+ name: "model",
56
+ container: null,
57
+ show: true,
58
+ storage: null,
59
+ visible: false,
60
+ size: null,
61
+ timestamp: null,
62
+ changed: false,
63
+ },
64
+ environment: {
65
+ name: "environment",
66
+ container: null,
67
+ show: true,
68
+ storage: null,
69
+ visible: false,
70
+ size: null,
71
+ timestamp: null,
72
+ changed: false,
73
+ },
74
+ materials: {
75
+ name: "materials",
76
+ container: null,
77
+ storage: null,
78
+ show: true,
79
+ visible: false,
80
+ size: null,
81
+ timestamp: null,
82
+ changed: false,
83
+ },
84
+ },
85
+ options: {
86
+ camera: {
87
+ value: null,
88
+ locked: true,
89
+ changed: false,
90
+ },
91
+ materials: {
92
+ innerWall: {
93
+ value: null,
94
+ prefix: "innerWall",
95
+ changed: false,
96
+ },
97
+ outerWall: {
98
+ value: null,
99
+ prefix: "outerWall",
100
+ changed: false,
101
+ },
102
+ innerFloor: {
103
+ value: null,
104
+ prefix: "innerFloor",
105
+ changed: false,
106
+ },
107
+ outerFloor: {
108
+ value: null,
109
+ prefix: "outerFloor",
110
+ changed: false,
111
+ },
112
+ },
113
+ },
64
114
  };
65
115
 
116
+ // DOM elements
117
+ #wrapper = null;
66
118
  #canvas = null;
67
119
 
68
120
  // Babylon.js core objects
@@ -113,7 +165,7 @@ class PrefViewer extends HTMLElement {
113
165
  if (this.#initialized) {
114
166
  data ? this.showModel() : this.hideModel();
115
167
  } else {
116
- this.#model.show = data;
168
+ this.#data.containers.model.show = data;
117
169
  }
118
170
  break;
119
171
  case "show-scene":
@@ -121,7 +173,7 @@ class PrefViewer extends HTMLElement {
121
173
  if (this.#initialized) {
122
174
  data ? this.showScene() : this.hideScene();
123
175
  } else {
124
- this.#environment.show = data;
176
+ this.#data.containers.environment.show = data;
125
177
  }
126
178
  break;
127
179
  }
@@ -142,7 +194,7 @@ class PrefViewer extends HTMLElement {
142
194
  }
143
195
 
144
196
  this.#initializeBabylon();
145
- this.#loadContainers(true, true);
197
+ this.#loadContainers(true, true, true);
146
198
  this.#initialized = true;
147
199
  }
148
200
 
@@ -163,17 +215,48 @@ class PrefViewer extends HTMLElement {
163
215
  }
164
216
 
165
217
  #wrapCanvas() {
166
- const wrapper = document.createElement("div");
167
- Object.assign(wrapper.style, {
218
+ this.#wrapper = document.createElement("div");
219
+ Object.assign(this.#wrapper.style, {
168
220
  width: "100%",
169
221
  height: "100%",
170
222
  position: "relative",
171
223
  });
172
- wrapper.appendChild(this.#canvas);
173
- this.shadowRoot.append(wrapper);
224
+ this.#wrapper.appendChild(this.#canvas);
225
+ this.shadowRoot.append(this.#wrapper);
174
226
  }
175
-
176
- // Bbylon.js
227
+
228
+ // Data
229
+ #checkCameraChanged(options) {
230
+ if (!options || !options.camera) {
231
+ return false;
232
+ }
233
+ this.#data.options.camera.changed = options.camera && options.camera !== this.#data.options.camera.value ? true : false;
234
+ this.#data.options.camera.value = this.#data.options.camera.changed ? options.camera : this.#data.options.camera.value;
235
+ }
236
+
237
+ #checkMaterialsChanged(options) {
238
+ if (!options) {
239
+ return false;
240
+ }
241
+ Object.keys(this.#data.options.materials).forEach((material) => {
242
+ const key = `${material}Material`;
243
+ this.#data.options.materials[material].changed = options[key] && options[key] !== this.#data.options.materials[material].value ? true : false;
244
+ this.#data.options.materials[material].value = this.#data.options.materials[material].changed ? options[key] : this.#data.options.materials[material].value;
245
+ });
246
+ }
247
+
248
+ #storeChangedFlagsForContainer(container) {
249
+ container.timestamp = container.changed.timestamp;
250
+ container.size = container.changed.size;
251
+ }
252
+
253
+ #resetChangedFlags() {
254
+ Object.values(this.#data.containers).forEach((container) => (container.changed = false));
255
+ Object.values(this.#data.options.materials).forEach((material) => (material.changed = false));
256
+ this.#data.options.camera.changed = false;
257
+ }
258
+
259
+ // Babylon.js
177
260
  async #initializeBabylon() {
178
261
  this.#engine = new Engine(this.#canvas, true, { alpha: true });
179
262
  this.#scene = new Scene(this.#engine);
@@ -181,18 +264,25 @@ class PrefViewer extends HTMLElement {
181
264
  this.#createCamera();
182
265
  this.#createLights();
183
266
  this.#setupInteraction();
184
-
267
+
185
268
  this.#engine.runRenderLoop(() => this.#scene && this.#scene.render());
186
269
  this.#canvasResizeObserver.observe(this.#canvas);
187
270
 
188
271
  await this.#createXRExperience();
189
272
  }
190
273
 
274
+ addStylesToARButton() {
275
+ 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"}';
276
+ const style = document.createElement("style");
277
+ style.appendChild(document.createTextNode(css));
278
+ this.#wrapper.appendChild(style);
279
+ }
280
+
191
281
  async #createXRExperience() {
192
282
  if (this.#XRExperience) {
193
283
  return true;
194
284
  }
195
-
285
+
196
286
  const sessionMode = "immersive-ar";
197
287
  const sessionSupported = await WebXRSessionManager.IsSessionSupportedAsync(sessionMode);
198
288
  if (!sessionSupported) {
@@ -200,16 +290,37 @@ class PrefViewer extends HTMLElement {
200
290
  return false;
201
291
  }
202
292
 
203
- const options = {
204
- uiOptions: {
205
- sessionMode: sessionMode,
206
- renderTarget: "xrLayer",
207
- referenceSpaceType: "local",
208
- },
209
- };
210
-
211
293
  try {
212
- this.#XRExperience = await WebXRDefaultExperience.CreateAsync(this.#scene, options);
294
+ const ground = MeshBuilder.CreateGround("ground", { width: 1000, height: 1000 }, this.#scene);
295
+ ground.isVisible = false;
296
+
297
+ const options = {
298
+ floorMeshes: [ground],
299
+ uiOptions: {
300
+ sessionMode: sessionMode,
301
+ renderTarget: "xrLayer",
302
+ referenceSpaceType: "local",
303
+ },
304
+ optionalFeatures: true,
305
+ };
306
+
307
+ this.#XRExperience = await WebXRDefaultExperience.CreateAsync(this.#scene, options);
308
+
309
+ const featuresManager = this.#XRExperience.baseExperience.featuresManager;
310
+ featuresManager.enableFeature(WebXRFeatureName.TELEPORTATION, "stable", {
311
+ xrInput: this.#XRExperience.input,
312
+ floorMeshes: [ground],
313
+ timeToTeleport: 1500,
314
+ });
315
+
316
+ this.#XRExperience.baseExperience.sessionManager.onXRReady.add(() => {
317
+ // Set the initial position of xrCamera: use nonVRCamera, which contains a copy of the original this.#scene.activeCamera before entering XR
318
+ this.#XRExperience.baseExperience.camera.setTransformationFromNonVRCamera(this.#XRExperience.baseExperience._nonVRCamera);
319
+ this.#XRExperience.baseExperience.camera.setTarget(Vector3.Zero());
320
+ this.#XRExperience.baseExperience.onInitialXRPoseSetObservable.notifyObservers(this.#XRExperience.baseExperience.camera);
321
+ });
322
+
323
+ this.addStylesToARButton();
213
324
  } catch (error) {
214
325
  console.warn("PrefViewer: failed to create WebXR experience", error);
215
326
  this.#XRExperience = null;
@@ -219,11 +330,13 @@ class PrefViewer extends HTMLElement {
219
330
  #canvasResizeObserver = new ResizeObserver(() => this.#engine && this.#engine.resize());
220
331
 
221
332
  #createCamera() {
222
- this.#camera = new ArcRotateCamera("camera", 3 * Math.PI / 2, Math.PI * 0.47, 10, Vector3.Zero(), this.#scene);
333
+ this.#camera = new ArcRotateCamera("camera", (3 * Math.PI) / 2, Math.PI * 0.47, 10, Vector3.Zero(), this.#scene);
223
334
  this.#camera.upperBetaLimit = Math.PI * 0.48;
224
335
  this.#camera.lowerBetaLimit = Math.PI * 0.25;
225
336
  this.#camera.lowerRadiusLimit = 5;
226
337
  this.#camera.upperRadiusLimit = 20;
338
+ this.#camera.metadata = { locked: false }
339
+ this.#camera = this.#camera;
227
340
  this.#camera.attachControl(this.#canvas, true);
228
341
  }
229
342
 
@@ -251,10 +364,14 @@ class PrefViewer extends HTMLElement {
251
364
 
252
365
  #setupInteraction() {
253
366
  this.#canvas.addEventListener("wheel", (event) => {
254
- if (!this.#scene || !this.#camera) return;
255
- const pick = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
367
+ if (!this.#scene || !this.#camera) {
368
+ return false;
369
+ }
370
+ //const pick = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
256
371
  //this.#camera.target = pick.hit ? pick.pickedPoint.clone() : this.#camera.target;
257
- this.#camera.inertialRadiusOffset -= event.deltaY * this.#camera.wheelPrecision * 0.001;
372
+ if (!this.#scene.activeCamera.metadata?.locked) {
373
+ this.#scene.activeCamera.inertialRadiusOffset -= event.deltaY * this.#scene.activeCamera.wheelPrecision * 0.001;
374
+ }
258
375
  event.preventDefault();
259
376
  });
260
377
  }
@@ -268,6 +385,27 @@ class PrefViewer extends HTMLElement {
268
385
  }
269
386
 
270
387
  // Utility methods for loading gltf/glb
388
+ async #getServerFileDataHeader(uri) {
389
+ return new Promise((resolve) => {
390
+ const xhr = new XMLHttpRequest();
391
+ xhr.open("HEAD", uri, true);
392
+ xhr.responseType = "blob";
393
+ xhr.onload = () => {
394
+ if (xhr.status === 200) {
395
+ const size = parseInt(xhr.getResponseHeader("Content-Length"));
396
+ const timestamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
397
+ resolve(size, timestamp);
398
+ } else {
399
+ resolve(0, null);
400
+ }
401
+ };
402
+ xhr.onerror = () => {
403
+ resolve(0, null);
404
+ };
405
+ xhr.send();
406
+ });
407
+ }
408
+
271
409
  #transformUrl(url) {
272
410
  return new Promise((resolve) => {
273
411
  resolve(url.replace(/^blob:[^/]+\//i, "").replace(/\\/g, "/"));
@@ -280,10 +418,11 @@ class PrefViewer extends HTMLElement {
280
418
  let decoded = "";
281
419
  let blob = null;
282
420
  let extension = null;
421
+ let size = raw.length;
283
422
  try {
284
423
  decoded = atob(raw);
285
424
  } catch {
286
- return { blob, extension };
425
+ return { blob, extension, size };
287
426
  }
288
427
  let isJson = false;
289
428
  try {
@@ -294,7 +433,7 @@ class PrefViewer extends HTMLElement {
294
433
  const type = isJson ? "model/gltf+json" : "model/gltf-binary";
295
434
  const array = Uint8Array.from(decoded, (c) => c.charCodeAt(0));
296
435
  blob = new Blob([array], { type });
297
- return { blob, extension };
436
+ return { blob, extension, size };
298
437
  }
299
438
 
300
439
  async #initStorage(db, table) {
@@ -306,41 +445,95 @@ class PrefViewer extends HTMLElement {
306
445
 
307
446
  // Methods for managing Asset Containers
308
447
  #setVisibilityOfWallAndFloorInModel(show) {
309
- if (this.#model.container && this.#model.visible) {
310
- const names = ["outer_0", "inner_1", "outerFloor", "innerFloor"];
311
- const nodes = this.#model.container.getNodes();
312
- this.#model.container
313
- .getNodes()
314
- .filter((filter) => names.includes(filter.name))
315
- .forEach((node) => node.setEnabled(show !== undefined ? show : this.#environment.show));
448
+ if (!this.#data.containers.model.assetContainer || !this.#data.containers.model.visible) {
449
+ return false;
316
450
  }
451
+ show = show !== undefined ? show : this.#data.containers.environment.visible;
452
+ const prefixes = Object.values(this.#data.options.materials).map((material) => material.prefix);
453
+ this.#data.containers.model.assetContainer.meshes.filter((meshToFilter) => prefixes.some((prefix) => meshToFilter.name.startsWith(prefix))).forEach((mesh) => mesh.setEnabled(show));
317
454
  }
318
455
 
319
- #addContainer(group) {
320
- if (group.container && !group.visible && group.show) {
321
- group.container.addAllToScene();
322
- group.visible = true;
456
+ #setOptionsMaterial(optionMaterial) {
457
+ if (!optionMaterial || !optionMaterial.prefix || !optionMaterial.value) {
458
+ return false;
459
+ }
460
+
461
+ const material = this.#data.containers.materials.assetContainer?.materials.find((mat) => mat.name === optionMaterial.value) || null;
462
+ if (!material) {
463
+ return false;
464
+ }
465
+
466
+ const containers = [];
467
+ if (this.#data.containers.model.assetContainer && (this.#data.containers.model.assetContainer.changed || optionMaterial.changed)) {
468
+ containers.push(this.#data.containers.model.assetContainer);
469
+ }
470
+ if (this.#data.containers.environment.assetContainer && (this.#data.containers.environment.assetContainer.changed || optionMaterial.changed)) {
471
+ containers.push(this.#data.containers.environment.assetContainer);
472
+ }
473
+ if (containers.length === 0) {
474
+ return false;
323
475
  }
476
+
477
+ containers.forEach((container) => {
478
+ container.meshes.filter((meshToFilter) => meshToFilter.name.startsWith(optionMaterial.prefix)).forEach((mesh) => {
479
+ if (mesh.material) {
480
+ mesh.material.dispose();
481
+ }
482
+ mesh.material = material;
483
+ });
484
+ });
485
+
486
+ return true;
324
487
  }
325
488
 
326
- #removeContainer(group) {
327
- if (group.container && group.visible) {
328
- group.container.removeAllFromScene();
329
- group.visible = false;
489
+ #setOptionsMaterials() {
490
+ Object.values(this.#data.options.materials).forEach((material) => this.#setOptionsMaterial(material));
491
+ }
492
+
493
+ #setOptionsCamera() {
494
+ if (!this.#data.options.camera.value || (!this.#data.options.camera.changed && !this.#data.containers.model.assetContainer.changed)) {
495
+ return false;
496
+ }
497
+ let camera = this.#data.containers.model.assetContainer?.cameras.find((cam) => cam.name === this.#data.options.camera.value) || null;
498
+ if (!camera) {
499
+ camera = this.#camera;
500
+ } else {
501
+ camera.metadata = { locked: this.#data.options.camera.locked };
502
+ if (!this.#data.options.camera.locked) {
503
+ camera.attachControl(this.#canvas, true);
504
+ }
505
+ }
506
+
507
+ this.#scene.activeCamera = camera;
508
+ }
509
+
510
+ #addContainer(container) {
511
+ if (container.assetContainer && !container.visible && container.show) {
512
+ container.assetContainer.addAllToScene();
513
+ container.visible = true;
330
514
  }
331
515
  }
332
516
 
333
- #replaceContainer(group, newContainer) {
334
- this.#removeContainer(group);
335
- group.container = newContainer;
336
- group.container.meshes.forEach((mesh) => {
517
+ #removeContainer(container) {
518
+ if (container.assetContainer && container.visible) {
519
+ container.assetContainer.removeAllFromScene();
520
+ container.visible = false;
521
+ }
522
+ }
523
+
524
+ #replaceContainer(container, newAssetContainer) {
525
+ this.#removeContainer(container);
526
+ container.assetContainer = newAssetContainer;
527
+ container.assetContainer.meshes.forEach((mesh) => {
337
528
  mesh.receiveShadows = true;
338
529
  this.#shadowGen.addShadowCaster(mesh, true);
339
530
  });
340
- this.#addContainer(group);
531
+ this.#addContainer(container);
341
532
  }
342
533
 
343
- async #loadAssetContainer(storage) {
534
+ async #loadAssetContainer(container) {
535
+ let storage = container?.storage;
536
+
344
537
  if (!storage) {
345
538
  return false;
346
539
  }
@@ -351,6 +544,11 @@ class PrefViewer extends HTMLElement {
351
544
  await this.#initStorage(storage.db, storage.table);
352
545
  const object = await loadModel(storage.id, storage.table);
353
546
  source = object.data;
547
+ if (object.timestamp === container.timestamp) {
548
+ return false;
549
+ } else {
550
+ container.changed = { timestamp: object.timestamp, size: object.size };
551
+ }
354
552
  }
355
553
 
356
554
  if (!source) {
@@ -359,20 +557,34 @@ class PrefViewer extends HTMLElement {
359
557
 
360
558
  let file = null;
361
559
 
362
- let { blob, extension } = this.#decodeBase64(source);
560
+ let { blob, extension, size } = this.#decodeBase64(source);
363
561
  if (blob && extension) {
364
- file = new File([blob], `model${extension}`, {
562
+ file = new File([blob], `${container.name}${extension}`, {
365
563
  type: blob.type,
366
564
  });
565
+ if (!container.changed) {
566
+ if (container.timestamp === null && container.size === size) {
567
+ return false;
568
+ } else {
569
+ container.changed = { timestamp: null, size: size };
570
+ }
571
+ }
367
572
  } else {
368
573
  const extMatch = source.match(/\.(gltf|glb)(\?|#|$)/i);
369
574
  extension = extMatch ? `.${extMatch[1].toLowerCase()}` : ".gltf";
575
+ const { fileSize, fileTimestamp } = await this.#getServerFileDataHeader(source);
576
+ if (container.timestamp === fileTimestamp && container.size === fileSize) {
577
+ return false;
578
+ } else {
579
+ container.changed = { timestamp: fileTimestamp, size: fileSize };
580
+ }
370
581
  }
371
582
 
372
583
  let options = {
373
584
  pluginExtension: extension,
374
585
  pluginOptions: {
375
586
  gltf: {
587
+ loadAllMaterials: true,
376
588
  preprocessUrlAsync: this.#transformUrl,
377
589
  },
378
590
  },
@@ -381,27 +593,43 @@ class PrefViewer extends HTMLElement {
381
593
  return LoadAssetContainerAsync(file || source, this.#scene, options);
382
594
  }
383
595
 
384
- async #loadContainers(loadModel = true, loadEnvironment = true) {
596
+ async #loadContainers(loadModel = true, loadEnvironment = true, loadMaterials = true) {
385
597
  const promiseArray = [];
386
-
387
- promiseArray.push(loadModel ? this.#loadAssetContainer(this.#model.storage) : false);
388
- promiseArray.push(loadEnvironment ? this.#loadAssetContainer(this.#environment.storage) : false);
598
+ promiseArray.push(loadModel ? this.#loadAssetContainer(this.#data.containers.model) : false);
599
+ promiseArray.push(loadEnvironment ? this.#loadAssetContainer(this.#data.containers.environment) : false);
600
+ promiseArray.push(loadMaterials ? this.#loadAssetContainer(this.#data.containers.materials) : false);
389
601
 
390
602
  Promise.allSettled(promiseArray)
391
603
  .then(async (values) => {
392
604
  const modelContainer = values[0];
393
605
  const environmentContainer = values[1];
606
+ const materialsContainer = values[2];
394
607
 
395
608
  if (modelContainer.status === "fulfilled" && modelContainer.value) {
396
- this.#replaceContainer(this.#model, modelContainer.value);
609
+ this.#replaceContainer(this.#data.containers.model, modelContainer.value);
610
+ this.#storeChangedFlagsForContainer(this.#data.containers.model);
611
+ } else {
612
+ this.#data.containers.model.show ? this.#addContainer(this.#data.containers.model) : this.#removeContainer(this.#data.containers.model);
397
613
  }
398
614
 
399
615
  if (environmentContainer.status === "fulfilled" && environmentContainer.value) {
400
- this.#replaceContainer(this.#environment, environmentContainer.value);
616
+ this.#replaceContainer(this.#data.containers.environment, environmentContainer.value);
617
+ this.#storeChangedFlagsForContainer(this.#data.containers.environment);
618
+ } else {
619
+ this.#data.containers.environment.show ? this.#addContainer(this.#data.containers.environment) : this.#removeContainer(this.#data.containers.environment);
401
620
  }
402
621
 
622
+ if (materialsContainer.status === "fulfilled" && materialsContainer.value) {
623
+ this.#replaceContainer(this.#data.containers.materials, materialsContainer.value);
624
+ this.#storeChangedFlagsForContainer(this.#data.containers.materials);
625
+ }
626
+
627
+ this.#setOptionsMaterials();
628
+ this.#setOptionsCamera();
403
629
  this.#setVisibilityOfWallAndFloorInModel();
404
630
 
631
+ this.#resetChangedFlags();
632
+
405
633
  this.dispatchEvent(
406
634
  new CustomEvent("model-loaded", {
407
635
  detail: { success: "" },
@@ -428,11 +656,21 @@ class PrefViewer extends HTMLElement {
428
656
  if (!config) {
429
657
  return false;
430
658
  }
431
- this.#model.storage = config.model?.storage || null;
432
- this.#model.show = config.model?.visible !== undefined ? config.model.visible : this.#model.show;
433
- this.#environment.storage = config.scene?.storage || null;
434
- this.#environment.show = config.scene?.visible !== undefined ? config.scene.visible : this.#environment.show;
435
- this.#initialized && this.#loadContainers(true, true);
659
+
660
+ // AssetContainers
661
+ this.#data.containers.model.storage = config.model?.storage || null;
662
+ this.#data.containers.model.show = config.model?.visible !== undefined ? config.model.visible : this.#data.containers.model.show;
663
+ this.#data.containers.environment.storage = config.scene?.storage || null;
664
+ this.#data.containers.environment.show = config.scene?.visible !== undefined ? config.scene.visible : this.#data.containers.environment.show;
665
+ this.#data.containers.materials.storage = config.materials?.storage || null;
666
+
667
+ // Options
668
+ if (config.options) {
669
+ this.#checkCameraChanged(config.options);
670
+ this.#checkMaterialsChanged(config.options);
671
+ }
672
+
673
+ this.#initialized && this.#loadContainers(true, true, true);
436
674
  }
437
675
 
438
676
  loadModel(model) {
@@ -440,9 +678,9 @@ class PrefViewer extends HTMLElement {
440
678
  if (!model) {
441
679
  return false;
442
680
  }
443
- this.#model.storage = model.storage || null;
444
- this.#model.show = model.visible !== undefined ? model.visible : this.#model.show;
445
- this.#initialized && this.#loadContainers(true, false);
681
+ this.#data.containers.model.storage = model.storage || null;
682
+ this.#data.containers.model.show = model.visible !== undefined ? model.visible : this.#data.containers.model.show;
683
+ this.#initialized && this.#loadContainers(true, false, false);
446
684
  }
447
685
 
448
686
  loadScene(scene) {
@@ -450,43 +688,43 @@ class PrefViewer extends HTMLElement {
450
688
  if (!scene) {
451
689
  return false;
452
690
  }
453
- this.#environment.storage = scene.storage || null;
454
- this.#environment.show = scene.visible !== undefined ? scene.visible : this.#environment.show;
455
- this.#initialized && this.#loadContainers(false, true);
691
+ this.#data.containers.environment.storage = scene.storage || null;
692
+ this.#data.containers.environment.show = scene.visible !== undefined ? scene.visible : this.#data.containers.environment.show;
693
+ this.#initialized && this.#loadContainers(false, true, false);
456
694
  }
457
695
 
458
696
  showModel() {
459
- this.#model.show = true;
460
- this.#addContainer(this.#model);
697
+ this.#data.containers.model.show = true;
698
+ this.#addContainer(this.#data.containers.model);
461
699
  }
462
700
 
463
701
  hideModel() {
464
- this.#model.show = false;
465
- this.#removeContainer(this.#model);
702
+ this.#data.containers.model.show = false;
703
+ this.#removeContainer(this.#data.containers.model);
466
704
  }
467
705
 
468
706
  showScene() {
469
- this.#environment.show = true;
470
- this.#addContainer(this.#environment);
707
+ this.#data.containers.environment.show = true;
708
+ this.#addContainer(this.#data.containers.environment);
471
709
  this.#setVisibilityOfWallAndFloorInModel();
472
710
  }
473
711
 
474
712
  hideScene() {
475
- this.#environment.show = false;
476
- this.#removeContainer(this.#environment);
713
+ this.#data.containers.environment.show = false;
714
+ this.#removeContainer(this.#data.containers.environment);
477
715
  this.#setVisibilityOfWallAndFloorInModel();
478
716
  }
479
717
 
480
718
  downloadModelGLB() {
481
719
  const fileName = "model";
482
- GLTF2Export.GLBAsync(this.#model.container, fileName, { exportWithoutWaitingForScene: true }).then((glb) => {
720
+ GLTF2Export.GLBAsync(this.#data.containers.model.assetContainer, fileName, { exportWithoutWaitingForScene: true }).then((glb) => {
483
721
  glb.downloadFiles();
484
722
  });
485
723
  }
486
724
 
487
725
  downloadModelUSDZ() {
488
726
  const fileName = "model";
489
- USDZExportAsync(this.#model.container).then((response) => {
727
+ USDZExportAsync(this.#data.containers.model.assetContainer).then((response) => {
490
728
  if (response) {
491
729
  Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
492
730
  }