@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.
- package/package.json +1 -1
- package/src/index.js +100 -46
package/package.json
CHANGED
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
|
|
10
|
-
* • Supports loading via remote
|
|
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
|
|
14
|
+
* • Emits 'model-loaded' and 'model-error' events for integration.
|
|
13
15
|
*
|
|
14
16
|
* Usage
|
|
15
17
|
* -----
|
|
16
|
-
* Load from
|
|
18
|
+
* Load model from IndexedDB:
|
|
17
19
|
* ```html
|
|
18
20
|
* <pref-viewer
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
-
*
|
|
28
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
186
|
+
this.#canvasResizeObserver.observe(this.#canvas);
|
|
187
|
+
|
|
188
|
+
await this.#createXRExperience();
|
|
196
189
|
}
|
|
197
190
|
|
|
198
|
-
#
|
|
199
|
-
|
|
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
|
|
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
|
|
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);
|