@preference-sl/pref-viewer 2.0.0 → 2.1.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "license": "MIT",
package/src/index.js CHANGED
@@ -1,97 +1,253 @@
1
+ /**
2
+ * =============================================================================
3
+ * PrefViewer Web Component (JavaScript)
4
+ * =============================================================================
5
+ *
6
+ * Overview
7
+ * --------
8
+ * `PrefViewer` is a self-contained Web Component built with Babylon.js that:
9
+ * • Inserts a <canvas> into its shadow DOM to render a glTF model.
10
+ * • Creates and manages a Babylon Engine, Scene, ArcRotateCamera, and basic lighting.
11
+ * • Listens for a `model` attribute to load different glTF files (defaults to "./models/patata.gltf").
12
+ * • Automatically disposes previous meshes when switching models.
13
+ * • Dispatches “model-loaded” and “model-error” CustomEvents so host pages can react.
14
+ *
15
+ * Usage
16
+ * -----
17
+ * 1. **Import the script (module)**
18
+ * <script type="module" src="path/to/pref-viewer.js"></script>
19
+ *
20
+ * 2. **Place the custom element in your HTML**
21
+ * <pref-viewer
22
+ * model="https://example.com/models/myModel.gltf"
23
+ * style="width:800px; height:600px;">
24
+ * </pref-viewer>
25
+ *
26
+ * 3. **Listen for loading events (optional)**
27
+ * const viewer = document.querySelector("pref-viewer");
28
+ * viewer.addEventListener("model-loaded", (evt) => {
29
+ * console.log("Loaded meshes:", evt.detail.meshes);
30
+ * });
31
+ * viewer.addEventListener("model-error", (evt) => {
32
+ * console.error("Failed to load model:", evt.detail.error);
33
+ * });
34
+ *
35
+ * 4. **Change models at runtime**
36
+ * viewer.setAttribute("model", "https://example.com/models/anotherModel.glb");
37
+ *
38
+ * -----------------------------------------------------------------------------
39
+ * Implementation code below with added console logs for debugging
40
+ * -----------------------------------------------------------------------------
41
+ */
42
+
1
43
  import {
2
44
  Engine,
3
45
  Scene,
4
46
  ArcRotateCamera,
5
47
  Vector3,
6
48
  SceneLoader,
7
- Color4
49
+ Color4,
50
+ HemisphericLight,
51
+ DirectionalLight
8
52
  } from "@babylonjs/core";
9
53
  import "@babylonjs/loaders";
10
54
 
11
55
  class PrefViewer extends HTMLElement {
12
- canvas;
13
- engine;
14
- scene;
15
- modelUrl = null;
16
-
17
56
  constructor() {
18
57
  super();
58
+ console.log("PrefViewer: constructor");
19
59
  this.attachShadow({ mode: "open" });
60
+ this._createCanvas();
61
+ this._wrapCanvas();
20
62
 
21
- this.canvas = document.createElement("canvas");
22
- Object.assign(this.canvas.style, {
23
- width: "100%",
24
- height: "100%",
25
- display: "block"
26
- });
27
-
28
- const wrapper = document.createElement("div");
29
- Object.assign(wrapper.style, {
30
- width: "100%",
31
- height: "100%",
32
- position: "relative"
33
- });
34
- wrapper.appendChild(this.canvas);
35
- this.shadowRoot.append(wrapper);
63
+ // Initialize properties
64
+ this.engine = null;
65
+ this.scene = null;
66
+ this.camera = null;
67
+ this.hemiLight = null;
68
+ this.dirLight = null;
69
+ this.modelUrl = null;
70
+ this._onWindowResize = null;
36
71
  }
37
72
 
38
73
  static get observedAttributes() {
39
74
  return ["model"];
40
75
  }
41
76
 
42
- attributeChangedCallback(name, _old, value) {
43
- if (name === "model" && value) {
44
- this.modelUrl = value;
45
- this.loadModel();
77
+ attributeChangedCallback(name, _oldValue, newValue) {
78
+ console.log(`PrefViewer: attributeChangedCallback - ${name} changed to ${newValue}`);
79
+ if (name === "model" && newValue) {
80
+ this.modelUrl = newValue;
81
+ console.log(`PrefViewer: modelUrl set to ${this.modelUrl}`);
82
+ this._reloadModel();
46
83
  }
47
84
  }
48
85
 
49
86
  connectedCallback() {
87
+ console.log("PrefViewer: connectedCallback");
88
+ // If no `model` attribute is present, use default bundled GLTF
50
89
  if (!this.hasAttribute("model")) {
51
90
  this.modelUrl = new URL("./models/patata.gltf", import.meta.url).href;
91
+ console.log(`PrefViewer: no model attribute, defaulting to ${this.modelUrl}`);
92
+ } else {
93
+ this.modelUrl = this.getAttribute("model");
94
+ console.log(`PrefViewer: model attribute present, using ${this.modelUrl}`);
52
95
  }
53
- this.initBabylon();
96
+ this._initializeBabylon();
54
97
  }
55
98
 
56
99
  disconnectedCallback() {
57
- this.engine?.dispose();
58
- window.removeEventListener("resize", this._resizeHandler);
100
+ console.log("PrefViewer: disconnectedCallback - disposing engine");
101
+ this._disposeEngine();
102
+ window.removeEventListener("resize", this._onWindowResize);
103
+ }
104
+
105
+ // ====== Private setup methods ======
106
+ _createCanvas() {
107
+ console.log("PrefViewer: _createCanvas");
108
+ this.canvas = document.createElement("canvas");
109
+ Object.assign(this.canvas.style, {
110
+ width: "100%",
111
+ height: "100%",
112
+ display: "block"
113
+ });
114
+ }
115
+
116
+ _wrapCanvas() {
117
+ console.log("PrefViewer: _wrapCanvas");
118
+ const wrapper = document.createElement("div");
119
+ Object.assign(wrapper.style, {
120
+ width: "100%",
121
+ height: "100%",
122
+ position: "relative"
123
+ });
124
+ wrapper.appendChild(this.canvas);
125
+ this.shadowRoot.append(wrapper);
59
126
  }
60
127
 
61
- initBabylon() {
128
+ _initializeBabylon() {
129
+ console.log("PrefViewer: _initializeBabylon - creating engine and scene");
130
+ // 1) Create engine and scene
62
131
  this.engine = new Engine(this.canvas, true, { alpha: true });
63
132
  this.scene = new Scene(this.engine);
64
133
  this.scene.clearColor = new Color4(1, 1, 1, 1);
65
134
 
66
- const camera = new ArcRotateCamera(
67
- "cam",
135
+ // 2) Hook into Babylon’s GLTF loader so "https://..." URIs aren't prefixed with blob:
136
+ console.log("PrefViewer: Adding preprocessUrl hook");
137
+ SceneLoader.OnPluginActivatedObservable.add((plugin) => {
138
+ console.log(`PrefViewer: Plugin activated - ${plugin.name}`);
139
+ if (plugin.name === "gltf" || plugin.name === "gltf2") {
140
+ plugin.preprocessUrl = (url) => {
141
+ // Normalize backslashes to forward slashes
142
+ const fixed = url.replace(/\\/g, "/");
143
+ console.log(`PrefViewer: preprocessUrl received "${url}", normalized to "${fixed}"`);
144
+ // If it starts with http:// or https://, return as-is:
145
+ if (/^https?:\/\//i.test(fixed)) {
146
+ console.log(`PrefViewer: preprocessUrl returning absolute URL "${fixed}"`);
147
+ return fixed;
148
+ }
149
+ // Otherwise, leave it (Babylon will treat it relative to the blob)
150
+ console.log(`PrefViewer: preprocessUrl returning relative URL "${fixed}"`);
151
+ return fixed;
152
+ };
153
+ }
154
+ });
155
+
156
+ // 3) Create camera and lights
157
+ console.log("PrefViewer: _createCamera and _createLights");
158
+ this._createCamera();
159
+ this._createLights();
160
+
161
+ // 4) Hook up input/event handlers
162
+ console.log("PrefViewer: _setupEventListeners");
163
+ this._setupEventListeners();
164
+
165
+ // 5) Start render loop
166
+ console.log("PrefViewer: Starting render loop");
167
+ this.engine.runRenderLoop(() => {
168
+ if (this.scene) {
169
+ this.scene.render();
170
+ }
171
+ });
172
+ this._onWindowResize = () => {
173
+ console.log("PrefViewer: Window resized - calling engine.resize()");
174
+ this.engine.resize();
175
+ };
176
+ window.addEventListener("resize", this._onWindowResize);
177
+
178
+ // 6) Load the initial model (if modelUrl is already set)
179
+ console.log("PrefViewer: _initializeBabylon calling _reloadModel");
180
+ this._reloadModel();
181
+ }
182
+
183
+ _createCamera() {
184
+ console.log("PrefViewer: _createCamera");
185
+ // ArcRotateCamera that orbits around origin
186
+ this.camera = new ArcRotateCamera(
187
+ "camera",
68
188
  Math.PI / 2,
69
189
  Math.PI / 3,
70
190
  10,
71
191
  Vector3.Zero(),
72
192
  this.scene
73
193
  );
74
- camera.attachControl(this.canvas, true);
194
+ this.camera.attachControl(this.canvas, true);
195
+ }
196
+
197
+ _createLights() {
198
+ console.log("PrefViewer: _createLights");
199
+ // Simple hemispheric + directional as a starting point
200
+ this.hemiLight = new HemisphericLight(
201
+ "hemiLight",
202
+ new Vector3(0, 1, 0),
203
+ this.scene
204
+ );
205
+ this.hemiLight.intensity = 0.6;
75
206
 
207
+ this.dirLight = new DirectionalLight(
208
+ "dirLight",
209
+ new Vector3(-0.5, -1, -0.5),
210
+ this.scene
211
+ );
212
+ this.dirLight.position = new Vector3(0, 5, 0);
213
+ this.dirLight.intensity = 0.8;
214
+ }
215
+
216
+ _setupEventListeners() {
217
+ console.log("PrefViewer: _setupEventListeners");
218
+ // Zoom toward point-of-interest on wheel scroll
76
219
  this.canvas.addEventListener("wheel", (evt) => {
77
- const pick = this.scene.pick(this.scene.pointerX, this.scene.pointerY);
78
- const pivot = pick.hit ? pick.pickedPoint.clone() : camera.target.clone();
79
- camera.target = pivot;
80
- camera.inertialRadiusOffset += evt.deltaY * camera.wheelPrecision * 0.01;
220
+ if (!this.scene || !this.camera) return;
221
+
222
+ const pickResult = this.scene.pick(
223
+ this.scene.pointerX,
224
+ this.scene.pointerY
225
+ );
226
+ const pivotPoint = pickResult.hit
227
+ ? pickResult.pickedPoint.clone()
228
+ : this.camera.target.clone();
229
+
230
+ this.camera.target = pivotPoint;
231
+ this.camera.inertialRadiusOffset +=
232
+ evt.deltaY * this.camera.wheelPrecision * 0.01;
81
233
  evt.preventDefault();
82
234
  });
235
+ }
83
236
 
84
- this.engine.runRenderLoop(() => this.scene.render());
85
- this._resizeHandler = () => this.engine.resize();
86
- window.addEventListener("resize", this._resizeHandler);
237
+ // ====== Model loading / management ======
238
+ async _reloadModel() {
239
+ console.log(`PrefViewer: _reloadModel - loading ${this.modelUrl}`);
240
+ if (!this.scene || !this.modelUrl) {
241
+ console.warn("PrefViewer: _reloadModel aborted (scene or modelUrl missing)");
242
+ return;
243
+ }
87
244
 
88
- this.loadModel();
89
- }
245
+ // Dispose previous meshes so we don’t accumulate them
246
+ this._disposePreviousMeshes();
90
247
 
91
- async loadModel() {
92
- if (!this.scene || !this.modelUrl) return;
93
248
  try {
94
- await SceneLoader.ImportMeshAsync(
249
+ console.log(`PrefViewer: ImportMeshAsync("${this.modelUrl}")`);
250
+ const result = await SceneLoader.ImportMeshAsync(
95
251
  null,
96
252
  "",
97
253
  this.modelUrl,
@@ -99,11 +255,56 @@ class PrefViewer extends HTMLElement {
99
255
  undefined,
100
256
  ".gltf"
101
257
  );
258
+ console.log("PrefViewer: Model loaded, creating default camera/light if needed");
102
259
  this.scene.createDefaultCameraOrLight(true, true, true);
260
+
261
+ console.log("PrefViewer: Dispatching model-loaded event");
262
+ this.dispatchEvent(
263
+ new CustomEvent("model-loaded", {
264
+ detail: {
265
+ meshes: result.meshes,
266
+ particleSystems: result.particleSystems
267
+ },
268
+ bubbles: true,
269
+ composed: true
270
+ })
271
+ );
103
272
  } catch (err) {
104
- console.error("Error loading model:", err);
273
+ console.error("PrefViewer: Error loading model:", err);
274
+ console.log("PrefViewer: Dispatching model-error event");
275
+ this.dispatchEvent(
276
+ new CustomEvent("model-error", {
277
+ detail: { error: err },
278
+ bubbles: true,
279
+ composed: true
280
+ })
281
+ );
282
+ }
283
+ }
284
+
285
+ _disposePreviousMeshes() {
286
+ console.log("PrefViewer: _disposePreviousMeshes");
287
+ if (!this.scene) return;
288
+ this.scene.meshes.slice().forEach((mesh) => {
289
+ if (mesh.getClassName() === "Mesh") {
290
+ console.log(`PrefViewer: Disposing mesh ${mesh.name}`);
291
+ mesh.dispose();
292
+ }
293
+ });
294
+ }
295
+
296
+ // ====== Cleanup ======
297
+ _disposeEngine() {
298
+ console.log("PrefViewer: _disposeEngine");
299
+ if (this.engine) {
300
+ this.engine.dispose();
301
+ this.engine = null;
302
+ this.scene = null;
303
+ this.camera = null;
304
+ this.hemiLight = null;
305
+ this.dirLight = null;
105
306
  }
106
307
  }
107
308
  }
108
309
 
109
- customElements.define("pref-viewer", PrefViewer);
310
+ customElements.define("pref-viewer", PrefViewer);
@@ -0,0 +1,240 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>PrefViewer Test (Global Babylon)</title>
6
+ <style>
7
+ html, body {
8
+ margin: 0;
9
+ padding: 0;
10
+ width: 100%;
11
+ height: 100%;
12
+ overflow: hidden;
13
+ }
14
+ #viewer-container {
15
+ width: 100%;
16
+ height: 100%;
17
+ display: flex;
18
+ align-items: center;
19
+ justify-content: center;
20
+ background: #f0f0f0;
21
+ }
22
+ </style>
23
+
24
+ <!-- 1) Load Babylon Core (Global) -->
25
+ <script src="https://cdn.babylonjs.com/babylon.js"></script>
26
+ <!-- 2) Load Babylon glTF loader so SceneLoader.ImportMeshAsync can parse .gltf -->
27
+ <script src="https://cdn.babylonjs.com/loaders/babylonjs.loaders.min.js"></script>
28
+ </head>
29
+ <body>
30
+ <div id="viewer-container">
31
+ <!-- Omit "model" attribute: PrefViewer will default to "./models/patata.gltf" -->
32
+ <pref-viewer style="width: 800px; height: 600px; border: 1px solid #ccc;"></pref-viewer>
33
+ </div>
34
+
35
+ <!-- 3) Define PrefViewer using the global `BABYLON` object -->
36
+ <script>
37
+ (function () {
38
+ const { Engine, Scene, ArcRotateCamera, Vector3, SceneLoader, Color4, HemisphericLight, DirectionalLight } = BABYLON;
39
+
40
+ class PrefViewer extends HTMLElement {
41
+ constructor() {
42
+ super();
43
+ this.attachShadow({ mode: "open" });
44
+ this.canvas = document.createElement("canvas");
45
+ Object.assign(this.canvas.style, {
46
+ width: "100%",
47
+ height: "100%",
48
+ display: "block",
49
+ });
50
+
51
+ const wrapper = document.createElement("div");
52
+ Object.assign(wrapper.style, {
53
+ width: "100%",
54
+ height: "100%",
55
+ position: "relative",
56
+ });
57
+ wrapper.appendChild(this.canvas);
58
+ this.shadowRoot.append(wrapper);
59
+
60
+ this.engine = null;
61
+ this.scene = null;
62
+ this.camera = null;
63
+ this.hemiLight = null;
64
+ this.dirLight = null;
65
+ this.modelUrl = null;
66
+ this._onWindowResize = null;
67
+ }
68
+
69
+ static get observedAttributes() {
70
+ return ["model"];
71
+ }
72
+
73
+ attributeChangedCallback(name, _oldValue, newValue) {
74
+ if (name === "model" && newValue) {
75
+ this.modelUrl = newValue;
76
+ this._reloadModel();
77
+ }
78
+ }
79
+
80
+ connectedCallback() {
81
+ // If no `model` attribute is provided, default to "models/patata.gltf"
82
+ if (!this.hasAttribute("model")) {
83
+ // Note: NO import.meta.url here—just use a relative path.
84
+ this.modelUrl = "models/patata.gltf";
85
+ }
86
+ this._initializeBabylon();
87
+ }
88
+
89
+ disconnectedCallback() {
90
+ this._disposeEngine();
91
+ if (this._onWindowResize) {
92
+ window.removeEventListener("resize", this._onWindowResize);
93
+ this._onWindowResize = null;
94
+ }
95
+ }
96
+
97
+ // —— Private setup methods ——
98
+
99
+ _initializeBabylon() {
100
+ // 1) Create engine & scene
101
+ this.engine = new Engine(this.canvas, true, { preserveDrawingBuffer: true, stencil: true, disableWebGL2Support: false });
102
+ this.scene = new Scene(this.engine);
103
+ this.scene.clearColor = new Color4(1, 1, 1, 1);
104
+
105
+ // 2) Create camera & lights
106
+ this._createCamera();
107
+ this._createLights();
108
+
109
+ // 3) Hook up events
110
+ this._setupEventListeners();
111
+
112
+ // 4) Start render loop
113
+ this.engine.runRenderLoop(() => {
114
+ if (this.scene) {
115
+ this.scene.render();
116
+ }
117
+ });
118
+ this._onWindowResize = () => this.engine.resize();
119
+ window.addEventListener("resize", this._onWindowResize);
120
+
121
+ // 5) Load the initial model
122
+ this._reloadModel();
123
+ }
124
+
125
+ _createCamera() {
126
+ // ArcRotateCamera around (0,0,0)
127
+ const camera = new ArcRotateCamera(
128
+ "camera",
129
+ Math.PI / 2,
130
+ Math.PI / 3,
131
+ 10,
132
+ Vector3.Zero(),
133
+ this.scene
134
+ );
135
+ camera.attachControl(this.canvas, true);
136
+ this.camera = camera;
137
+ }
138
+
139
+ _createLights() {
140
+ // Hemispheric light (fills in shadows)
141
+ const hemiLight = new HemisphericLight("hemiLight", new Vector3(0, 1, 0), this.scene);
142
+ hemiLight.intensity = 0.8;
143
+
144
+ // Directional key light
145
+ const dirLight = new DirectionalLight("dirLight", new Vector3(-0.5, -1, -0.5), this.scene);
146
+ dirLight.position = new Vector3(0, 5, 0);
147
+ dirLight.intensity = 0.8;
148
+
149
+ this.hemiLight = hemiLight;
150
+ this.dirLight = dirLight;
151
+ }
152
+
153
+ _setupEventListeners() {
154
+ // Zoom towards the pointer's picked point on wheel scroll:
155
+ this.canvas.addEventListener("wheel", (evt) => {
156
+ if (!this.scene || !this.camera) return;
157
+ const pickResult = this.scene.pick(this.scene.pointerX, this.scene.pointerY);
158
+ const pivotPoint = pickResult.hit
159
+ ? pickResult.pickedPoint.clone()
160
+ : this.camera.target.clone();
161
+ this.camera.target = pivotPoint;
162
+ this.camera.inertialRadiusOffset += evt.deltaY * this.camera.wheelPrecision * 0.01;
163
+ evt.preventDefault();
164
+ });
165
+ }
166
+
167
+ // —— Model loading / management ——
168
+
169
+ async _reloadModel() {
170
+ if (!this.scene || !this.modelUrl) return;
171
+
172
+ // Dispose previous meshes (to avoid stacking them up)
173
+ this._disposePreviousMeshes();
174
+
175
+ try {
176
+ const result = await SceneLoader.ImportMeshAsync(
177
+ null,
178
+ "",
179
+ this.modelUrl,
180
+ this.scene,
181
+ undefined,
182
+ ".gltf"
183
+ );
184
+
185
+ // Ensure there's at least one camera / light if glTF had none
186
+ this.scene.createDefaultCameraOrLight(true, true, true);
187
+
188
+ // Turn off back-face culling so you can see both sides
189
+ result.meshes.forEach((mesh) => {
190
+ if (mesh.material) {
191
+ mesh.material.backFaceCulling = false;
192
+ }
193
+ });
194
+
195
+ this.dispatchEvent(
196
+ new CustomEvent("model-loaded", {
197
+ detail: { meshes: result.meshes, particleSystems: result.particleSystems },
198
+ bubbles: true,
199
+ composed: true,
200
+ })
201
+ );
202
+ } catch (err) {
203
+ console.error("Error loading model:", err);
204
+ this.dispatchEvent(
205
+ new CustomEvent("model-error", {
206
+ detail: { error: err },
207
+ bubbles: true,
208
+ composed: true,
209
+ })
210
+ );
211
+ }
212
+ }
213
+
214
+ _disposePreviousMeshes() {
215
+ if (!this.scene) return;
216
+ this.scene.meshes.slice().forEach((mesh) => {
217
+ if (mesh.getClassName && mesh.getClassName() === "Mesh") {
218
+ mesh.dispose();
219
+ }
220
+ });
221
+ }
222
+
223
+ // —— Cleanup ——
224
+
225
+ _disposeEngine() {
226
+ if (this.engine) {
227
+ this.engine.dispose();
228
+ this.engine = null;
229
+ this.scene = null;
230
+ this.camera = null;
231
+ }
232
+ }
233
+ }
234
+
235
+ customElements.define("pref-viewer", PrefViewer);
236
+ })();
237
+ </script>
238
+
239
+ </body>
240
+ </html>
package/src/test.html ADDED
@@ -0,0 +1,62 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>PrefViewer Test (Reuse Index.js)</title>
6
+ <style>
7
+ html, body {
8
+ margin: 0;
9
+ padding: 0;
10
+ width: 100%;
11
+ height: 100%;
12
+ overflow: hidden;
13
+ }
14
+ #viewer-container {
15
+ width: 100%;
16
+ height: 100%;
17
+ display: flex;
18
+ align-items: center;
19
+ justify-content: center;
20
+ background: #f0f0f0;
21
+ }
22
+ </style>
23
+
24
+ <!--
25
+ IMPORT MAP
26
+ ──────────
27
+ This tells the browser where to fetch "@babylonjs/core" and "@babylonjs/loaders".
28
+ We use jsDelivr's "?module" query so that it automatically serves the ESM build
29
+ corresponding to each package's "module" field in package.json.
30
+
31
+ Make sure the version (5.0.1 here) matches what you have in package.json.
32
+ If you have "@babylonjs/core": "^5.0.2" in your package.json, change both URLs
33
+ to "@5.0.2?module", etc.
34
+
35
+ This import-map must appear *before* you load index.js as a module.
36
+ -->
37
+ <script type="importmap">
38
+ {
39
+ "imports": {
40
+ "@babylonjs/core": "https://cdn.jsdelivr.net/npm/@babylonjs/core@5.0.1?module",
41
+ "@babylonjs/loaders": "https://cdn.jsdelivr.net/npm/@babylonjs/loaders@5.0.1?module"
42
+ }
43
+ }
44
+ </script>
45
+ </head>
46
+ <body>
47
+ <div id="viewer-container">
48
+ <!--
49
+ By omitting the `model` attribute, PrefViewer’s connectedCallback()
50
+ will fall back to "./models/patata.gltf" (relative to index.js).
51
+ -->
52
+ <pref-viewer style="width: 800px; height: 600px; border: 1px solid #ccc;"></pref-viewer>
53
+ </div>
54
+
55
+ <!--
56
+ Load your existing ES-module index.js. Because of the import-map above,
57
+ any `import { … } from "@babylonjs/core"` inside index.js will resolve to
58
+ "https://cdn.jsdelivr.net/npm/@babylonjs/core@5.0.1?module", etc.
59
+ -->
60
+ <script type="module" src="./index.js"></script>
61
+ </body>
62
+ </html>