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

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 +100 -46
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.10.0-beta.0",
3
+ "version": "2.10.0-beta.2",
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
@@ -6,30 +6,40 @@
6
6
  * Overview
7
7
  * --------
8
8
  * `PrefViewer` is a self-contained Web Component built with Babylon.js that:
9
- * • Renders glTF or GLB models in a <canvas> inside its shadow DOM.
10
- * • Supports loading via remote URLs (`model` attribute) or Base64 data URIs (`model-data` attribute).
9
+ * • Renders the glTF or GLB model of the product ('model') and merges it with the glTF or GLB model of environment ('scene') into a <canvas> inside its shadow DOM.
10
+ * • Supports loading via remote URL (storage.url), Base64 data URI (storage.url), or from a Base64 stored entry in IndexedDB (storage.db, storage.table, storage.id).
11
+ * • The data for loading both models is provided via the 'config' (both model and scene), 'model' and 'scene' attributes. The 'config' attribute can carry more initial configurations than just the models.
12
+ * • Exposes methods for making changes to the scene that replicate what the attribute observables do: 'loadConfig', 'loadModel', 'loadScene'.
11
13
  * • Automatically handles scene creation (engine, camera, lighting) and resource cleanup.
12
- * • Emits `model-loaded` and `model-error` events for integration.
14
+ * • Emits 'model-loaded' and 'model-error' events for integration.
13
15
  *
14
16
  * Usage
15
17
  * -----
16
- * Load from a URL:
18
+ * Load model from IndexedDB:
17
19
  * ```html
18
20
  * <pref-viewer
19
- * model="https://example.com/models/myModel.glb"
20
- * style="width:800px; height:600px;">
21
+ * model='{ "storage": { "db": "PrefConfiguratorDB", "table": "gltfModels", "id": "1234-1234-1234-1234-1234" }, "visible": true" }'
22
+ * style="width:800px; height:600px;">
23
+ * </pref-viewer>
24
+ * ```
25
+ *
26
+ * Load scene a URL:
27
+ * ```html
28
+ * <pref-viewer
29
+ * scene='{ "storage": { "url" : "https://example.com/scenes/scene1.gltf" }, "visible": true" }'
30
+ * style="width:800px; height:600px;">
21
31
  * </pref-viewer>
22
32
  * ```
23
33
  *
24
- * Load from Base64 data:
34
+ * Load model from Base64 data:
25
35
  * ```html
26
36
  * <pref-viewer
27
- * model-data="data:application/gltf+json;base64,UEsDB..."
28
- * style="width:800px; height:600px;">
37
+ * model='{ "storage": { "url" : "data:model/gltf+json;base64,UEsDB..." }, "visible": true" }'
38
+ * style="width:800px; height:600px;">
29
39
  * </pref-viewer>
30
40
  * ```
31
41
  */
32
- import { Engine, Scene, ArcRotateCamera, Vector3, Color4, HemisphericLight, DirectionalLight, PointLight, ShadowGenerator, LoadAssetContainerAsync, Tools } from "@babylonjs/core";
42
+ import { Engine, Scene, ArcRotateCamera, Vector3, Color4, HemisphericLight, DirectionalLight, PointLight, ShadowGenerator, LoadAssetContainerAsync, Tools, WebXRSessionManager, WebXRDefaultExperience } from "@babylonjs/core";
33
43
  import "@babylonjs/loaders";
34
44
  import { USDZExportAsync, GLTF2Export } from "@babylonjs/serializers";
35
45
  import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression";
@@ -41,14 +51,14 @@ class PrefViewer extends HTMLElement {
41
51
 
42
52
  #model = {
43
53
  container: null,
44
- show: true,
54
+ show: true, // Show model by default
45
55
  storage: null,
46
56
  visible: false,
47
57
  };
48
58
 
49
59
  #environment = {
50
60
  container: null,
51
- show: true,
61
+ show: true, // Show environment by default
52
62
  storage: null,
53
63
  visible: false,
54
64
  };
@@ -63,6 +73,7 @@ class PrefViewer extends HTMLElement {
63
73
  #dirLight = null;
64
74
  #cameraLight = null;
65
75
  #shadowGen = null;
76
+ #XRExperience = null;
66
77
 
67
78
  constructor() {
68
79
  super();
@@ -89,33 +100,13 @@ class PrefViewer extends HTMLElement {
89
100
  let data = null;
90
101
  switch (name) {
91
102
  case "config":
92
- data = JSON.parse(value);
93
- if (!data) {
94
- return false;
95
- }
96
- this.#model.storage = data.model?.storage || null;
97
- this.#model.show = data.model?.visible || true;
98
- this.#environment.storage = data.scene?.storage || null;
99
- this.#environment.show = data.scene?.visible || true;
100
- this.#initialized && this.#loadContainers(true, true);
103
+ this.loadConfig(value);
101
104
  break;
102
105
  case "model":
103
- data = JSON.parse(value);
104
- if (!data) {
105
- return false;
106
- }
107
- this.#model.storage = data.model?.storage || null;
108
- this.#model.show = data.model?.visible || true;
109
- this.#initialized && this.#loadContainers(true, false);
106
+ this.loadModel(value);
110
107
  break;
111
108
  case "scene":
112
- data = JSON.parse(value);
113
- if (!data) {
114
- return false;
115
- }
116
- this.#environment.storage = data.scene?.storage || null;
117
- this.#environment.show = data.scene?.visible || true;
118
- this.#initialized && this.#loadContainers(false, true);
109
+ this.loadScene(value);
119
110
  break;
120
111
  case "show-model":
121
112
  data = value.toLowerCase?.() === "true";
@@ -157,7 +148,7 @@ class PrefViewer extends HTMLElement {
157
148
 
158
149
  disconnectedCallback() {
159
150
  this.#disposeEngine();
160
- window.removeEventListener("resize", this.#onWindowResize);
151
+ this.#canvasResizeObserver.disconnect();
161
152
  }
162
153
 
163
154
  // Web Component
@@ -181,26 +172,58 @@ class PrefViewer extends HTMLElement {
181
172
  wrapper.appendChild(this.#canvas);
182
173
  this.shadowRoot.append(wrapper);
183
174
  }
184
-
175
+
185
176
  // Bbylon.js
186
- #initializeBabylon() {
177
+ async #initializeBabylon() {
187
178
  this.#engine = new Engine(this.#canvas, true, { alpha: true });
188
179
  this.#scene = new Scene(this.#engine);
189
180
  this.#scene.clearColor = new Color4(1, 1, 1, 1);
190
181
  this.#createCamera();
191
182
  this.#createLights();
192
183
  this.#setupInteraction();
193
-
184
+
194
185
  this.#engine.runRenderLoop(() => this.#scene && this.#scene.render());
195
- window.addEventListener("resize", this.#onWindowResize);
186
+ this.#canvasResizeObserver.observe(this.#canvas);
187
+
188
+ await this.#createXRExperience();
196
189
  }
197
190
 
198
- #onWindowResize() {
199
- this.#engine && this.#engine.resize();
191
+ async #createXRExperience() {
192
+ if (this.#XRExperience) {
193
+ return true;
194
+ }
195
+
196
+ const sessionMode = "immersive-ar";
197
+ const sessionSupported = await WebXRSessionManager.IsSessionSupportedAsync(sessionMode);
198
+ if (!sessionSupported) {
199
+ console.info("PrefViewer: WebXR in mode AR is not supported");
200
+ return false;
201
+ }
202
+
203
+ const options = {
204
+ uiOptions: {
205
+ sessionMode: sessionMode,
206
+ renderTarget: "xrLayer",
207
+ referenceSpaceType: "local",
208
+ },
209
+ };
210
+
211
+ try {
212
+ this.#XRExperience = await WebXRDefaultExperience.CreateAsync(this.#scene, options);
213
+ } catch (error) {
214
+ console.warn("PrefViewer: failed to create WebXR experience", error);
215
+ this.#XRExperience = null;
216
+ }
200
217
  }
201
218
 
219
+ #canvasResizeObserver = new ResizeObserver(() => this.#engine && this.#engine.resize());
220
+
202
221
  #createCamera() {
203
- this.#camera = new ArcRotateCamera("camera", Math.PI / 2, Math.PI / 3, 10, Vector3.Zero(), this.#scene);
222
+ this.#camera = new ArcRotateCamera("camera", 3 * Math.PI / 2, Math.PI * 0.47, 10, Vector3.Zero(), this.#scene);
223
+ this.#camera.upperBetaLimit = Math.PI * 0.48;
224
+ this.#camera.lowerBetaLimit = Math.PI * 0.25;
225
+ this.#camera.lowerRadiusLimit = 5;
226
+ this.#camera.upperRadiusLimit = 20;
204
227
  this.#camera.attachControl(this.#canvas, true);
205
228
  }
206
229
 
@@ -230,8 +253,8 @@ class PrefViewer extends HTMLElement {
230
253
  this.#canvas.addEventListener("wheel", (event) => {
231
254
  if (!this.#scene || !this.#camera) return;
232
255
  const pick = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
233
- this.#camera.target = pick.hit ? pick.pickedPoint.clone() : this.#camera.target;
234
- this.#camera.inertialRadiusOffset += event.deltaY * this.#camera.wheelPrecision * 0.01;
256
+ //this.#camera.target = pick.hit ? pick.pickedPoint.clone() : this.#camera.target;
257
+ this.#camera.inertialRadiusOffset -= event.deltaY * this.#camera.wheelPrecision * 0.001;
235
258
  event.preventDefault();
236
259
  });
237
260
  }
@@ -377,7 +400,6 @@ class PrefViewer extends HTMLElement {
377
400
  this.#replaceContainer(this.#environment, environmentContainer.value);
378
401
  }
379
402
 
380
- this.#scene.createDefaultCamera(true, true, true);
381
403
  this.#setVisibilityOfWallAndFloorInModel();
382
404
 
383
405
  this.dispatchEvent(
@@ -401,6 +423,38 @@ class PrefViewer extends HTMLElement {
401
423
  }
402
424
 
403
425
  // Public Methods
426
+ loadConfig(config) {
427
+ config = typeof config === "string" ? JSON.parse(config) : config;
428
+ if (!config) {
429
+ return false;
430
+ }
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);
436
+ }
437
+
438
+ loadModel(model) {
439
+ model = typeof model === "string" ? JSON.parse(model) : model;
440
+ if (!model) {
441
+ return false;
442
+ }
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);
446
+ }
447
+
448
+ loadScene(scene) {
449
+ scene = typeof scene === "string" ? JSON.parse(scene) : scene;
450
+ if (!scene) {
451
+ return false;
452
+ }
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);
456
+ }
457
+
404
458
  showModel() {
405
459
  this.#model.show = true;
406
460
  this.#addContainer(this.#model);