MikaFil commited on
Commit
e704083
·
verified ·
1 Parent(s): c10c30e

Update viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +279 -188
viewer.js CHANGED
@@ -14,204 +14,288 @@ let minZoom, maxZoom, minAngle, maxAngle, minAzimuth, maxAzimuth, minPivotY, min
14
  let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRotationZ;
15
  let plyUrl, glbUrl;
16
 
 
 
 
 
 
 
 
 
 
17
  export async function initializeViewer(config, instanceId) {
18
- if (viewerInitialized) return;
19
-
20
- const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
21
- const isMobile = isIOS || /Android/i.test(navigator.userAgent);
22
-
23
- plyUrl = config.ply_url;
24
- glbUrl = config.glb_url;
25
- minZoom = parseFloat(config.minZoom || "1");
26
- maxZoom = parseFloat(config.maxZoom || "20");
27
- minAngle = parseFloat(config.minAngle || "-45");
28
- maxAngle = parseFloat(config.maxAngle || "90");
29
- minAzimuth = (config.minAzimuth !== undefined) ? parseFloat(config.minAzimuth) : -360;
30
- maxAzimuth = (config.maxAzimuth !== undefined) ? parseFloat(config.maxAzimuth) : 360;
31
- minPivotY = parseFloat(config.minPivotY || "0");
32
- minY = (config.minY !== undefined) ? parseFloat(config.minY) : 0;
33
-
34
- modelX = (config.modelX !== undefined) ? parseFloat(config.modelX) : 0;
35
- modelY = (config.modelY !== undefined) ? parseFloat(config.modelY) : 0;
36
- modelZ = (config.modelZ !== undefined) ? parseFloat(config.modelZ) : 0;
37
- modelScale = (config.modelScale !== undefined) ? parseFloat(config.modelScale) : 1;
38
- modelRotationX = (config.modelRotationX !== undefined) ? parseFloat(config.modelRotationX) : 0;
39
- modelRotationY = (config.modelRotationY !== undefined) ? parseFloat(config.modelRotationY) : 0;
40
- modelRotationZ = (config.modelRotationZ !== undefined) ? parseFloat(config.modelRotationZ) : 0;
41
-
42
- const cameraX = (config.cameraX !== undefined) ? parseFloat(config.cameraX) : 0;
43
- const cameraY = (config.cameraY !== undefined) ? parseFloat(config.cameraY) : 2;
44
- const cameraZ = (config.cameraZ !== undefined) ? parseFloat(config.cameraZ) : 5;
45
- const cameraXPhone = (config.cameraXPhone !== undefined) ? parseFloat(config.cameraXPhone) : cameraX;
46
- const cameraYPhone = (config.cameraYPhone !== undefined) ? parseFloat(config.cameraYPhone) : cameraY;
47
- const cameraZPhone = (config.cameraZPhone !== undefined) ? parseFloat(config.cameraZPhone) : (cameraZ * 1.5);
48
-
49
- chosenCameraX = isMobile ? cameraXPhone : cameraX;
50
- chosenCameraY = isMobile ? cameraYPhone : cameraY;
51
- chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
52
-
53
- const canvasId = 'canvas-' + instanceId;
54
- const progressDialog = document.getElementById('progress-dialog-' + instanceId);
55
- const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
56
- const viewerContainer = document.getElementById('viewer-container-' + instanceId);
57
-
58
- let oldCanvas = document.getElementById(canvasId);
59
- if (oldCanvas) oldCanvas.remove();
60
-
61
- const canvas = document.createElement('canvas');
62
- canvas.id = canvasId;
63
- canvas.className = 'ply-canvas';
64
- canvas.style.width = "100%";
65
- canvas.style.height = "100%";
66
- canvas.setAttribute('tabindex', '0');
67
- viewerContainer.insertBefore(canvas, progressDialog);
68
-
69
- canvas.style.touchAction = "none";
70
- canvas.style.webkitTouchCallout = "none";
71
- canvas.addEventListener('gesturestart', e => e.preventDefault());
72
- canvas.addEventListener('gesturechange', e => e.preventDefault());
73
- canvas.addEventListener('gestureend', e => e.preventDefault());
74
- canvas.addEventListener('dblclick', e => e.preventDefault());
75
- canvas.addEventListener('touchstart', e => { if (e.touches.length > 1) e.preventDefault(); }, { passive: false });
76
-
77
- progressDialog.style.display = 'block';
78
-
79
- if (!pc) {
80
- pc = await import("https://esm.run/playcanvas");
81
- window.pc = pc;
82
  }
83
 
84
- const device = await pc.createGraphicsDevice(canvas, {
85
- deviceTypes: ["webgl2"],
86
- glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
87
- twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
88
- antialias: false
89
- });
90
- device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
91
-
92
- const opts = new pc.AppOptions();
93
- opts.graphicsDevice = device;
94
- opts.mouse = new pc.Mouse(document.body);
95
- opts.touch = new pc.TouchDevice(document.body);
96
- opts.componentSystems = [
97
- pc.RenderComponentSystem,
98
- pc.CameraComponentSystem,
99
- pc.LightComponentSystem,
100
- pc.ScriptComponentSystem,
101
- pc.GSplatComponentSystem,
102
- pc.CollisionComponentSystem,
103
- pc.RigidbodyComponentSystem
104
- ];
105
- opts.resourceHandlers = [
106
- pc.TextureHandler,
107
- pc.ContainerHandler,
108
- pc.ScriptHandler,
109
- pc.GSplatHandler
110
- ];
111
-
112
- app = new pc.Application(canvas, opts);
113
- app.setCanvasFillMode(pc.FILLMODE_NONE);
114
- app.setCanvasResolution(pc.RESOLUTION_AUTO);
115
-
116
- resizeObserver = new ResizeObserver(entries => {
117
- entries.forEach(entry => {
118
- app.resizeCanvas(entry.contentRect.width, entry.contentRect.height);
119
- });
120
- });
121
- resizeObserver.observe(viewerContainer);
122
-
123
- window.addEventListener('resize', () => app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight));
124
- app.on('destroy', () => resizeObserver.disconnect());
125
-
126
- // --- Assets: Add GLB and HDR support ---
127
- const assets = {
128
- model: new pc.Asset('gsplat', 'gsplat', { url: plyUrl }),
129
- orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
130
- galerie: glbUrl ? new pc.Asset('galerie', 'container', { url: glbUrl }) : null,
131
- hdr: new pc.Asset('hdr', 'texture', { url: "https://huggingface.co/datasets/bilca/ply_files/resolve/main/galeries/blanc.png" }, { type: pc.TEXTURETYPE_RGBP, mipmaps: false })
132
- };
133
- const assetArray = Object.values(assets).filter(a => !!a);
134
-
135
- const loader = new pc.AssetListLoader(assetArray, app.assets);
136
- loader.load(async () => {
137
- app.start();
138
- progressDialog.style.display = 'none';
139
-
140
- // Add GSplat (PLY) first
141
- modelEntity = new pc.Entity('model');
142
- modelEntity.addComponent('gsplat', { asset: assets.model });
143
- modelEntity.setLocalPosition(modelX, modelY, modelZ);
144
- modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
145
- modelEntity.setLocalScale(modelScale, modelScale, modelScale);
146
- app.root.addChild(modelEntity);
147
-
148
- // Then add GLB gallery (if available)
149
- if (assets.galerie && assets.galerie.resource && assets.galerie.resource.instantiateRenderEntity) {
150
- galleryEntity = assets.galerie.resource.instantiateRenderEntity();
151
- app.root.addChild(galleryEntity);
 
152
  }
153
 
154
- // Set HDR environment (after app.start)
155
- if (assets.hdr && assets.hdr.resource) {
156
- app.scene.envAtlas = assets.hdr.resource;
 
 
 
 
 
 
 
 
157
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
- // Camera
160
- cameraEntity = new pc.Entity('camera');
161
- cameraEntity.addComponent('camera', { clearColor: new pc.Color(0.2, 0.2, 0.2, 1) });
162
- cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
163
- cameraEntity.lookAt(modelEntity.getPosition());
164
- cameraEntity.addComponent('script');
165
- cameraEntity.script.create('orbitCamera', {
166
- attributes: {
167
- focusEntity: modelEntity,
168
- inertiaFactor: 0.2,
169
- distanceMax: maxZoom,
170
- distanceMin: minZoom,
171
- pitchAngleMax: maxAngle,
172
- pitchAngleMin: minAngle,
173
- yawAngleMax: maxAzimuth,
174
- yawAngleMin: minAzimuth,
175
- minPivotY: minPivotY,
176
- frameOnStart: false
177
- }
 
 
 
 
 
 
178
  });
179
- cameraEntity.script.create('orbitCameraInputMouse');
180
- cameraEntity.script.create('orbitCameraInputTouch');
181
- app.root.addChild(cameraEntity);
182
 
183
- app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
- // Use Script B's robust camera reset logic after everything is ready
186
- app.once('update', () => resetViewerCamera());
 
 
 
 
 
 
 
 
 
 
187
 
188
- // Tooltips (if config.tooltips_url and tooltips.js available)
189
- try {
190
- if (config.tooltips_url) {
191
- const tooltipsModule = await import('./tooltips.js');
192
- tooltipsModule.initializeTooltips({
193
- app,
194
- cameraEntity,
195
- modelEntity,
196
- tooltipsUrl: config.tooltips_url,
197
- defaultVisible: !!config.showTooltipsDefault,
198
- moveDuration: config.tooltipMoveDuration || 0.6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  });
 
 
 
 
 
 
200
  }
201
- } catch (e) {
202
- // Tooltips optional, fail silently
203
- }
204
 
205
- viewerInitialized = true;
206
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  }
208
 
209
  // Camera reset logic from Script B
210
  export function resetViewerCamera() {
211
  try {
212
- if (!cameraEntity || !modelEntity || !app) return;
 
 
 
213
  const orbitCam = cameraEntity.script.orbitCamera;
214
- if (!orbitCam) return;
 
 
 
215
 
216
  const modelPos = modelEntity.getPosition();
217
  const tempEnt = new pc.Entity();
@@ -248,21 +332,28 @@ export function resetViewerCamera() {
248
  if (orbitCam._updatePosition) orbitCam._updatePosition();
249
 
250
  tempEnt.destroy();
 
251
  } catch (e) {
 
252
  // Silent fail
253
  }
254
  }
255
 
256
  export function cleanupViewer() {
257
- if (app) {
258
- try { app.destroy(); } catch {}
259
- app = null;
260
- }
261
- cameraEntity = null;
262
- modelEntity = null;
263
- viewerInitialized = false;
264
- if (resizeObserver) {
265
- resizeObserver.disconnect();
266
- resizeObserver = null;
 
 
 
 
 
267
  }
268
  }
 
14
  let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRotationZ;
15
  let plyUrl, glbUrl;
16
 
17
+ // Utility: log and alert (for iPhone debug)
18
+ function safeLog(msg) {
19
+ if (typeof msg !== 'string') {
20
+ try { msg = JSON.stringify(msg); } catch {}
21
+ }
22
+ console.log(msg);
23
+ try { alert(msg); } catch {}
24
+ }
25
+
26
  export async function initializeViewer(config, instanceId) {
27
+ if (viewerInitialized) {
28
+ safeLog("Viewer already initialized");
29
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  }
31
 
32
+ try {
33
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
34
+ const isMobile = isIOS || /Android/i.test(navigator.userAgent);
35
+
36
+ plyUrl = config.ply_url;
37
+ glbUrl = config.glb_url;
38
+ minZoom = parseFloat(config.minZoom || "1");
39
+ maxZoom = parseFloat(config.maxZoom || "20");
40
+ minAngle = parseFloat(config.minAngle || "-45");
41
+ maxAngle = parseFloat(config.maxAngle || "90");
42
+ minAzimuth = (config.minAzimuth !== undefined) ? parseFloat(config.minAzimuth) : -360;
43
+ maxAzimuth = (config.maxAzimuth !== undefined) ? parseFloat(config.maxAzimuth) : 360;
44
+ minPivotY = parseFloat(config.minPivotY || "0");
45
+ minY = (config.minY !== undefined) ? parseFloat(config.minY) : 0;
46
+
47
+ modelX = (config.modelX !== undefined) ? parseFloat(config.modelX) : 0;
48
+ modelY = (config.modelY !== undefined) ? parseFloat(config.modelY) : 0;
49
+ modelZ = (config.modelZ !== undefined) ? parseFloat(config.modelZ) : 0;
50
+ modelScale = (config.modelScale !== undefined) ? parseFloat(config.modelScale) : 1;
51
+ modelRotationX = (config.modelRotationX !== undefined) ? parseFloat(config.modelRotationX) : 0;
52
+ modelRotationY = (config.modelRotationY !== undefined) ? parseFloat(config.modelRotationY) : 0;
53
+ modelRotationZ = (config.modelRotationZ !== undefined) ? parseFloat(config.modelRotationZ) : 0;
54
+
55
+ const cameraX = (config.cameraX !== undefined) ? parseFloat(config.cameraX) : 0;
56
+ const cameraY = (config.cameraY !== undefined) ? parseFloat(config.cameraY) : 2;
57
+ const cameraZ = (config.cameraZ !== undefined) ? parseFloat(config.cameraZ) : 5;
58
+ const cameraXPhone = (config.cameraXPhone !== undefined) ? parseFloat(config.cameraXPhone) : cameraX;
59
+ const cameraYPhone = (config.cameraYPhone !== undefined) ? parseFloat(config.cameraYPhone) : cameraY;
60
+ const cameraZPhone = (config.cameraZPhone !== undefined) ? parseFloat(config.cameraZPhone) : (cameraZ * 1.5);
61
+
62
+ chosenCameraX = isMobile ? cameraXPhone : cameraX;
63
+ chosenCameraY = isMobile ? cameraYPhone : cameraY;
64
+ chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
65
+
66
+ const canvasId = 'canvas-' + instanceId;
67
+ const progressDialog = document.getElementById('progress-dialog-' + instanceId);
68
+ const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
69
+ const viewerContainer = document.getElementById('viewer-container-' + instanceId);
70
+
71
+ let oldCanvas = document.getElementById(canvasId);
72
+ if (oldCanvas) oldCanvas.remove();
73
+
74
+ const canvas = document.createElement('canvas');
75
+ canvas.id = canvasId;
76
+ canvas.className = 'ply-canvas';
77
+ canvas.style.width = "100%";
78
+ canvas.style.height = "100%";
79
+ canvas.setAttribute('tabindex', '0');
80
+ viewerContainer.insertBefore(canvas, progressDialog);
81
+
82
+ canvas.style.touchAction = "none";
83
+ canvas.style.webkitTouchCallout = "none";
84
+ canvas.addEventListener('gesturestart', e => e.preventDefault());
85
+ canvas.addEventListener('gesturechange', e => e.preventDefault());
86
+ canvas.addEventListener('gestureend', e => e.preventDefault());
87
+ canvas.addEventListener('dblclick', e => e.preventDefault());
88
+ canvas.addEventListener('touchstart', e => { if (e.touches.length > 1) e.preventDefault(); }, { passive: false });
89
+
90
+ progressDialog.style.display = 'block';
91
+
92
+ if (!pc) {
93
+ try {
94
+ pc = await import("https://esm.run/playcanvas");
95
+ window.pc = pc;
96
+ safeLog("PlayCanvas loaded!");
97
+ } catch (e) {
98
+ safeLog("PlayCanvas import failed: " + e);
99
+ throw e;
100
+ }
101
  }
102
 
103
+ let device;
104
+ try {
105
+ device = await pc.createGraphicsDevice(canvas, {
106
+ deviceTypes: ["webgl2"],
107
+ glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
108
+ twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
109
+ antialias: false
110
+ });
111
+ } catch (e) {
112
+ safeLog("createGraphicsDevice failed: " + e);
113
+ throw e;
114
  }
115
+ device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
116
+
117
+ const opts = new pc.AppOptions();
118
+ opts.graphicsDevice = device;
119
+ opts.mouse = new pc.Mouse(document.body);
120
+ opts.touch = new pc.TouchDevice(document.body);
121
+ opts.componentSystems = [
122
+ pc.RenderComponentSystem,
123
+ pc.CameraComponentSystem,
124
+ pc.LightComponentSystem,
125
+ pc.ScriptComponentSystem,
126
+ pc.GSplatComponentSystem,
127
+ pc.CollisionComponentSystem,
128
+ pc.RigidbodyComponentSystem
129
+ ];
130
+ opts.resourceHandlers = [
131
+ pc.TextureHandler,
132
+ pc.ContainerHandler,
133
+ pc.ScriptHandler,
134
+ pc.GSplatHandler
135
+ ];
136
+
137
+ app = new pc.Application(canvas, opts);
138
+ app.setCanvasFillMode(pc.FILLMODE_NONE);
139
+ app.setCanvasResolution(pc.RESOLUTION_AUTO);
140
+
141
+ resizeObserver = new ResizeObserver(entries => {
142
+ entries.forEach(entry => {
143
+ app.resizeCanvas(entry.contentRect.width, entry.contentRect.height);
144
+ });
145
+ });
146
+ resizeObserver.observe(viewerContainer);
147
 
148
+ window.addEventListener('resize', () => app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight));
149
+ app.on('destroy', () => resizeObserver.disconnect());
150
+
151
+ // --- Assets: Add GLB and HDR support ---
152
+ const assets = {
153
+ model: new pc.Asset('gsplat', 'gsplat', { url: plyUrl }),
154
+ orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
155
+ galerie: glbUrl ? new pc.Asset('galerie', 'container', { url: glbUrl }) : null,
156
+ hdr: new pc.Asset('hdr', 'texture', { url: "https://huggingface.co/datasets/bilca/ply_files/resolve/main/galeries/blanc.png" }, { type: pc.TEXTURETYPE_RGBP, mipmaps: false })
157
+ };
158
+ const assetArray = Object.values(assets).filter(a => !!a);
159
+
160
+ const loader = new pc.AssetListLoader(assetArray, app.assets);
161
+
162
+ // Asset error handling
163
+ assets.model.on('error', err => {
164
+ safeLog("GSplat model failed: " + err);
165
+ });
166
+ if (assets.galerie) {
167
+ assets.galerie.on('error', err => {
168
+ safeLog("GLB failed: " + err);
169
+ });
170
+ }
171
+ assets.hdr.on('error', err => {
172
+ safeLog("HDR failed: " + err);
173
  });
 
 
 
174
 
175
+ loader.load(async () => {
176
+ safeLog("Assets loaded, starting app...");
177
+ app.start();
178
+ progressDialog.style.display = 'none';
179
+
180
+ // Add GSplat (PLY) first
181
+ try {
182
+ modelEntity = new pc.Entity('model');
183
+ modelEntity.addComponent('gsplat', { asset: assets.model });
184
+ modelEntity.setLocalPosition(modelX, modelY, modelZ);
185
+ modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
186
+ modelEntity.setLocalScale(modelScale, modelScale, modelScale);
187
+ app.root.addChild(modelEntity);
188
+ safeLog("PLY/GSplat added");
189
+ } catch (e) {
190
+ safeLog("Failed to add PLY/GSplat: " + e);
191
+ }
192
 
193
+ // Then add GLB gallery (if available)
194
+ try {
195
+ if (assets.galerie && assets.galerie.resource && assets.galerie.resource.instantiateRenderEntity) {
196
+ galleryEntity = assets.galerie.resource.instantiateRenderEntity();
197
+ app.root.addChild(galleryEntity);
198
+ safeLog("GLB gallery added to scene");
199
+ } else if (assets.galerie) {
200
+ safeLog("GLB asset loaded, but no instantiateRenderEntity");
201
+ }
202
+ } catch (e) {
203
+ safeLog("Failed to add GLB gallery: " + e);
204
+ }
205
 
206
+ // Set HDR environment (after app.start)
207
+ try {
208
+ if (assets.hdr && assets.hdr.resource) {
209
+ app.scene.envAtlas = assets.hdr.resource;
210
+ safeLog("HDR environment applied");
211
+ }
212
+ } catch (e) {
213
+ safeLog("Failed to set HDR environment: " + e);
214
+ }
215
+
216
+ // Camera
217
+ try {
218
+ cameraEntity = new pc.Entity('camera');
219
+ cameraEntity.addComponent('camera', { clearColor: new pc.Color(0.2, 0.2, 0.2, 1) });
220
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
221
+ cameraEntity.lookAt(modelEntity.getPosition());
222
+ cameraEntity.addComponent('script');
223
+ cameraEntity.script.create('orbitCamera', {
224
+ attributes: {
225
+ focusEntity: modelEntity,
226
+ inertiaFactor: 0.2,
227
+ distanceMax: maxZoom,
228
+ distanceMin: minZoom,
229
+ pitchAngleMax: maxAngle,
230
+ pitchAngleMin: minAngle,
231
+ yawAngleMax: maxAzimuth,
232
+ yawAngleMin: minAzimuth,
233
+ minPivotY: minPivotY,
234
+ frameOnStart: false
235
+ }
236
  });
237
+ cameraEntity.script.create('orbitCameraInputMouse');
238
+ cameraEntity.script.create('orbitCameraInputTouch');
239
+ app.root.addChild(cameraEntity);
240
+ safeLog("Camera set up");
241
+ } catch (e) {
242
+ safeLog("Failed to set up camera: " + e);
243
  }
 
 
 
244
 
245
+ app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
246
+
247
+ // Use Script B's robust camera reset logic after everything is ready
248
+ app.once('update', () => {
249
+ try {
250
+ resetViewerCamera();
251
+ safeLog("Camera reset after init");
252
+ } catch (e) {
253
+ safeLog("Camera reset failed: " + e);
254
+ }
255
+ });
256
+
257
+ // Tooltips (if config.tooltips_url and tooltips.js available)
258
+ try {
259
+ if (config.tooltips_url) {
260
+ const tooltipsModule = await import('./tooltips.js');
261
+ tooltipsModule.initializeTooltips({
262
+ app,
263
+ cameraEntity,
264
+ modelEntity,
265
+ tooltipsUrl: config.tooltips_url,
266
+ defaultVisible: !!config.showTooltipsDefault,
267
+ moveDuration: config.tooltipMoveDuration || 0.6
268
+ });
269
+ safeLog("Tooltips loaded and initialized");
270
+ }
271
+ } catch (e) {
272
+ safeLog("Tooltips load/init error: " + e);
273
+ }
274
+
275
+ viewerInitialized = true;
276
+ safeLog("Viewer initialized OK");
277
+ });
278
+ } catch (err) {
279
+ safeLog("Fatal init error: " + err);
280
+ const progressDialog = document.getElementById('progress-dialog-' + instanceId);
281
+ if (progressDialog) {
282
+ progressDialog.innerHTML = `<p style="color: red">Error loading viewer: ${err && err.message ? err.message : err}</p>`;
283
+ }
284
+ }
285
  }
286
 
287
  // Camera reset logic from Script B
288
  export function resetViewerCamera() {
289
  try {
290
+ if (!cameraEntity || !modelEntity || !app) {
291
+ safeLog("resetViewerCamera: missing entity or app");
292
+ return;
293
+ }
294
  const orbitCam = cameraEntity.script.orbitCamera;
295
+ if (!orbitCam) {
296
+ safeLog("resetViewerCamera: orbitCamera script missing");
297
+ return;
298
+ }
299
 
300
  const modelPos = modelEntity.getPosition();
301
  const tempEnt = new pc.Entity();
 
332
  if (orbitCam._updatePosition) orbitCam._updatePosition();
333
 
334
  tempEnt.destroy();
335
+ safeLog("Camera reset complete");
336
  } catch (e) {
337
+ safeLog("resetViewerCamera exception: " + e);
338
  // Silent fail
339
  }
340
  }
341
 
342
  export function cleanupViewer() {
343
+ try {
344
+ if (app) {
345
+ try { app.destroy(); } catch (e) { safeLog("App destroy error: " + e); }
346
+ app = null;
347
+ }
348
+ cameraEntity = null;
349
+ modelEntity = null;
350
+ viewerInitialized = false;
351
+ if (resizeObserver) {
352
+ resizeObserver.disconnect();
353
+ resizeObserver = null;
354
+ }
355
+ safeLog("Viewer cleaned up");
356
+ } catch (e) {
357
+ safeLog("cleanupViewer error: " + e);
358
  }
359
  }