@preference-sl/pref-viewer 2.10.0-beta.8 → 2.10.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 (3) hide show
  1. package/package.json +5 -5
  2. package/src/gltf-storage.js +167 -193
  3. package/src/index.js +491 -115
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,75 +39,107 @@
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 PrefViewerTask {
50
+ static Types = Object.freeze({
51
+ Config: "config",
52
+ Environment: "environment",
53
+ Materials: "materials",
54
+ Model: "model",
55
+ Options: "options",
56
+ });
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;
73
+
74
+ Object.freeze(this);
75
+ }
76
+ }
77
+
49
78
  class PrefViewer extends HTMLElement {
50
- #initialized = false;
79
+ initialized = false;
80
+ loaded = false;
81
+ loading = false;
82
+ #taskQueue = [];
51
83
 
52
84
  #data = {
53
85
  containers: {
54
86
  model: {
55
87
  name: "model",
56
- container: null,
88
+ assetContainer: null,
57
89
  show: true,
58
90
  storage: null,
59
91
  visible: false,
60
92
  size: null,
61
- timestamp: null,
62
- changed: false,
93
+ timeStamp: null,
94
+ changed: { pending: false, success: false },
63
95
  },
64
96
  environment: {
65
97
  name: "environment",
66
- container: null,
98
+ assetContainer: null,
67
99
  show: true,
68
100
  storage: null,
69
101
  visible: false,
70
102
  size: null,
71
- timestamp: null,
72
- changed: false,
103
+ timeStamp: null,
104
+ changed: { pending: false, success: false },
73
105
  },
74
106
  materials: {
75
107
  name: "materials",
76
- container: null,
108
+ assetContainer: null,
77
109
  storage: null,
78
110
  show: true,
79
111
  visible: false,
80
112
  size: null,
81
- timestamp: null,
82
- changed: false,
113
+ timeStamp: null,
114
+ changed: { pending: false, success: false },
83
115
  },
84
116
  },
85
117
  options: {
86
118
  camera: {
87
119
  value: null,
88
120
  locked: true,
89
- changed: false,
121
+ changed: { pending: false, success: false },
90
122
  },
91
123
  materials: {
92
124
  innerWall: {
93
125
  value: null,
94
126
  prefix: "innerWall",
95
- changed: false,
127
+ changed: { pending: false, success: false },
96
128
  },
97
129
  outerWall: {
98
130
  value: null,
99
131
  prefix: "outerWall",
100
- changed: false,
132
+ changed: { pending: false, success: false },
101
133
  },
102
134
  innerFloor: {
103
135
  value: null,
104
136
  prefix: "innerFloor",
105
- changed: false,
137
+ changed: { pending: false, success: false },
106
138
  },
107
139
  outerFloor: {
108
140
  value: null,
109
141
  prefix: "outerFloor",
110
- changed: false,
142
+ changed: { pending: false, success: false },
111
143
  },
112
144
  },
113
145
  },
@@ -160,9 +192,15 @@ class PrefViewer extends HTMLElement {
160
192
  case "scene":
161
193
  this.loadScene(value);
162
194
  break;
195
+ case "materials":
196
+ this.loadMaterials(value);
197
+ break;
198
+ case "options":
199
+ this.setOptions(value);
200
+ break;
163
201
  case "show-model":
164
202
  data = value.toLowerCase?.() === "true";
165
- if (this.#initialized) {
203
+ if (this.initialized) {
166
204
  data ? this.showModel() : this.hideModel();
167
205
  } else {
168
206
  this.#data.containers.model.show = data;
@@ -170,7 +208,7 @@ class PrefViewer extends HTMLElement {
170
208
  break;
171
209
  case "show-scene":
172
210
  data = value.toLowerCase?.() === "true";
173
- if (this.#initialized) {
211
+ if (this.initialized) {
174
212
  data ? this.showScene() : this.hideScene();
175
213
  } else {
176
214
  this.#data.containers.environment.show = data;
@@ -184,18 +222,19 @@ class PrefViewer extends HTMLElement {
184
222
  const error = 'PrefViewer: provide "models" as array of model and environment';
185
223
  console.error(error);
186
224
  this.dispatchEvent(
187
- new CustomEvent("model-error", {
188
- detail: { error: new Error(error) },
225
+ new CustomEvent("scene-error", {
189
226
  bubbles: true,
227
+ cancelable: false,
190
228
  composed: true,
229
+ detail: { error: new Error(error) },
191
230
  })
192
231
  );
193
232
  return false;
194
233
  }
195
234
 
196
235
  this.#initializeBabylon();
197
- this.#loadContainers(true, true, true);
198
- this.#initialized = true;
236
+ this.initialized = true;
237
+ this.#processNextTask();
199
238
  }
200
239
 
201
240
  disconnectedCallback() {
@@ -225,14 +264,90 @@ class PrefViewer extends HTMLElement {
225
264
  this.shadowRoot.append(this.#wrapper);
226
265
  }
227
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
+
228
335
  // Data
229
336
  #checkCameraChanged(options) {
230
337
  if (!options || !options.camera) {
231
338
  return false;
232
339
  }
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
- return this.#data.options.camera.changed;
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;
236
351
  }
237
352
 
238
353
  #checkMaterialsChanged(options) {
@@ -242,40 +357,61 @@ class PrefViewer extends HTMLElement {
242
357
  let someChanged = false;
243
358
  Object.keys(this.#data.options.materials).forEach((material) => {
244
359
  const key = `${material}Material`;
245
- this.#data.options.materials[material].changed = options[key] && options[key] !== this.#data.options.materials[material].value ? true : false;
246
- this.#data.options.materials[material].value = this.#data.options.materials[material].changed ? options[key] : this.#data.options.materials[material].value;
247
- someChanged = someChanged || this.#data.options.materials[material].changed;
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;
248
372
  });
249
373
  return someChanged;
250
374
  }
251
375
 
252
- #storeChangedFlagsForContainer(container) {
253
- container.timestamp = container.changed.timestamp;
254
- container.size = container.changed.size;
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
+ }
255
384
  }
256
385
 
257
386
  #resetChangedFlags() {
258
- Object.values(this.#data.containers).forEach((container) => (container.changed = false));
259
- Object.values(this.#data.options.materials).forEach((material) => (material.changed = false));
260
- this.#data.options.camera.changed = false;
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);
261
393
  }
262
394
 
263
395
  // Babylon.js
264
396
  async #initializeBabylon() {
265
397
  this.#engine = new Engine(this.#canvas, true, { alpha: true });
398
+ this.#engine.disableUniformBuffers = true;
266
399
  this.#scene = new Scene(this.#engine);
267
400
  this.#scene.clearColor = new Color4(1, 1, 1, 1);
268
401
  this.#createCamera();
269
402
  this.#createLights();
270
403
  this.#setupInteraction();
271
-
272
- this.#engine.runRenderLoop(() => this.#scene && this.#scene.render());
273
- this.#canvasResizeObserver.observe(this.#canvas);
274
-
275
404
  await this.#createXRExperience();
405
+ this.#engine.runRenderLoop(this.#renderLoop);
406
+ this.#canvasResizeObserver.observe(this.#canvas);
276
407
  }
277
408
 
278
- 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() {
279
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"}';
280
416
  const style = document.createElement("style");
281
417
  style.appendChild(document.createTextNode(css));
@@ -324,7 +460,7 @@ class PrefViewer extends HTMLElement {
324
460
  this.#XRExperience.baseExperience.onInitialXRPoseSetObservable.notifyObservers(this.#XRExperience.baseExperience.camera);
325
461
  });
326
462
 
327
- this.addStylesToARButton();
463
+ this.#addStylesToARButton();
328
464
  } catch (error) {
329
465
  console.warn("PrefViewer: failed to create WebXR experience", error);
330
466
  this.#XRExperience = null;
@@ -339,12 +475,18 @@ class PrefViewer extends HTMLElement {
339
475
  this.#camera.lowerBetaLimit = Math.PI * 0.25;
340
476
  this.#camera.lowerRadiusLimit = 5;
341
477
  this.#camera.upperRadiusLimit = 20;
342
- this.#camera.metadata = { locked: false }
343
- this.#camera = this.#camera;
478
+ this.#camera.metadata = { locked: false };
344
479
  this.#camera.attachControl(this.#canvas, true);
480
+ this.#scene.activeCamera = this.#camera;
345
481
  }
346
482
 
347
483
  #createLights() {
484
+ this.#initEnvironmentTexture();
485
+
486
+ if (this.#scene.environmentTexture) {
487
+ return true;
488
+ }
489
+
348
490
  // 1) Stronger ambient fill
349
491
  this.#hemiLight = new HemisphericLight("hemiLight", new Vector3(-10, 10, -10), this.#scene);
350
492
  this.#hemiLight.intensity = 0.6;
@@ -354,7 +496,7 @@ class PrefViewer extends HTMLElement {
354
496
  this.#dirLight.position = new Vector3(5, 4, 5); // light is IN FRONT + ABOVE + to the RIGHT
355
497
  this.#dirLight.intensity = 0.6;
356
498
 
357
- // 3) Soft shadows
499
+ // // 3) Soft shadows
358
500
  this.#shadowGen = new ShadowGenerator(1024, this.#dirLight);
359
501
  this.#shadowGen.useBlurExponentialShadowMap = true;
360
502
  this.#shadowGen.blurKernel = 16;
@@ -366,6 +508,94 @@ class PrefViewer extends HTMLElement {
366
508
  this.#cameraLight.intensity = 0.3;
367
509
  }
368
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
+
369
599
  #setupInteraction() {
370
600
  this.#canvas.addEventListener("wheel", (event) => {
371
601
  if (!this.#scene || !this.#camera) {
@@ -397,14 +627,14 @@ class PrefViewer extends HTMLElement {
397
627
  xhr.onload = () => {
398
628
  if (xhr.status === 200) {
399
629
  const size = parseInt(xhr.getResponseHeader("Content-Length"));
400
- const timestamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
401
- resolve(size, timestamp);
630
+ const timeStamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
631
+ resolve([size, timeStamp]);
402
632
  } else {
403
- resolve(0, null);
633
+ resolve([0, null]);
404
634
  }
405
635
  };
406
636
  xhr.onerror = () => {
407
- resolve(0, null);
637
+ resolve([0, null]);
408
638
  };
409
639
  xhr.send();
410
640
  });
@@ -432,7 +662,7 @@ class PrefViewer extends HTMLElement {
432
662
  try {
433
663
  JSON.parse(decoded);
434
664
  isJson = true;
435
- } catch {}
665
+ } catch { }
436
666
  extension = isJson ? ".gltf" : ".glb";
437
667
  const type = isJson ? "model/gltf+json" : "model/gltf-binary";
438
668
  const array = Uint8Array.from(decoded, (c) => c.charCodeAt(0));
@@ -468,10 +698,10 @@ class PrefViewer extends HTMLElement {
468
698
  }
469
699
 
470
700
  const containers = [];
471
- if (this.#data.containers.model.assetContainer && (this.#data.containers.model.assetContainer.changed || optionMaterial.changed)) {
701
+ if (this.#data.containers.model.assetContainer && (this.#data.containers.model.changed.pending || this.#data.containers.materials.changed.pending || optionMaterial.changed.pending)) {
472
702
  containers.push(this.#data.containers.model.assetContainer);
473
703
  }
474
- if (this.#data.containers.environment.assetContainer && (this.#data.containers.environment.assetContainer.changed || optionMaterial.changed)) {
704
+ if (this.#data.containers.environment.assetContainer && (this.#data.containers.environment.changed.pending || this.#data.containers.materials.changed.pending || optionMaterial.changed.pending)) {
475
705
  containers.push(this.#data.containers.environment.assetContainer);
476
706
  }
477
707
  if (containers.length === 0) {
@@ -488,6 +718,13 @@ class PrefViewer extends HTMLElement {
488
718
  })
489
719
  );
490
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
+
491
728
  return someSetted;
492
729
  }
493
730
 
@@ -501,46 +738,69 @@ class PrefViewer extends HTMLElement {
501
738
  }
502
739
 
503
740
  #setOptionsCamera() {
504
- if (!this.#data.options.camera.value || (!this.#data.options.camera.changed && !this.#data.containers.model.assetContainer.changed)) {
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) {
505
742
  return false;
506
743
  }
507
744
 
508
- let camera = this.#data.containers.model.assetContainer?.cameras.find((cam) => cam.name === this.#data.options.camera.value) || null;
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;
509
746
  if (!camera) {
510
- return false;
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
+ }
511
766
  }
512
-
513
- camera.metadata = { locked: this.#data.options.camera.locked };
514
- if (!this.#data.options.camera.locked) {
767
+ if (!this.#data.options.camera.locked && this.#data.options.camera.value !== null) {
515
768
  camera.attachControl(this.#canvas, true);
516
769
  }
517
770
  this.#scene.activeCamera = camera;
518
-
519
771
  return true;
520
772
  }
521
773
 
522
774
  #addContainer(container) {
523
- if (container.assetContainer && !container.visible && container.show) {
524
- container.assetContainer.addAllToScene();
525
- container.visible = true;
775
+ if (!container.assetContainer || container.visible || !container.show) {
776
+ return false;
526
777
  }
778
+
779
+ container.assetContainer.addAllToScene();
780
+ container.visible = true;
781
+ return true;
527
782
  }
528
783
 
529
784
  #removeContainer(container) {
530
- if (container.assetContainer && container.visible) {
531
- container.assetContainer.removeAllFromScene();
532
- container.visible = false;
785
+ if (!container.assetContainer || !container.visible) {
786
+ return false;
533
787
  }
788
+
789
+ container.assetContainer.removeAllFromScene();
790
+ container.visible = false;
791
+ return true;
534
792
  }
535
793
 
536
794
  #replaceContainer(container, newAssetContainer) {
537
- this.#removeContainer(container);
795
+ if (container.assetContainer) {
796
+ this.#removeContainer(container);
797
+ container.assetContainer.dispose();
798
+ container.assetContainer = null;
799
+ }
800
+ this.#scene.getEngine().releaseEffects();
538
801
  container.assetContainer = newAssetContainer;
539
- container.assetContainer.meshes.forEach((mesh) => {
540
- mesh.receiveShadows = true;
541
- this.#shadowGen.addShadowCaster(mesh, true);
542
- });
543
802
  this.#addContainer(container);
803
+ return true;
544
804
  }
545
805
 
546
806
  async #loadAssetContainer(container) {
@@ -556,10 +816,11 @@ class PrefViewer extends HTMLElement {
556
816
  await this.#initStorage(storage.db, storage.table);
557
817
  const object = await loadModel(storage.id, storage.table);
558
818
  source = object.data;
559
- if (object.timestamp === container.timestamp) {
819
+ if (object.timeStamp === container.timeStamp) {
820
+ container.changed = { pending: false, success: false };
560
821
  return false;
561
822
  } else {
562
- container.changed = { timestamp: object.timestamp, size: object.size };
823
+ container.changed = { pending: true, size: object.size, success: false, timeStamp: object.timeStamp };
563
824
  }
564
825
  }
565
826
 
@@ -574,29 +835,34 @@ class PrefViewer extends HTMLElement {
574
835
  file = new File([blob], `${container.name}${extension}`, {
575
836
  type: blob.type,
576
837
  });
577
- if (!container.changed) {
578
- if (container.timestamp === null && container.size === size) {
838
+ if (!container.changed.pending) {
839
+ if (container.timeStamp === null && container.size === size) {
840
+ container.changed = { pending: false, success: false };
579
841
  return false;
580
842
  } else {
581
- container.changed = { timestamp: null, size: size };
843
+ container.changed = { pending: true, size: size, success: false, timeStamp: null };
582
844
  }
583
845
  }
584
846
  } else {
585
847
  const extMatch = source.match(/\.(gltf|glb)(\?|#|$)/i);
586
848
  extension = extMatch ? `.${extMatch[1].toLowerCase()}` : ".gltf";
587
- const { fileSize, fileTimestamp } = await this.#getServerFileDataHeader(source);
588
- if (container.timestamp === fileTimestamp && container.size === fileSize) {
849
+ const [fileSize, fileTimeStamp] = await this.#getServerFileDataHeader(source);
850
+ if (container.size === fileSize && container.timeStamp === fileTimeStamp) {
851
+ container.changed = { pending: false, success: false };
589
852
  return false;
590
853
  } else {
591
- container.changed = { timestamp: fileTimestamp, size: fileSize };
854
+ container.changed = { pending: true, size: fileSize, success: false, timeStamp: fileTimeStamp };
592
855
  }
593
856
  }
594
857
 
858
+ // https://doc.babylonjs.com/typedoc/interfaces/BABYLON.LoadAssetContainerOptions
595
859
  let options = {
596
860
  pluginExtension: extension,
597
861
  pluginOptions: {
598
862
  gltf: {
863
+ compileMaterials: true,
599
864
  loadAllMaterials: true,
865
+ loadOnlyMaterials: container.name === "materials",
600
866
  preprocessUrlAsync: this.#transformUrl,
601
867
  },
602
868
  },
@@ -606,75 +872,126 @@ class PrefViewer extends HTMLElement {
606
872
  }
607
873
 
608
874
  async #loadContainers(loadModel = true, loadEnvironment = true, loadMaterials = true) {
875
+ this.#engine.stopRenderLoop(this.#renderLoop);
876
+
609
877
  const promiseArray = [];
610
878
  promiseArray.push(loadModel ? this.#loadAssetContainer(this.#data.containers.model) : false);
611
879
  promiseArray.push(loadEnvironment ? this.#loadAssetContainer(this.#data.containers.environment) : false);
612
880
  promiseArray.push(loadMaterials ? this.#loadAssetContainer(this.#data.containers.materials) : false);
613
881
 
614
882
  Promise.allSettled(promiseArray)
615
- .then(async (values) => {
883
+ .then((values) => {
616
884
  const modelContainer = values[0];
617
885
  const environmentContainer = values[1];
618
886
  const materialsContainer = values[2];
619
887
 
620
888
  if (modelContainer.status === "fulfilled" && modelContainer.value) {
889
+ modelContainer.value.lights = [];
621
890
  this.#replaceContainer(this.#data.containers.model, modelContainer.value);
622
- this.#storeChangedFlagsForContainer(this.#data.containers.model);
891
+ this.#storeChangedFlagsForContainer(this.#data.containers.model, true);
623
892
  } else {
624
- this.#data.containers.model.show ? this.#addContainer(this.#data.containers.model) : this.#removeContainer(this.#data.containers.model);
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);
625
897
  }
626
898
 
627
899
  if (environmentContainer.status === "fulfilled" && environmentContainer.value) {
628
900
  this.#replaceContainer(this.#data.containers.environment, environmentContainer.value);
629
- this.#storeChangedFlagsForContainer(this.#data.containers.environment);
901
+ this.#storeChangedFlagsForContainer(this.#data.containers.environment, true);
630
902
  } else {
631
- this.#data.containers.environment.show ? this.#addContainer(this.#data.containers.environment) : this.#removeContainer(this.#data.containers.environment);
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);
632
907
  }
633
908
 
634
909
  if (materialsContainer.status === "fulfilled" && materialsContainer.value) {
635
910
  this.#replaceContainer(this.#data.containers.materials, materialsContainer.value);
636
- this.#storeChangedFlagsForContainer(this.#data.containers.materials);
911
+ this.#storeChangedFlagsForContainer(this.#data.containers.materials, true);
912
+ } else {
913
+ this.#storeChangedFlagsForContainer(this.#data.containers.materials, false);
637
914
  }
638
915
 
639
916
  this.#setOptionsMaterials();
640
917
  this.#setOptionsCamera();
641
918
  this.#setVisibilityOfWallAndFloorInModel();
642
-
643
- this.#resetChangedFlags();
644
-
645
- this.dispatchEvent(
646
- new CustomEvent("model-loaded", {
647
- detail: { success: "" },
648
- bubbles: true,
649
- composed: true,
650
- })
651
- );
652
919
  })
653
920
  .catch((error) => {
921
+ this.loaded = true;
654
922
  console.error("PrefViewer: failed to load model", error);
655
923
  this.dispatchEvent(
656
- new CustomEvent("model-error", {
657
- detail: { error: error },
924
+ new CustomEvent("scene-error", {
658
925
  bubbles: true,
926
+ cancelable: false,
659
927
  composed: true,
928
+ detail: { error: error },
660
929
  })
661
930
  );
931
+ })
932
+ .finally(async () => {
933
+ this.#setMaxSimultaneousLights();
934
+ this.#initShadows();
935
+ await this.#setStatusLoaded();
662
936
  });
663
937
  }
664
938
 
665
- // Public Methods
666
- loadConfig(config) {
667
- config = typeof config === "string" ? JSON.parse(config) : config;
668
- if (!config) {
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) {
669
949
  return false;
670
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();
671
974
 
672
975
  // Containers
673
- this.#data.containers.model.storage = config.model?.storage || null;
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;
674
981
  this.#data.containers.model.show = config.model?.visible !== undefined ? config.model.visible : this.#data.containers.model.show;
675
- this.#data.containers.environment.storage = config.scene?.storage || null;
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;
676
988
  this.#data.containers.environment.show = config.scene?.visible !== undefined ? config.scene.visible : this.#data.containers.environment.show;
677
- this.#data.containers.materials.storage = config.materials?.storage || null;
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;
678
995
 
679
996
  // Options
680
997
  if (config.options) {
@@ -682,13 +999,50 @@ class PrefViewer extends HTMLElement {
682
999
  this.#checkMaterialsChanged(config.options);
683
1000
  }
684
1001
 
685
- this.#initialized && this.#loadContainers(true, true, true);
1002
+ this.#loadContainers(loadModel, loadEnvironment, loadMaterials);
686
1003
  }
687
1004
 
688
- setOptions(options) {
689
- if (!options) {
690
- return false;
691
- }
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
+
692
1046
  let someSetted = false;
693
1047
  if (this.#checkCameraChanged(options)) {
694
1048
  someSetted = someSetted || this.#setOptionsCamera();
@@ -696,19 +1050,27 @@ class PrefViewer extends HTMLElement {
696
1050
  if (this.#checkMaterialsChanged(options)) {
697
1051
  someSetted = someSetted || this.#setOptionsMaterials();
698
1052
  }
699
- this.#resetChangedFlags();
700
- debugger;
1053
+
1054
+ await this.#setStatusLoaded();
1055
+
701
1056
  return someSetted;
702
1057
  }
703
1058
 
1059
+ // Public Methods
1060
+ loadConfig(config) {
1061
+ config = typeof config === "string" ? JSON.parse(config) : config;
1062
+ if (!config) {
1063
+ return false;
1064
+ }
1065
+ this.#addTaskToQueue(config, "config");
1066
+ }
1067
+
704
1068
  loadModel(model) {
705
1069
  model = typeof model === "string" ? JSON.parse(model) : model;
706
1070
  if (!model) {
707
1071
  return false;
708
1072
  }
709
- this.#data.containers.model.storage = model.storage || null;
710
- this.#data.containers.model.show = model.visible !== undefined ? model.visible : this.#data.containers.model.show;
711
- this.#initialized && this.#loadContainers(true, false, false);
1073
+ this.#addTaskToQueue(model, "model");
712
1074
  }
713
1075
 
714
1076
  loadScene(scene) {
@@ -716,9 +1078,23 @@ class PrefViewer extends HTMLElement {
716
1078
  if (!scene) {
717
1079
  return false;
718
1080
  }
719
- this.#data.containers.environment.storage = scene.storage || null;
720
- this.#data.containers.environment.show = scene.visible !== undefined ? scene.visible : this.#data.containers.environment.show;
721
- this.#initialized && this.#loadContainers(false, true, false);
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");
722
1098
  }
723
1099
 
724
1100
  showModel() {