sceneview-web 1.2.0 → 1.4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sceneview-web",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "One-liner 3D for the web — SceneView.modelViewer('canvas', 'model.glb'). Powered by Filament.js WASM.",
5
5
  "main": "sceneview.js",
6
6
  "author": "SceneView Tools",
package/sceneview.js CHANGED
@@ -4,45 +4,30 @@
4
4
  * One line to render a 3D model:
5
5
  * SceneView.modelViewer("canvas", "model.glb")
6
6
  *
7
- * No prerequisites — sceneview.js loads Filament.js CDN automatically.
8
- * Just include one script:
9
- * <script src="js/sceneview.js"></script>
10
- *
11
- * Powered by Filament.js (Google's PBR renderer, WASM).
7
+ * Powered by Filament.js v1.70.1 (Google's PBR renderer, WASM).
12
8
  * https://sceneview.github.io
13
9
  *
14
- * @version 1.1.0
10
+ * @version 1.4.0
15
11
  * @license MIT
16
12
  */
17
13
  (function(global) {
18
14
  'use strict';
19
15
 
20
- var FILAMENT_CDN = 'https://cdn.jsdelivr.net/npm/filament@1.52.3/filament.js';
16
+ // Filament.js is loaded via <script> tag in HTML (js/filament/filament.js)
17
+ // This avoids dynamic script injection issues with WASM resolution.
21
18
 
22
19
  /**
23
- * Load Filament.js CDN dynamically if not already present.
24
- * Returns a Promise that resolves when the Filament global is available.
20
+ * Wait for Filament to be available (loaded by the script tag).
25
21
  */
26
22
  function _ensureFilament() {
27
23
  return new Promise(function(resolve, reject) {
28
- // Already loaded
29
- if (typeof Filament !== 'undefined') {
30
- resolve();
31
- return;
32
- }
33
- // Check if script tag already exists but hasn't finished loading
34
- var existing = document.querySelector('script[src*="filament"]');
35
- if (existing) {
36
- existing.addEventListener('load', function() { resolve(); });
37
- existing.addEventListener('error', function() { reject(new Error('SceneView: Failed to load Filament.js from CDN')); });
38
- return;
39
- }
40
- // Inject script tag
41
- var script = document.createElement('script');
42
- script.src = FILAMENT_CDN;
43
- script.onload = function() { resolve(); };
44
- script.onerror = function() { reject(new Error('SceneView: Failed to load Filament.js from CDN (' + FILAMENT_CDN + ')')); };
45
- document.head.appendChild(script);
24
+ if (typeof Filament !== 'undefined') { resolve(); return; }
25
+ // Poll briefly in case the script tag hasn't finished loading
26
+ var attempts = 0;
27
+ var check = setInterval(function() {
28
+ if (typeof Filament !== 'undefined') { clearInterval(check); resolve(); }
29
+ if (++attempts > 100) { clearInterval(check); reject(new Error('SceneView: Filament.js not loaded')); }
30
+ }, 50);
46
31
  });
47
32
  }
48
33
 
@@ -61,7 +46,7 @@
61
46
  this._cameraEntity = cameraEntity;
62
47
  this._loader = loader;
63
48
  this._asset = null;
64
- this._angle = 0;
49
+ this._angle = 0.785; // Start at ~45° like model-viewer
65
50
  this._autoRotate = true;
66
51
  this._orbitRadius = 3.5;
67
52
  this._orbitHeight = 0.8;
@@ -69,6 +54,12 @@
69
54
  this._running = true;
70
55
  this._isDragging = false;
71
56
  this._lastMouse = { x: 0, y: 0 };
57
+ // Inertia for smooth orbit deceleration
58
+ this._velocityAngle = 0;
59
+ this._velocityHeight = 0;
60
+ this._dampingFactor = 0.95;
61
+ this._wantsAutoRotate = true; // Remember initial preference for resume after drag
62
+ this._autoRotateTimer = null;
72
63
  this._setupControls();
73
64
  this._setupResizeObserver();
74
65
  this._startRenderLoop();
@@ -78,34 +69,29 @@
78
69
  loadModel(url) {
79
70
  var self = this;
80
71
  return new Promise(function(resolve, reject) {
81
- // If already fetched, show immediately
82
- if (Filament.assets && Filament.assets[url]) {
83
- try {
84
- self._showModel(url);
85
- resolve(self);
86
- } catch (e) {
87
- reject(e);
88
- }
89
- return;
90
- }
91
- // Fetch via Filament.init with asset — this always fires the callback
92
- // because it needs to fetch the asset even if WASM is already loaded
93
- Filament.init([url], function() {
94
- try {
95
- self._showModel(url);
96
- resolve(self);
97
- } catch (e) {
98
- reject(e);
99
- }
100
- });
72
+ fetch(url)
73
+ .then(function(resp) { return resp.arrayBuffer(); })
74
+ .then(function(buffer) {
75
+ Filament.assets = Filament.assets || {};
76
+ Filament.assets[url] = new Uint8Array(buffer);
77
+ try {
78
+ self._showModel(url);
79
+ resolve(self);
80
+ } catch (e) {
81
+ reject(e);
82
+ }
83
+ })
84
+ .catch(reject);
101
85
  });
102
86
  }
103
87
 
104
88
  _showModel(url) {
105
89
  // Remove previous model
106
90
  if (this._asset) {
107
- this._asset.getRenderableEntities().forEach(function(e) { this._scene.remove(e); }.bind(this));
108
- this._scene.remove(this._asset.getRoot());
91
+ try {
92
+ this._asset.getRenderableEntities().forEach(function(e) { this._scene.remove(e); }.bind(this));
93
+ this._scene.remove(this._asset.getRoot());
94
+ } catch (e) { /* ignore cleanup errors */ }
109
95
  this._asset = null;
110
96
  }
111
97
 
@@ -120,7 +106,7 @@
120
106
  this._scene.addEntities(asset.getRenderableEntities());
121
107
  this._asset = asset;
122
108
 
123
- // Auto-frame: try getBoundingBox, fall back to defaults
109
+ // Auto-frame the model
124
110
  try {
125
111
  var bbox = asset.getBoundingBox();
126
112
  var cx = (bbox.min[0] + bbox.max[0]) / 2;
@@ -132,95 +118,154 @@
132
118
  var maxDim = Math.max(sx, sy, sz);
133
119
  if (maxDim > 0) {
134
120
  this._orbitTarget = [cx, cy, cz];
135
- this._orbitRadius = maxDim * 2.5;
121
+ // Tighter framing than before (1.8x instead of 2.5x)
122
+ this._orbitRadius = maxDim * 1.8;
136
123
  this._orbitHeight = cy;
137
124
  }
138
- } catch (e) {
139
- // getBoundingBox not available on all assets, use defaults
140
- }
125
+ } catch (e) { /* use defaults */ }
141
126
  }
142
127
 
143
- /** Enable/disable auto-rotation */
144
- setAutoRotate(enabled) {
145
- this._autoRotate = enabled;
128
+ setAutoRotate(enabled) { this._autoRotate = enabled; this._wantsAutoRotate = enabled; return this; }
129
+ setCameraDistance(d) { this._orbitRadius = d; return this; }
130
+
131
+ setBackgroundColor(r, g, b, a) {
132
+ this._renderer.setClearOptions({ clearColor: [r, g, b, a !== undefined ? a : 1], clear: true });
146
133
  return this;
147
134
  }
148
135
 
149
- /** Set camera orbit distance */
150
- setCameraDistance(distance) {
151
- this._orbitRadius = distance;
152
- return this;
136
+ /** Add a model to the scene (without removing existing ones) */
137
+ addModel(url) {
138
+ var self = this;
139
+ return new Promise(function(resolve, reject) {
140
+ fetch(url)
141
+ .then(function(resp) { return resp.arrayBuffer(); })
142
+ .then(function(buffer) {
143
+ var data = new Uint8Array(buffer);
144
+ try {
145
+ var asset = self._loader.createAsset(data);
146
+ if (!asset) { reject(new Error('Failed to parse: ' + url)); return; }
147
+ asset.loadResources();
148
+ self._scene.addEntity(asset.getRoot());
149
+ self._scene.addEntities(asset.getRenderableEntities());
150
+ resolve(asset);
151
+ } catch (e) { reject(e); }
152
+ })
153
+ .catch(reject);
154
+ });
153
155
  }
154
156
 
155
- /** Set background color [r, g, b, a] (0-1 range) */
156
- setBackgroundColor(r, g, b, a) {
157
- this._renderer.setClearOptions({ clearColor: [r, g, b, a !== undefined ? a : 1], clear: true });
158
- return this;
157
+ /** Load a GLB from a Uint8Array buffer directly */
158
+ loadGLBBuffer(buffer, key) {
159
+ var asset = this._loader.createAsset(buffer);
160
+ if (!asset) return null;
161
+ asset.loadResources();
162
+ this._scene.addEntity(asset.getRoot());
163
+ this._scene.addEntities(asset.getRenderableEntities());
164
+ return asset;
165
+ }
166
+
167
+ /** Remove an asset from the scene */
168
+ removeAsset(asset) {
169
+ if (!asset) return;
170
+ try {
171
+ asset.getRenderableEntities().forEach(function(e) { this._scene.remove(e); }.bind(this));
172
+ this._scene.remove(asset.getRoot());
173
+ } catch (e) { /* ignore cleanup errors */ }
159
174
  }
160
175
 
161
- /** Dispose all resources */
176
+ /** Access engine for advanced Filament operations */
177
+ get engine() { return this._engine; }
178
+ get scene() { return this._scene; }
179
+
162
180
  dispose() {
163
181
  this._running = false;
164
- if (this._resizeObserver) {
165
- this._resizeObserver.disconnect();
166
- }
167
- Filament.Engine.destroy(this._engine);
182
+ if (this._resizeObserver) this._resizeObserver.disconnect();
183
+ try { Filament.Engine.destroy(this._engine); } catch (e) { /* already destroyed */ }
168
184
  }
169
185
 
170
- // --- Private ---
171
-
172
186
  _setupControls() {
173
187
  var canvas = this._canvas;
174
188
  var self = this;
175
189
 
176
- // Mouse orbit
177
190
  canvas.addEventListener('mousedown', function(e) {
178
191
  self._isDragging = true;
179
192
  self._lastMouse = { x: e.clientX, y: e.clientY };
180
193
  self._autoRotate = false;
194
+ self._velocityAngle = 0;
195
+ self._velocityHeight = 0;
196
+ if (self._autoRotateTimer) { clearTimeout(self._autoRotateTimer); self._autoRotateTimer = null; }
181
197
  });
182
198
  canvas.addEventListener('mousemove', function(e) {
183
199
  if (!self._isDragging) return;
184
- self._angle -= (e.clientX - self._lastMouse.x) * 0.005;
185
- self._orbitHeight += (e.clientY - self._lastMouse.y) * 0.01;
200
+ var dx = (e.clientX - self._lastMouse.x) * 0.005;
201
+ var dy = (e.clientY - self._lastMouse.y) * 0.01;
202
+ self._velocityAngle = -dx;
203
+ self._velocityHeight = dy;
204
+ self._angle -= dx;
205
+ self._orbitHeight += dy;
186
206
  self._lastMouse = { x: e.clientX, y: e.clientY };
187
207
  });
188
- canvas.addEventListener('mouseup', function() { self._isDragging = false; });
189
- canvas.addEventListener('mouseleave', function() { self._isDragging = false; });
208
+ canvas.addEventListener('mouseup', function() {
209
+ self._isDragging = false;
210
+ // Resume auto-rotate after 3s idle (like model-viewer)
211
+ if (self._wantsAutoRotate) {
212
+ self._autoRotateTimer = setTimeout(function() { self._autoRotate = true; }, 3000);
213
+ }
214
+ });
215
+ canvas.addEventListener('mouseleave', function() {
216
+ self._isDragging = false;
217
+ if (self._wantsAutoRotate) {
218
+ self._autoRotateTimer = setTimeout(function() { self._autoRotate = true; }, 3000);
219
+ }
220
+ });
190
221
 
191
- // Scroll zoom
192
222
  canvas.addEventListener('wheel', function(e) {
193
223
  e.preventDefault();
194
224
  self._orbitRadius *= (1 + e.deltaY * 0.001);
195
225
  self._orbitRadius = Math.max(0.5, Math.min(50, self._orbitRadius));
196
226
  }, { passive: false });
197
227
 
198
- // Touch orbit
199
228
  canvas.addEventListener('touchstart', function(e) {
200
229
  if (e.touches.length === 1) {
201
230
  self._isDragging = true;
202
231
  self._lastMouse = { x: e.touches[0].clientX, y: e.touches[0].clientY };
203
232
  self._autoRotate = false;
233
+ self._velocityAngle = 0;
234
+ self._velocityHeight = 0;
235
+ if (self._autoRotateTimer) { clearTimeout(self._autoRotateTimer); self._autoRotateTimer = null; }
204
236
  }
205
237
  });
206
238
  canvas.addEventListener('touchmove', function(e) {
207
239
  if (!self._isDragging || e.touches.length !== 1) return;
208
240
  e.preventDefault();
209
- self._angle -= (e.touches[0].clientX - self._lastMouse.x) * 0.005;
210
- self._orbitHeight += (e.touches[0].clientY - self._lastMouse.y) * 0.01;
241
+ var dx = (e.touches[0].clientX - self._lastMouse.x) * 0.005;
242
+ var dy = (e.touches[0].clientY - self._lastMouse.y) * 0.01;
243
+ self._velocityAngle = -dx;
244
+ self._velocityHeight = dy;
245
+ self._angle -= dx;
246
+ self._orbitHeight += dy;
211
247
  self._lastMouse = { x: e.touches[0].clientX, y: e.touches[0].clientY };
212
248
  }, { passive: false });
213
- canvas.addEventListener('touchend', function() { self._isDragging = false; });
249
+ canvas.addEventListener('touchend', function() {
250
+ self._isDragging = false;
251
+ if (self._wantsAutoRotate) {
252
+ self._autoRotateTimer = setTimeout(function() { self._autoRotate = true; }, 3000);
253
+ }
254
+ });
214
255
  }
215
256
 
216
257
  _setupResizeObserver() {
217
258
  var self = this;
218
259
  this._resizeObserver = new ResizeObserver(function() {
219
260
  var canvas = self._canvas;
220
- canvas.width = canvas.clientWidth * devicePixelRatio;
221
- canvas.height = canvas.clientHeight * devicePixelRatio;
261
+ var dpr = Math.min(devicePixelRatio, 2); // Cap at 2x for performance
262
+ canvas.width = canvas.clientWidth * dpr;
263
+ canvas.height = canvas.clientHeight * dpr;
222
264
  self._view.setViewport([0, 0, canvas.width, canvas.height]);
223
- self._camera.setProjectionFov(45, canvas.width / canvas.height, 0.1, 1000, Filament.Camera$Fov.VERTICAL);
265
+ self._camera.setProjectionFov(
266
+ self._fov || 45, canvas.width / canvas.height, 0.1, 1000,
267
+ Filament.Camera$Fov.VERTICAL
268
+ );
224
269
  });
225
270
  this._resizeObserver.observe(this._canvas);
226
271
  }
@@ -229,7 +274,19 @@
229
274
  var self = this;
230
275
  function render() {
231
276
  if (!self._running) return;
232
- if (self._autoRotate) self._angle += 0.006;
277
+
278
+ // Auto-rotate: 30°/sec ÷ 60fps (matches model-viewer)
279
+ if (self._autoRotate) self._angle += 0.00873;
280
+
281
+ // Inertia damping after drag release
282
+ if (!self._isDragging) {
283
+ self._angle += self._velocityAngle;
284
+ self._orbitHeight += self._velocityHeight;
285
+ self._velocityAngle *= self._dampingFactor;
286
+ self._velocityHeight *= self._dampingFactor;
287
+ if (Math.abs(self._velocityAngle) < 0.00005) self._velocityAngle = 0;
288
+ if (Math.abs(self._velocityHeight) < 0.00005) self._velocityHeight = 0;
289
+ }
233
290
 
234
291
  var t = self._orbitTarget;
235
292
  var r = self._orbitRadius;
@@ -240,20 +297,28 @@
240
297
  [0, 1, 0]
241
298
  );
242
299
 
243
- if (self._renderer.beginFrame(self._swapChain)) {
244
- self._renderer.render(self._swapChain, self._view);
245
- self._renderer.endFrame();
246
- }
247
300
  self._engine.execute();
301
+ try {
302
+ if (self._renderer.beginFrame(self._swapChain)) {
303
+ self._renderer.renderView(self._view);
304
+ self._renderer.endFrame();
305
+ }
306
+ } catch (e) {
307
+ // Filament 1.70 may need different render call
308
+ console.error('SceneView render error:', e.message);
309
+ self._running = false;
310
+ }
248
311
  requestAnimationFrame(render);
249
312
  }
250
313
  render();
251
314
  }
252
315
  }
253
316
 
317
+ // Singleton guard — prevent multiple engine creations on same canvas
318
+ var _activeCanvases = new Set();
319
+
254
320
  /**
255
- * Internal: set up Filament engine, scene, lights on a canvas.
256
- * Called inside a Filament.init() callback where WASM is guaranteed ready.
321
+ * Set up Filament engine, scene, lights on a canvas.
257
322
  */
258
323
  function _createEngine(canvasOrId, options) {
259
324
  options = options || {};
@@ -261,11 +326,21 @@
261
326
  var canvas = typeof canvasOrId === 'string'
262
327
  ? document.getElementById(canvasOrId)
263
328
  : canvasOrId;
264
-
265
329
  if (!canvas) throw new Error('Canvas not found: ' + canvasOrId);
266
330
 
267
- canvas.width = canvas.clientWidth * devicePixelRatio;
268
- canvas.height = canvas.clientHeight * devicePixelRatio;
331
+ // Prevent double initialization on the same canvas
332
+ if (_activeCanvases.has(canvas)) {
333
+ console.warn('SceneView: Canvas already initialized, skipping');
334
+ return null;
335
+ }
336
+ _activeCanvases.add(canvas);
337
+
338
+ var dpr = Math.min(devicePixelRatio, 2);
339
+ // Ensure canvas has actual layout dimensions (not default 300x150)
340
+ var cssW = canvas.clientWidth || canvas.offsetWidth || 500;
341
+ var cssH = canvas.clientHeight || canvas.offsetHeight || 500;
342
+ canvas.width = cssW * dpr;
343
+ canvas.height = cssH * dpr;
269
344
 
270
345
  var engine = Filament.Engine.create(canvas);
271
346
  var scene = engine.createScene();
@@ -282,11 +357,19 @@
282
357
  var bg = options.backgroundColor || [0.05, 0.06, 0.1, 1.0];
283
358
  renderer.setClearOptions({ clearColor: bg, clear: true });
284
359
 
285
- var aspect = canvas.width / canvas.height;
286
- camera.setProjectionFov(options.fov || 45, aspect, 0.1, 1000, Filament.Camera$Fov.VERTICAL);
360
+ var fov = options.fov || 45;
361
+ camera.setProjectionFov(fov, canvas.width / canvas.height, 0.1, 1000, Filament.Camera$Fov.VERTICAL);
287
362
  camera.lookAt([0, 1, 5], [0, 0, 0], [0, 1, 0]);
288
363
 
289
- // Default sunlight
364
+ // --- Post-processing quality ---
365
+ try {
366
+ view.setAmbientOcclusionOptions({
367
+ enabled: true, radius: 0.3, bias: 0.0005, intensity: 1.0, quality: 1
368
+ });
369
+ } catch (e) { /* skip */ }
370
+
371
+ // --- 3-point studio lighting ---
372
+ // Sun/key light — warm, strong
290
373
  var sun = Filament.EntityManager.get().create();
291
374
  Filament.LightManager.Builder(Filament.LightManager$Type.SUN)
292
375
  .color([0.98, 0.92, 0.89])
@@ -298,90 +381,123 @@
298
381
  .build(engine, sun);
299
382
  scene.addEntity(sun);
300
383
 
301
- // Fill light
384
+ // Fill light — cool, softer
302
385
  var fill = Filament.EntityManager.get().create();
303
386
  Filament.LightManager.Builder(Filament.LightManager$Type.DIRECTIONAL)
304
- .color([0.6, 0.65, 0.8])
305
- .intensity(30000)
387
+ .color([0.7, 0.75, 0.9])
388
+ .intensity(60000)
306
389
  .direction([-0.5, 0.5, 1.0])
307
390
  .build(engine, fill);
308
391
  scene.addEntity(fill);
309
392
 
310
- // Asset loader (reused across model loads)
311
- var loader = engine.createAssetLoader();
393
+ // Back/rim light edge highlight
394
+ var back = Filament.EntityManager.get().create();
395
+ Filament.LightManager.Builder(Filament.LightManager$Type.DIRECTIONAL)
396
+ .color([0.5, 0.6, 0.9])
397
+ .intensity(50000)
398
+ .direction([0, 0.3, 1.0])
399
+ .build(engine, back);
400
+ scene.addEntity(back);
401
+
402
+ // --- IBL: load real KTX if available, fallback to synthetic SH ---
403
+ var iblUrl = options.iblUrl || 'environments/neutral_ibl.ktx';
404
+ fetch(iblUrl)
405
+ .then(function(r) {
406
+ if (!r.ok) throw new Error('HTTP ' + r.status);
407
+ return r.arrayBuffer().then(function(ab) { return new Uint8Array(ab); });
408
+ })
409
+ .then(function(buffer) {
410
+ try {
411
+ var ibl = engine.createIblFromKtx1(buffer);
412
+ ibl.setIntensity(options.iblIntensity || 40000);
413
+ scene.setIndirectLight(ibl);
414
+ // Create skybox from IBL reflection cubemap if skybox enabled
415
+ if (options.skybox !== false) {
416
+ try {
417
+ var reflections = ibl.getReflectionsTexture();
418
+ if (reflections) {
419
+ var skybox = Filament.Skybox.Builder()
420
+ .environment(reflections)
421
+ .build(engine);
422
+ scene.setSkybox(skybox);
423
+ console.log('SceneView: Skybox created from IBL cubemap');
424
+ }
425
+ } catch (skyErr) {
426
+ // Skybox not supported in this build — that's OK
427
+ console.log('SceneView: Skybox not available (IBL-only mode)');
428
+ }
429
+ }
430
+ console.log('SceneView: KTX IBL loaded (' + Math.round(buffer.length / 1024) + 'KB)');
431
+ } catch (e) {
432
+ console.warn('SceneView: createIblFromKtx1 failed, using SH fallback', e);
433
+ _applySyntheticIBL(engine, scene);
434
+ }
435
+ })
436
+ .catch(function() {
437
+ _applySyntheticIBL(engine, scene);
438
+ });
312
439
 
440
+ var loader = engine.createAssetLoader();
313
441
  var instance = new SceneViewInstance(canvas, engine, scene, renderer, view, swapChain, camera, cameraEntity, loader);
442
+ instance._fov = fov;
314
443
 
315
- if (options.autoRotate === false) {
316
- instance.setAutoRotate(false);
317
- }
444
+ if (options.autoRotate === false) instance.setAutoRotate(false);
318
445
 
319
446
  return instance;
320
447
  }
321
448
 
322
- /**
323
- * Create an empty SceneView on a canvas.
324
- * Filament.js is loaded automatically from CDN if not already present.
325
- *
326
- * @param {string|HTMLCanvasElement} canvasOrId - Canvas element or its ID
327
- * @param {Object} [options] - Configuration options
328
- * @returns {Promise<SceneViewInstance>}
329
- */
449
+ /** Fallback IBL from spherical harmonics when KTX not available */
450
+ function _applySyntheticIBL(engine, scene) {
451
+ try {
452
+ var ibl = Filament.IndirectLight.Builder()
453
+ .irradiance(3, [
454
+ 0.65, 0.65, 0.70,
455
+ 0.10, 0.10, 0.12,
456
+ 0.15, 0.15, 0.18,
457
+ -0.02, -0.02, -0.01,
458
+ 0.04, 0.04, 0.05,
459
+ 0.08, 0.08, 0.10,
460
+ 0.01, 0.01, 0.01,
461
+ -0.02, -0.02, -0.02,
462
+ 0.03, 0.03, 0.03
463
+ ])
464
+ .intensity(35000)
465
+ .build(engine);
466
+ scene.setIndirectLight(ibl);
467
+ console.log('SceneView: Using synthetic SH IBL');
468
+ } catch (e) { /* skip */ }
469
+ }
470
+
330
471
  function create(canvasOrId, options) {
331
472
  return _ensureFilament().then(function() {
332
473
  return new Promise(function(resolve, reject) {
333
- // If WASM is already initialized (Engine exists), skip Filament.init
334
474
  if (typeof Filament.Engine !== 'undefined') {
335
475
  try {
336
- resolve(_createEngine(canvasOrId, options));
337
- } catch (e) {
338
- reject(e);
339
- }
476
+ var instance = _createEngine(canvasOrId, options);
477
+ if (instance) resolve(instance);
478
+ else reject(new Error('SceneView: Canvas already initialized'));
479
+ } catch (e) { reject(e); }
340
480
  return;
341
481
  }
342
- // First time: initialize WASM
343
482
  Filament.init([], function() {
344
483
  try {
345
- resolve(_createEngine(canvasOrId, options));
346
- } catch (e) {
347
- reject(e);
348
- }
484
+ var instance = _createEngine(canvasOrId, options);
485
+ if (instance) resolve(instance);
486
+ else reject(new Error('SceneView: Canvas already initialized'));
487
+ } catch (e) { reject(e); }
349
488
  });
350
489
  });
351
490
  });
352
491
  }
353
492
 
354
- /**
355
- * One-liner: create viewer and load a model.
356
- * Filament.js is loaded automatically from CDN if not already present.
357
- *
358
- * @param {string|HTMLCanvasElement} canvasOrId
359
- * @param {string} modelUrl - URL to .glb/.gltf model
360
- * @param {Object} [options]
361
- * @returns {Promise<SceneViewInstance>}
362
- */
363
493
  function modelViewer(canvasOrId, modelUrl, options) {
364
- return _ensureFilament().then(function() {
365
- return new Promise(function(resolve, reject) {
366
- // Always use Filament.init with the model URL in the assets array.
367
- // This works whether WASM is already loaded or not, because Filament
368
- // needs to fetch the model asset and will call back when done.
369
- Filament.init([modelUrl], function() {
370
- try {
371
- var instance = _createEngine(canvasOrId, options);
372
- instance._showModel(modelUrl);
373
- resolve(instance);
374
- } catch (e) {
375
- reject(e);
376
- }
377
- });
378
- });
494
+ return create(canvasOrId, options).then(function(instance) {
495
+ return instance.loadModel(modelUrl);
379
496
  });
380
497
  }
381
498
 
382
- // Public API
383
499
  global.SceneView = {
384
- version: '1.1.0',
500
+ version: '1.4.0',
385
501
  create: create,
386
502
  modelViewer: modelViewer
387
503
  };