MikaFil commited on
Commit
6e742f9
·
verified ·
1 Parent(s): e32cc21

Update viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +192 -237
viewer.js CHANGED
@@ -1,7 +1,7 @@
1
  // viewer.js
2
  // ==============================
3
 
4
- let pc; // will hold the PlayCanvas module once imported
5
  export let app = null;
6
  let cameraEntity = null;
7
  let modelEntity = null;
@@ -14,13 +14,9 @@ let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRot
14
  let plyUrl, glbUrl;
15
 
16
  export async function initializeViewer(config, instanceId) {
17
- alert("[viewer.js] initializeViewer called");
18
-
19
- if (viewerInitialized) {
20
- alert("[viewer.js] Already initialized, skipping.");
21
- return;
22
- }
23
 
 
24
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
25
  const isMobile = isIOS || /Android/i.test(navigator.userAgent);
26
 
@@ -61,8 +57,6 @@ export async function initializeViewer(config, instanceId) {
61
  const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
62
  const viewerContainer = document.getElementById('viewer-container-' + instanceId);
63
 
64
- alert("[viewer.js] DOM elements grabbed");
65
-
66
  // 3. Create <canvas>
67
  let oldCanvas = document.getElementById(canvasId);
68
  if (oldCanvas) oldCanvas.remove();
@@ -70,26 +64,24 @@ export async function initializeViewer(config, instanceId) {
70
  canvas.id = canvasId;
71
  canvas.className = 'ply-canvas';
72
  canvas.style.zIndex = "1";
 
 
 
73
  viewerContainer.insertBefore(canvas, progressDialog);
74
- alert("[viewer.js] Canvas inserted");
75
 
76
- // Prevent iOS Safari from zooming the whole page via gestures on canvas
 
 
 
 
 
 
77
  canvas.addEventListener('touchstart', function(e) {
78
  if (e.touches.length > 1) e.preventDefault();
79
  }, { passive: false });
80
- canvas.addEventListener('gesturestart', function(e) {
81
- e.preventDefault();
82
- });
83
- canvas.addEventListener('gesturechange', function(e) {
84
- e.preventDefault();
85
- });
86
- canvas.addEventListener('gestureend', function(e) {
87
- e.preventDefault();
88
- });
89
 
90
- // 4. Wheel listener
91
  canvas.addEventListener('wheel', e => {
92
- alert("[viewer.js] Wheel event on canvas");
93
  if (cameraEntity && cameraEntity.script && cameraEntity.script.orbitCamera) {
94
  const orbitCam = cameraEntity.script.orbitCamera;
95
  const sens = (cameraEntity.script.orbitCameraInputMouse?.distanceSensitivity) || 0.4;
@@ -103,218 +95,196 @@ export async function initializeViewer(config, instanceId) {
103
  }
104
  }, { passive: false });
105
 
 
106
  progressDialog.style.display = 'block';
107
- alert("[viewer.js] Canvas ready, loading PlayCanvas...");
108
 
109
- // 5. Import PlayCanvas
110
  if (!pc) {
111
- try {
112
- pc = await import("https://esm.run/playcanvas");
113
- window.pc = pc;
114
- alert("[viewer.js] PlayCanvas loaded");
115
- } catch (err) {
116
- alert("[viewer.js] ERROR loading PlayCanvas: " + err);
117
- return;
118
- }
119
  }
120
 
121
- try {
122
- // 6. Setup device & app
123
- const device = await pc.createGraphicsDevice(canvas, {
124
- deviceTypes: ["webgl2"],
125
- glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
126
- twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
127
- antialias: false
128
- });
129
- device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
130
-
131
- const opts = new pc.AppOptions();
132
- opts.graphicsDevice = device;
133
- opts.mouse = new pc.Mouse(canvas);
134
- opts.touch = new pc.TouchDevice(canvas);
135
- opts.componentSystems = [
136
- pc.RenderComponentSystem,
137
- pc.CameraComponentSystem,
138
- pc.LightComponentSystem,
139
- pc.ScriptComponentSystem,
140
- pc.GSplatComponentSystem,
141
- pc.CollisionComponentSystem,
142
- pc.RigidbodyComponentSystem
143
- ];
144
- opts.resourceHandlers = [
145
- pc.TextureHandler,
146
- pc.ContainerHandler,
147
- pc.ScriptHandler,
148
- pc.GSplatHandler
149
- ];
150
-
151
- app = new pc.Application(canvas, opts);
152
- app.setCanvasFillMode(pc.FILLMODE_NONE);
153
- app.setCanvasResolution(pc.RESOLUTION_AUTO);
154
-
155
- // Attach ResizeObserver to keep canvas in sync with container size
156
- resizeObserver = new ResizeObserver(entries => {
157
- for (const entry of entries) {
158
- const { width, height } = entry.contentRect;
159
- if (app) {
160
- app.resizeCanvas(width, height);
161
- }
162
  }
163
- });
164
- resizeObserver.observe(viewerContainer);
 
165
 
166
- window.addEventListener('resize', () => {
167
- if (app) app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
168
- });
169
 
170
- app.on('destroy', () => {
171
- window.removeEventListener('resize', resizeCanvas);
172
- });
 
173
 
174
- alert("[viewer.js] PlayCanvas app initialized");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
 
176
- // 7. Assets
177
- const assets = {
178
- model: new pc.Asset('gsplat', 'gsplat', { url: plyUrl }),
179
- orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
180
- galerie: new pc.Asset('galerie', 'container', { url: glbUrl }),
181
- hdr: new pc.Asset('hdr', 'texture', {
182
- url: "https://huggingface.co/datasets/bilca/ply_files/resolve/main/galeries/blanc.png"
183
- }, { type: pc.TEXTURETYPE_RGBP, mipmaps: false })
184
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
 
186
- alert("[viewer.js] Assets declared");
 
 
 
 
187
 
188
- const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
189
- let lastProg = 0;
190
- assets.model.on('load', () => {
191
- progressDialog.style.display = 'none';
192
- alert("[viewer.js] Main model loaded");
 
 
 
 
 
 
193
  });
194
- assets.model.on('error', err => {
195
- progressDialog.innerHTML = `<p style="color: red">Error loading model: ${err}</p>`;
196
- alert("[viewer.js] ERROR loading main model: " + err);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  });
198
-
199
- const progCheck = setInterval(() => {
200
- if (assets.model.resource) {
201
- progressIndicator.value = 100;
202
- clearInterval(progCheck);
203
- progressDialog.style.display = 'none';
204
- } else if (assets.model.loading) {
205
- lastProg = Math.min(lastProg + 2, 90);
206
- progressIndicator.value = lastProg;
207
  }
208
- }, 100);
209
-
210
- loader.load(async () => {
211
- alert("[viewer.js] Asset loader complete. Starting app...");
212
- app.start();
213
- app.scene.envAtlas = assets.hdr.resource;
214
- alert("[viewer.js] Scene envAtlas set");
215
-
216
- // Model entity
217
- modelEntity = new pc.Entity('model');
218
- modelEntity.addComponent('gsplat', { asset: assets.model });
219
- modelEntity.setLocalPosition(modelX, modelY, modelZ);
220
- modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
221
- modelEntity.setLocalScale(modelScale, modelScale, modelScale);
222
- app.root.addChild(modelEntity);
223
- alert("[viewer.js] Main model entity added");
224
-
225
- // Light
226
- const dirLight = new pc.Entity('Cascaded Light');
227
- dirLight.addComponent('light', {
228
- type: 'directional',
229
- color: pc.Color.WHITE,
230
- shadowBias: 0.3,
231
- normalOffsetBias: 0.2,
232
- intensity: 1.0,
233
- soft: true,
234
- shadowResolution: 4096,
235
- penumbraSize: 7,
236
- penumbraFalloff: 1.5,
237
- shadowSamples: 128,
238
- shadowBlockerSamples: 16,
239
- castShadows: true,
240
- shadowType: pc.SHADOW_PCSS_32F,
241
- shadowDistance: 1000
242
- });
243
- dirLight.setLocalEulerAngles(0, 0, 0);
244
- app.root.addChild(dirLight);
245
- alert("[viewer.js] Directional light entity added");
246
-
247
- // Gallery GLB
248
- if (assets.galerie && assets.galerie.resource && assets.galerie.resource.instantiateRenderEntity) {
249
- const galleryEntity = assets.galerie.resource.instantiateRenderEntity();
250
- app.root.addChild(galleryEntity);
251
- alert("[viewer.js] Gallery GLB entity added");
252
  }
 
 
 
 
 
253
 
254
- // Camera setup
255
- cameraEntity = new pc.Entity('camera');
256
- cameraEntity.addComponent('camera', {
257
- clearColor: config.canvas_background
258
- ? parseInt(config.canvas_background.substr(1, 2), 16) / 255
259
- : 0
260
- });
261
- cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
262
- cameraEntity.lookAt(modelEntity.getPosition());
263
-
264
- cameraEntity.addComponent('script');
265
- cameraEntity.script.create('orbitCamera', {
266
- attributes: {
267
- inertiaFactor: 0.2,
268
- focusEntity: modelEntity,
269
- distanceMax: maxZoom,
270
- distanceMin: minZoom,
271
- pitchAngleMax: maxAngle,
272
- pitchAngleMin: minAngle,
273
- yawAngleMax: maxAzimuth,
274
- yawAngleMin: minAzimuth,
275
- minPivotY: minPivotY,
276
- frameOnStart: false
277
- }
278
- });
279
- cameraEntity.script.create('orbitCameraInputMouse', {
280
- attributes: {
281
- orbitSensitivity: isMobile ? 0.6 : 0.3,
282
- distanceSensitivity: isMobile ? 0.5 : 0.4
283
- }
284
- });
285
- if (cameraEntity.script.orbitCameraInputMouse) {
286
- cameraEntity.script.orbitCameraInputMouse.onMouseWheel = function() {
287
- alert("[viewer.js] onMouseWheel (custom suppressed)");
288
- };
289
  }
290
- cameraEntity.script.create('orbitCameraInputTouch', {
291
- attributes: {
292
- orbitSensitivity: 0.6,
293
- distanceSensitivity: 0.5
294
- }
295
- });
296
- app.root.addChild(cameraEntity);
297
- alert("[viewer.js] Camera entity added and set up");
298
-
299
- // Reset & constrain updates
300
- app.once('update', () => {
301
- alert("[viewer.js] First app update (reset camera)");
302
- resetViewerCamera();
303
- });
304
- app.on('update', dt => {
305
- if (cameraEntity) {
306
- const pos = cameraEntity.getPosition();
307
- if (pos.y < minY) {
308
- cameraEntity.setPosition(pos.x, minY, pos.z);
309
- }
310
- }
311
- });
312
-
313
- // Final resize
314
- app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
315
- alert("[viewer.js] Canvas resized to viewer container");
316
-
317
- // Tooltips
318
  try {
319
  const tooltipsModule = await import('./tooltips.js');
320
  await tooltipsModule.initializeTooltips({
@@ -325,24 +295,15 @@ export async function initializeViewer(config, instanceId) {
325
  defaultVisible: !!config.showTooltipsDefault,
326
  moveDuration: config.tooltipMoveDuration || 0.6
327
  });
328
- alert("[viewer.js] Tooltips initialized");
329
- } catch (e) {
330
- alert("[viewer.js] Tooltips.js import/init failed: " + e);
331
- }
332
-
333
- progressDialog.style.display = 'none';
334
- alert("[viewer.js] App fully loaded!");
335
- viewerInitialized = true;
336
- });
337
 
338
- } catch (error) {
339
- progressDialog.innerHTML = `<p style="color: red">Error loading viewer: ${error && error.message ? error.message : error}</p>`;
340
- alert("[viewer.js] ERROR in PlayCanvas app setup: " + (error && error.message ? error.message : error));
341
- }
342
  }
343
 
344
  export function resetViewerCamera() {
345
- alert("[viewer.js] resetViewerCamera called");
346
  try {
347
  if (!cameraEntity || !modelEntity || !app) return;
348
  const orbitCam = cameraEntity.script.orbitCamera;
@@ -383,14 +344,10 @@ export function resetViewerCamera() {
383
  if (orbitCam._updatePosition) orbitCam._updatePosition();
384
 
385
  tempEnt.destroy();
386
- alert("[viewer.js] resetViewerCamera completed");
387
- } catch (e) {
388
- alert("[viewer.js] ERROR in resetViewerCamera: " + e);
389
- }
390
  }
391
 
392
  export function cleanupViewer() {
393
- alert("[viewer.js] cleanupViewer called");
394
  if (app) {
395
  try {
396
  app.destroy();
@@ -400,10 +357,8 @@ export function cleanupViewer() {
400
  cameraEntity = null;
401
  modelEntity = null;
402
  viewerInitialized = false;
403
-
404
  if (resizeObserver) {
405
  resizeObserver.disconnect();
406
  resizeObserver = null;
407
  }
408
- alert("[viewer.js] cleanupViewer completed");
409
  }
 
1
  // viewer.js
2
  // ==============================
3
 
4
+ let pc; // PlayCanvas module (will be loaded dynamically)
5
  export let app = null;
6
  let cameraEntity = null;
7
  let modelEntity = null;
 
14
  let plyUrl, glbUrl;
15
 
16
  export async function initializeViewer(config, instanceId) {
17
+ if (viewerInitialized) return;
 
 
 
 
 
18
 
19
+ // Device detection
20
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
21
  const isMobile = isIOS || /Android/i.test(navigator.userAgent);
22
 
 
57
  const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
58
  const viewerContainer = document.getElementById('viewer-container-' + instanceId);
59
 
 
 
60
  // 3. Create <canvas>
61
  let oldCanvas = document.getElementById(canvasId);
62
  if (oldCanvas) oldCanvas.remove();
 
64
  canvas.id = canvasId;
65
  canvas.className = 'ply-canvas';
66
  canvas.style.zIndex = "1";
67
+ canvas.style.width = "100%";
68
+ canvas.style.height = "100%";
69
+ canvas.setAttribute('tabindex', '0');
70
  viewerContainer.insertBefore(canvas, progressDialog);
 
71
 
72
+ // Prevent iOS Safari zoom gesture conflicts
73
+ canvas.style.touchAction = "none";
74
+ canvas.style.webkitTouchCallout = "none";
75
+ canvas.addEventListener('gesturestart', e => e.preventDefault());
76
+ canvas.addEventListener('gesturechange', e => e.preventDefault());
77
+ canvas.addEventListener('gestureend', e => e.preventDefault());
78
+ canvas.addEventListener('dblclick', e => e.preventDefault());
79
  canvas.addEventListener('touchstart', function(e) {
80
  if (e.touches.length > 1) e.preventDefault();
81
  }, { passive: false });
 
 
 
 
 
 
 
 
 
82
 
83
+ // 4. Wheel/zoom listener (desktop)
84
  canvas.addEventListener('wheel', e => {
 
85
  if (cameraEntity && cameraEntity.script && cameraEntity.script.orbitCamera) {
86
  const orbitCam = cameraEntity.script.orbitCamera;
87
  const sens = (cameraEntity.script.orbitCameraInputMouse?.distanceSensitivity) || 0.4;
 
95
  }
96
  }, { passive: false });
97
 
98
+ // Show loading
99
  progressDialog.style.display = 'block';
 
100
 
101
+ // 5. Import PlayCanvas if not present
102
  if (!pc) {
103
+ pc = await import("https://esm.run/playcanvas");
104
+ window.pc = pc;
 
 
 
 
 
 
105
  }
106
 
107
+ // 6. Setup device & app
108
+ const device = await pc.createGraphicsDevice(canvas, {
109
+ deviceTypes: ["webgl2"],
110
+ glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
111
+ twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
112
+ antialias: false
113
+ });
114
+ device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
115
+
116
+ const opts = new pc.AppOptions();
117
+ opts.graphicsDevice = device;
118
+ opts.mouse = new pc.Mouse(canvas);
119
+ opts.touch = new pc.TouchDevice(canvas);
120
+ opts.componentSystems = [
121
+ pc.RenderComponentSystem,
122
+ pc.CameraComponentSystem,
123
+ pc.LightComponentSystem,
124
+ pc.ScriptComponentSystem,
125
+ pc.GSplatComponentSystem,
126
+ pc.CollisionComponentSystem,
127
+ pc.RigidbodyComponentSystem
128
+ ];
129
+ opts.resourceHandlers = [
130
+ pc.TextureHandler,
131
+ pc.ContainerHandler,
132
+ pc.ScriptHandler,
133
+ pc.GSplatHandler
134
+ ];
135
+
136
+ app = new pc.Application(canvas, opts);
137
+ app.setCanvasFillMode(pc.FILLMODE_NONE);
138
+ app.setCanvasResolution(pc.RESOLUTION_AUTO);
139
+
140
+ // Attach ResizeObserver to keep canvas in sync with container size
141
+ resizeObserver = new ResizeObserver(entries => {
142
+ for (const entry of entries) {
143
+ const { width, height } = entry.contentRect;
144
+ if (app) {
145
+ app.resizeCanvas(width, height);
 
 
146
  }
147
+ }
148
+ });
149
+ resizeObserver.observe(viewerContainer);
150
 
151
+ window.addEventListener('resize', () => {
152
+ if (app) app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
153
+ });
154
 
155
+ app.on('destroy', () => {
156
+ if (resizeObserver) resizeObserver.disconnect();
157
+ resizeObserver = null;
158
+ });
159
 
160
+ // 7. Asset loading (progress bar)
161
+ const assets = {
162
+ model: new pc.Asset('gsplat', 'gsplat', { url: plyUrl }),
163
+ orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
164
+ galerie: new pc.Asset('galerie', 'container', { url: glbUrl }),
165
+ hdr: new pc.Asset('hdr', 'texture', {
166
+ url: "https://huggingface.co/datasets/bilca/ply_files/resolve/main/galeries/blanc.png"
167
+ }, { type: pc.TEXTURETYPE_RGBP, mipmaps: false })
168
+ };
169
+
170
+ let lastProg = 0;
171
+ assets.model.on('load', () => { progressDialog.style.display = 'none'; });
172
+ assets.model.on('error', err => {
173
+ progressDialog.innerHTML = `<p style="color: red">Error loading model: ${err}</p>`;
174
+ });
175
 
176
+ const progCheck = setInterval(() => {
177
+ if (assets.model.resource) {
178
+ progressIndicator.value = 100;
179
+ clearInterval(progCheck);
180
+ progressDialog.style.display = 'none';
181
+ } else if (assets.model.loading) {
182
+ lastProg = Math.min(lastProg + 2, 90);
183
+ progressIndicator.value = lastProg;
184
+ }
185
+ }, 100);
186
+
187
+ const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
188
+ loader.load(async () => {
189
+ app.start();
190
+ app.scene.envAtlas = assets.hdr.resource;
191
+
192
+ // Main model entity
193
+ modelEntity = new pc.Entity('model');
194
+ modelEntity.addComponent('gsplat', { asset: assets.model });
195
+ modelEntity.setLocalPosition(modelX, modelY, modelZ);
196
+ modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
197
+ modelEntity.setLocalScale(modelScale, modelScale, modelScale);
198
+ app.root.addChild(modelEntity);
199
+
200
+ // Light
201
+ const dirLight = new pc.Entity('Cascaded Light');
202
+ dirLight.addComponent('light', {
203
+ type: 'directional',
204
+ color: pc.Color.WHITE,
205
+ shadowBias: 0.3,
206
+ normalOffsetBias: 0.2,
207
+ intensity: 1.0,
208
+ soft: true,
209
+ shadowResolution: 4096,
210
+ penumbraSize: 7,
211
+ penumbraFalloff: 1.5,
212
+ shadowSamples: 128,
213
+ shadowBlockerSamples: 16,
214
+ castShadows: true,
215
+ shadowType: pc.SHADOW_PCSS_32F,
216
+ shadowDistance: 1000
217
+ });
218
+ dirLight.setLocalEulerAngles(0, 0, 0);
219
+ app.root.addChild(dirLight);
220
 
221
+ // Gallery GLB (optional)
222
+ if (assets.galerie && assets.galerie.resource && assets.galerie.resource.instantiateRenderEntity) {
223
+ const galleryEntity = assets.galerie.resource.instantiateRenderEntity();
224
+ app.root.addChild(galleryEntity);
225
+ }
226
 
227
+ // Camera setup
228
+ cameraEntity = new pc.Entity('camera');
229
+ cameraEntity.addComponent('camera', {
230
+ clearColor: config.canvas_background
231
+ ? new pc.Color(
232
+ parseInt(config.canvas_background.substr(1, 2), 16) / 255,
233
+ parseInt(config.canvas_background.substr(3, 2), 16) / 255,
234
+ parseInt(config.canvas_background.substr(5, 2), 16) / 255,
235
+ 1
236
+ )
237
+ : new pc.Color(0.2, 0.2, 0.2, 1)
238
  });
239
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
240
+ cameraEntity.lookAt(modelEntity.getPosition());
241
+
242
+ cameraEntity.addComponent('script');
243
+ cameraEntity.script.create('orbitCamera', {
244
+ attributes: {
245
+ inertiaFactor: 0.2,
246
+ focusEntity: modelEntity,
247
+ distanceMax: maxZoom,
248
+ distanceMin: minZoom,
249
+ pitchAngleMax: maxAngle,
250
+ pitchAngleMin: minAngle,
251
+ yawAngleMax: maxAzimuth,
252
+ yawAngleMin: minAzimuth,
253
+ minPivotY: minPivotY,
254
+ minY: minY,
255
+ frameOnStart: false
256
+ }
257
  });
258
+ cameraEntity.script.create('orbitCameraInputMouse', {
259
+ attributes: {
260
+ orbitSensitivity: isMobile ? 0.6 : 0.3,
261
+ distanceSensitivity: isMobile ? 0.5 : 0.4
 
 
 
 
 
262
  }
263
+ });
264
+ cameraEntity.script.create('orbitCameraInputTouch', {
265
+ attributes: {
266
+ orbitSensitivity: 0.6,
267
+ distanceSensitivity: 0.5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  }
269
+ });
270
+ app.root.addChild(cameraEntity);
271
+
272
+ // On first update, reset camera to proper config
273
+ app.once('update', () => { resetViewerCamera(); });
274
 
275
+ // Clamp Y so camera never goes below floor
276
+ app.on('update', dt => {
277
+ if (cameraEntity) {
278
+ const pos = cameraEntity.getPosition();
279
+ if (pos.y < minY) cameraEntity.setPosition(pos.x, minY, pos.z);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  }
281
+ });
282
+
283
+ // Final resize
284
+ app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
285
+
286
+ // Tooltips
287
+ if (config.tooltips_url) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  try {
289
  const tooltipsModule = await import('./tooltips.js');
290
  await tooltipsModule.initializeTooltips({
 
295
  defaultVisible: !!config.showTooltipsDefault,
296
  moveDuration: config.tooltipMoveDuration || 0.6
297
  });
298
+ } catch (e) {/* Fail quietly */}
299
+ }
 
 
 
 
 
 
 
300
 
301
+ // Ready!
302
+ viewerInitialized = true;
303
+ });
 
304
  }
305
 
306
  export function resetViewerCamera() {
 
307
  try {
308
  if (!cameraEntity || !modelEntity || !app) return;
309
  const orbitCam = cameraEntity.script.orbitCamera;
 
344
  if (orbitCam._updatePosition) orbitCam._updatePosition();
345
 
346
  tempEnt.destroy();
347
+ } catch (e) {/* Fail quietly */}
 
 
 
348
  }
349
 
350
  export function cleanupViewer() {
 
351
  if (app) {
352
  try {
353
  app.destroy();
 
357
  cameraEntity = null;
358
  modelEntity = null;
359
  viewerInitialized = false;
 
360
  if (resizeObserver) {
361
  resizeObserver.disconnect();
362
  resizeObserver = null;
363
  }
 
364
  }