MikaFil commited on
Commit
caacb2c
·
verified ·
1 Parent(s): 116dd91

Update viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +243 -125
viewer.js CHANGED
@@ -1,18 +1,17 @@
1
- // viewer.js
2
-
3
- let pc; // PlayCanvas, for GLB only
4
  export let app = null;
5
  let cameraEntity = null;
6
  let modelEntity = null;
7
  let viewerInitialized = false;
8
  let resizeObserver = null;
 
9
 
10
- let splatRenderer = null;
11
- let splatScene = null;
12
- let splatCamera = null;
13
- let splatControls = null;
14
- let splatFrame = null;
15
 
 
16
  function isIOS() {
17
  return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
18
  }
@@ -25,111 +24,82 @@ export async function initializeViewer(config, instanceId) {
25
  if (viewerInitialized) return;
26
 
27
  // Config
28
- const plyUrl = config.ply_url;
29
- const glbUrl = config.glb_url;
30
-
31
- // Use SPLAT for PLY on iOS or if file is >40MB, PlayCanvas otherwise
32
- let useSplat = false;
33
- if (plyUrl && isIOS()) useSplat = true;
34
-
35
- // --- SPLAT Renderer Fallback for PLY on iOS ---
36
- if (useSplat && plyUrl) {
37
- try {
38
- logAndAlert('[viewer.js] Using SPLAT renderer for PLY on iOS.');
39
- const SPLAT = await import("https://bilca-gsplat-library.static.hf.space/dist/index.js");
40
- const canvasId = 'canvas-' + instanceId;
41
- const progressDialog = document.getElementById('progress-dialog-' + instanceId);
42
- const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
43
- const viewerContainer = document.getElementById('viewer-container-' + instanceId);
44
- const canvas = document.getElementById(canvasId);
45
-
46
- progressDialog.style.display = 'block';
47
-
48
- splatRenderer = new SPLAT.WebGLRenderer(canvas);
49
- splatScene = new SPLAT.Scene();
50
- splatCamera = new SPLAT.Camera();
51
-
52
- // Camera/orbit config
53
- const isMobile = isIOS() || /Android/i.test(navigator.userAgent);
54
- const initAlpha = isMobile && config.initAlphaPhone ? parseFloat(config.initAlphaPhone) : parseFloat(config.initAlpha || 0.5);
55
- const initBeta = isMobile && config.initBetaPhone ? parseFloat(config.initBetaPhone) : parseFloat(config.initBeta || 0.5);
56
- const initRadius = isMobile && config.initRadiusPhone ? parseFloat(config.initRadiusPhone) : parseFloat(config.initRadius || 5);
57
-
58
- splatControls = new SPLAT.OrbitControls(
59
- splatCamera,
60
- canvas,
61
- 0.5, 0.5, 5,
62
- true,
63
- new SPLAT.Vector3(),
64
- initAlpha, initBeta, initRadius
65
- );
66
- splatControls.maxZoom = parseFloat(config.maxZoom || 20);
67
- splatControls.minZoom = parseFloat(config.minZoom || 0);
68
- splatControls.minAngle = parseFloat(config.minAngle || 0);
69
- splatControls.maxAngle = parseFloat(config.maxAngle || 360);
70
- splatControls.panSpeed = isMobile ? 0.5 : 1.2;
71
-
72
- // Load PLY (with progress)
73
- await SPLAT.PLYLoader.LoadAsync(
74
- plyUrl,
75
- splatScene,
76
- (progress) => { progressIndicator.value = progress * 100; }
77
- );
78
- progressDialog.style.display = 'none';
79
-
80
- // Render loop
81
- function frame() {
82
- splatControls.update();
83
- splatRenderer.render(splatScene, splatCamera);
84
- splatFrame = requestAnimationFrame(frame);
85
  }
86
- frame();
87
-
88
- window.addEventListener("resize", () => {
89
- splatRenderer.setSize(canvas.clientWidth, canvas.clientHeight);
90
- });
91
 
92
- viewerInitialized = true;
93
- logAndAlert('[viewer.js] SPLAT viewer initialized!');
94
- return; // Done, do not run PlayCanvas path
95
 
96
- } catch (err) {
97
- alert('[viewer.js] SPLAT loading error: ' + err.message);
98
- return;
99
- }
 
100
  }
101
 
102
- // === PlayCanvas Path (GLB or non-iOS) ===
103
  try {
104
- if (!pc) {
105
- pc = await import("https://esm.run/playcanvas");
106
- window.pc = pc;
107
- logAndAlert('[viewer.js] PlayCanvas module loaded.');
108
- }
109
-
110
- // DOM
111
- const canvasId = 'canvas-' + instanceId;
112
- const progressDialog = document.getElementById('progress-dialog-' + instanceId);
113
- const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
114
- const viewerContainer = document.getElementById('viewer-container-' + instanceId);
115
-
116
- let oldCanvas = document.getElementById(canvasId);
117
- if (oldCanvas) oldCanvas.remove();
118
- const canvas = document.createElement('canvas');
119
- canvas.id = canvasId;
120
- canvas.className = 'ply-canvas';
121
- canvas.style.zIndex = "1";
122
- viewerContainer.insertBefore(canvas, progressDialog);
123
-
124
- progressDialog.style.display = 'block';
125
-
126
  logAndAlert('[viewer.js] Creating graphics device...');
127
  const device = await pc.createGraphicsDevice(canvas, {
128
  deviceTypes: ["webgl2"],
129
  glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
130
  twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
131
  antialias: false,
132
- preserveDrawingBuffer: false,
133
  alpha: false
134
  });
135
  logAndAlert('[viewer.js] Graphics device created!');
@@ -174,29 +144,104 @@ export async function initializeViewer(config, instanceId) {
174
  window.addEventListener('resize', () => {
175
  if (app) app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
176
  });
 
 
 
177
 
178
- // Assets (GLB only)
179
  const assets = {
180
- galerie: new pc.Asset('galerie', 'container', { url: glbUrl }),
181
  orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
 
182
  hdr: new pc.Asset('hdr', 'texture', {
183
- url: `https://huggingface.co/datasets/bilca/ply_files/resolve/main/galeries/blanc.png`
184
  }, { type: pc.TEXTURETYPE_RGBP, mipmaps: false })
185
  };
186
 
187
  const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
188
  let lastProg = 0;
189
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  loader.load(async () => {
 
191
  logAndAlert('[viewer.js] All assets loaded; starting app.');
192
  app.start();
193
  app.scene.envAtlas = assets.hdr.resource;
194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  // Gallery GLB
196
- let galleryEntity = null;
197
  if (assets.galerie && assets.galerie.resource) {
198
  try {
199
- galleryEntity = assets.galerie.resource.instantiateRenderEntity();
200
  app.root.addChild(galleryEntity);
201
  logAndAlert('[viewer.js] GLB entity added to scene!');
202
  } catch (e) {
@@ -211,27 +256,33 @@ export async function initializeViewer(config, instanceId) {
211
  ? parseInt(config.canvas_background.substr(1, 2), 16) / 255
212
  : 0,
213
  });
214
- cameraEntity.setPosition(0, 2, 5);
215
- if (galleryEntity) cameraEntity.lookAt(galleryEntity.getPosition());
216
 
217
  cameraEntity.addComponent('script');
218
  cameraEntity.script.create('orbitCamera', {
219
  attributes: {
220
  inertiaFactor: 0.2,
221
- focusEntity: galleryEntity,
222
- distanceMax: 20,
223
- distanceMin: 1,
224
- pitchAngleMax: 90,
225
- pitchAngleMin: -45,
 
 
 
226
  frameOnStart: false
227
  }
228
  });
229
  cameraEntity.script.create('orbitCameraInputMouse', {
230
  attributes: {
231
- orbitSensitivity: 0.3,
232
- distanceSensitivity: 0.4
233
  }
234
  });
 
 
 
235
  cameraEntity.script.create('orbitCameraInputTouch', {
236
  attributes: {
237
  orbitSensitivity: 0.6,
@@ -241,46 +292,113 @@ export async function initializeViewer(config, instanceId) {
241
  app.root.addChild(cameraEntity);
242
  logAndAlert('[viewer.js] Camera entity added to scene!');
243
 
 
244
  app.once('update', () => {
245
  logAndAlert('[viewer.js] First app update! Resetting camera.');
 
246
  });
247
  app.on('update', dt => {
 
 
 
 
 
 
248
  if (!window._pcFirstUpdate) {
249
  window._pcFirstUpdate = true;
250
  logAndAlert('[viewer.js] Entered update/render loop!');
251
  }
252
  });
253
 
 
254
  app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
255
- viewerInitialized = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  progressDialog.style.display = 'none';
 
257
  logAndAlert('[viewer.js] Viewer fully initialized and running!');
258
  });
259
 
260
  } catch (error) {
 
261
  logAndAlert("[viewer.js] Error initializing PlayCanvas viewer: " + error.message);
262
- const progressDialog = document.getElementById('progress-dialog-' + instanceId);
263
- if (progressDialog) progressDialog.innerHTML = `<p style="color: red">Error loading viewer: ${error.message}</p>`;
264
  }
265
  }
266
 
267
  export function resetViewerCamera() {
268
- // Not implemented for PlayCanvas path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  }
 
270
  export function cleanupViewer() {
271
  if (app) {
272
- try { app.destroy(); } catch {}
 
 
273
  app = null;
274
  }
275
- if (splatFrame) {
276
- cancelAnimationFrame(splatFrame);
277
- splatFrame = null;
278
- }
279
  cameraEntity = null;
280
  modelEntity = null;
281
  viewerInitialized = false;
 
282
  if (resizeObserver) {
283
  resizeObserver.disconnect();
284
  resizeObserver = null;
285
  }
286
- }
 
1
+ let pc; // PlayCanvas
 
 
2
  export let app = null;
3
  let cameraEntity = null;
4
  let modelEntity = null;
5
  let viewerInitialized = false;
6
  let resizeObserver = null;
7
+ let loadTimeout = null; // For iOS load fallback
8
 
9
+ let chosenCameraX, chosenCameraY, chosenCameraZ;
10
+ let minZoom, maxZoom, minAngle, maxAngle, minAzimuth, maxAzimuth, minPivotY, minY;
11
+ let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRotationZ;
12
+ let plyUrl, glbUrl;
 
13
 
14
+ // iOS detection
15
  function isIOS() {
16
  return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
17
  }
 
24
  if (viewerInitialized) return;
25
 
26
  // Config
27
+ plyUrl = config.ply_url;
28
+ glbUrl = config.glb_url;
29
+ minZoom = parseFloat(config.minZoom || "1");
30
+ maxZoom = parseFloat(config.maxZoom || "20");
31
+ minAngle = parseFloat(config.minAngle || "-45");
32
+ maxAngle = parseFloat(config.maxAngle || "90");
33
+ minAzimuth = config.minAzimuth !== undefined ? parseFloat(config.minAzimuth) : -360;
34
+ maxAzimuth = config.maxAzimuth !== undefined ? parseFloat(config.maxAzimuth) : 360;
35
+ minPivotY = parseFloat(config.minPivotY || "0");
36
+ minY = config.minY !== undefined ? parseFloat(config.minY) : 0;
37
+
38
+ modelX = config.modelX !== undefined ? parseFloat(config.modelX) : 0;
39
+ modelY = config.modelY !== undefined ? parseFloat(config.modelY) : 0;
40
+ modelZ = config.modelZ !== undefined ? parseFloat(config.modelZ) : 0;
41
+ modelScale = config.modelScale !== undefined ? parseFloat(config.modelScale) : 1;
42
+ modelRotationX = config.modelRotationX !== undefined ? parseFloat(config.modelRotationX) : 0;
43
+ modelRotationY = config.modelRotationY !== undefined ? parseFloat(config.modelRotationY) : 0;
44
+ modelRotationZ = config.modelRotationZ !== undefined ? parseFloat(config.modelRotationZ) : 0;
45
+
46
+ const cameraX = config.cameraX !== undefined ? parseFloat(config.cameraX) : 0;
47
+ const cameraY = config.cameraY !== undefined ? parseFloat(config.cameraY) : 2;
48
+ const cameraZ = config.cameraZ !== undefined ? parseFloat(config.cameraZ) : 5;
49
+ const cameraXPhone = config.cameraXPhone !== undefined ? parseFloat(config.cameraXPhone) : cameraX;
50
+ const cameraYPhone = config.cameraYPhone !== undefined ? parseFloat(config.cameraYPhone) : cameraY;
51
+ const cameraZPhone = config.cameraZPhone !== undefined ? parseFloat(config.cameraZPhone) : cameraZ * 1.5;
52
+
53
+ const mobile = isIOS() || /Android/i.test(navigator.userAgent);
54
+ chosenCameraX = mobile ? cameraXPhone : cameraX;
55
+ chosenCameraY = mobile ? cameraYPhone : cameraY;
56
+ chosenCameraZ = mobile ? cameraZPhone : cameraZ;
57
+
58
+ // DOM
59
+ const canvasId = 'canvas-' + instanceId;
60
+ const progressDialog = document.getElementById('progress-dialog-' + instanceId);
61
+ const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
62
+ const viewerContainer = document.getElementById('viewer-container-' + instanceId);
63
+
64
+ let oldCanvas = document.getElementById(canvasId);
65
+ if (oldCanvas) oldCanvas.remove();
66
+ const canvas = document.createElement('canvas');
67
+ canvas.id = canvasId;
68
+ canvas.className = 'ply-canvas';
69
+ canvas.style.zIndex = "1";
70
+ viewerContainer.insertBefore(canvas, progressDialog);
71
+
72
+ canvas.addEventListener('wheel', e => {
73
+ if (cameraEntity && cameraEntity.script && cameraEntity.script.orbitCamera) {
74
+ const orbitCam = cameraEntity.script.orbitCamera;
75
+ const sens = (cameraEntity.script.orbitCameraInputMouse?.distanceSensitivity) || 0.4;
76
+ if (cameraEntity.camera.projection === pc.PROJECTION_PERSPECTIVE) {
77
+ orbitCam.distance -= e.deltaY * 0.01 * sens * (orbitCam.distance * 0.1);
78
+ } else {
79
+ orbitCam.orthoHeight -= e.deltaY * 0.01 * sens * (orbitCam.orthoHeight * 0.1);
 
 
 
 
80
  }
81
+ e.preventDefault();
82
+ e.stopPropagation();
83
+ }
84
+ }, { passive: false });
 
85
 
86
+ progressDialog.style.display = 'block';
 
 
87
 
88
+ // PlayCanvas
89
+ if (!pc) {
90
+ pc = await import("https://esm.run/playcanvas");
91
+ window.pc = pc;
92
+ logAndAlert('[viewer.js] PlayCanvas module loaded.');
93
  }
94
 
 
95
  try {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  logAndAlert('[viewer.js] Creating graphics device...');
97
  const device = await pc.createGraphicsDevice(canvas, {
98
  deviceTypes: ["webgl2"],
99
  glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
100
  twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
101
  antialias: false,
102
+ preserveDrawingBuffer: isIOS() ? true : false,
103
  alpha: false
104
  });
105
  logAndAlert('[viewer.js] Graphics device created!');
 
144
  window.addEventListener('resize', () => {
145
  if (app) app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
146
  });
147
+ app.on('destroy', () => {
148
+ window.removeEventListener('resize', resizeCanvas);
149
+ });
150
 
151
+ // Assets
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: new pc.Asset('galerie', 'container', { url: glbUrl }),
156
  hdr: new pc.Asset('hdr', 'texture', {
157
+ url: https://huggingface.co/datasets/bilca/ply_files/resolve/main/galeries/blanc.png
158
  }, { type: pc.TEXTURETYPE_RGBP, mipmaps: false })
159
  };
160
 
161
  const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
162
  let lastProg = 0;
163
 
164
+ // iOS PLY load timeout/fallback
165
+ if (isIOS()) {
166
+ // 90MB is a big PLY!
167
+ loadTimeout = setTimeout(() => {
168
+ alert("⚠️ 3D model loading is taking unusually long on your device. Large PLY files (>40MB) may exceed memory limits on iOS Safari and cannot be shown. Try a smaller model, or use a desktop browser for best results.");
169
+ progressDialog.innerHTML = <p style="color:red">Loading failed or too slow. Try a smaller model, or use a desktop browser.</p>;
170
+ }, 20000); // 20 seconds
171
+ }
172
+
173
+ assets.model.on('load', () => {
174
+ if (loadTimeout) { clearTimeout(loadTimeout); loadTimeout = null; }
175
+ progressDialog.style.display = 'none';
176
+ if (assets.model.resource && assets.model.resource._buffer) {
177
+ let buf = assets.model.resource._buffer;
178
+ logAndAlert([viewer.js] PLY/GSplat asset loaded! Buffer byteLength: ${buf.byteLength || buf.length || 'n/a'});
179
+ } else {
180
+ logAndAlert([viewer.js] PLY/GSplat asset loaded! [NO BUFFER?]);
181
+ }
182
+ });
183
+
184
+ assets.model.on('error', err => {
185
+ if (loadTimeout) { clearTimeout(loadTimeout); loadTimeout = null; }
186
+ logAndAlert('[viewer.js] Error loading model: ' + err);
187
+ progressDialog.innerHTML = <p style="color: red">Error loading model: ${err}</p>;
188
+ });
189
+
190
+ const progCheck = setInterval(() => {
191
+ if (assets.model.resource) {
192
+ progressIndicator.value = 100;
193
+ clearInterval(progCheck);
194
+ progressDialog.style.display = 'none';
195
+ } else if (assets.model.loading) {
196
+ lastProg = Math.min(lastProg + 2, 90);
197
+ progressIndicator.value = lastProg;
198
+ }
199
+ }, 100);
200
+
201
  loader.load(async () => {
202
+ if (loadTimeout) { clearTimeout(loadTimeout); loadTimeout = null; }
203
  logAndAlert('[viewer.js] All assets loaded; starting app.');
204
  app.start();
205
  app.scene.envAtlas = assets.hdr.resource;
206
 
207
+ // Model entity
208
+ modelEntity = new pc.Entity('model');
209
+ try {
210
+ modelEntity.addComponent('gsplat', { asset: assets.model });
211
+ modelEntity.setLocalPosition(modelX, modelY, modelZ);
212
+ modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
213
+ modelEntity.setLocalScale(modelScale, modelScale, modelScale);
214
+ app.root.addChild(modelEntity);
215
+ logAndAlert('[viewer.js] Model entity added to scene!');
216
+ } catch (e) {
217
+ logAndAlert('[viewer.js] Error adding model entity: ' + e.message);
218
+ }
219
+
220
+ // Light
221
+ const dirLight = new pc.Entity('Cascaded Light');
222
+ dirLight.addComponent('light', {
223
+ type: 'directional',
224
+ color: pc.Color.WHITE,
225
+ shadowBias: 0.3,
226
+ normalOffsetBias: 0.2,
227
+ intensity: 1.0,
228
+ soft: true,
229
+ shadowResolution: 4096,
230
+ penumbraSize: 7,
231
+ penumbraFalloff: 1.5,
232
+ shadowSamples: 128,
233
+ shadowBlockerSamples: 16,
234
+ castShadows: true,
235
+ shadowType: pc.SHADOW_PCSS_32F,
236
+ shadowDistance: 1000
237
+ });
238
+ dirLight.setLocalEulerAngles(0, 0, 0);
239
+ app.root.addChild(dirLight);
240
+
241
  // Gallery GLB
 
242
  if (assets.galerie && assets.galerie.resource) {
243
  try {
244
+ const galleryEntity = assets.galerie.resource.instantiateRenderEntity();
245
  app.root.addChild(galleryEntity);
246
  logAndAlert('[viewer.js] GLB entity added to scene!');
247
  } catch (e) {
 
256
  ? parseInt(config.canvas_background.substr(1, 2), 16) / 255
257
  : 0,
258
  });
259
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
260
+ cameraEntity.lookAt(modelEntity.getPosition());
261
 
262
  cameraEntity.addComponent('script');
263
  cameraEntity.script.create('orbitCamera', {
264
  attributes: {
265
  inertiaFactor: 0.2,
266
+ focusEntity: modelEntity,
267
+ distanceMax: maxZoom,
268
+ distanceMin: minZoom,
269
+ pitchAngleMax: maxAngle,
270
+ pitchAngleMin: minAngle,
271
+ yawAngleMax: maxAzimuth,
272
+ yawAngleMin: minAzimuth,
273
+ minPivotY: minPivotY,
274
  frameOnStart: false
275
  }
276
  });
277
  cameraEntity.script.create('orbitCameraInputMouse', {
278
  attributes: {
279
+ orbitSensitivity: mobile ? 0.6 : 0.3,
280
+ distanceSensitivity: mobile ? 0.5 : 0.4
281
  }
282
  });
283
+ if (cameraEntity.script.orbitCameraInputMouse) {
284
+ cameraEntity.script.orbitCameraInputMouse.onMouseWheel = function() {};
285
+ }
286
  cameraEntity.script.create('orbitCameraInputTouch', {
287
  attributes: {
288
  orbitSensitivity: 0.6,
 
292
  app.root.addChild(cameraEntity);
293
  logAndAlert('[viewer.js] Camera entity added to scene!');
294
 
295
+ // Reset & constrain updates
296
  app.once('update', () => {
297
  logAndAlert('[viewer.js] First app update! Resetting camera.');
298
+ resetViewerCamera();
299
  });
300
  app.on('update', dt => {
301
+ if (cameraEntity) {
302
+ const pos = cameraEntity.getPosition();
303
+ if (pos.y < minY) {
304
+ cameraEntity.setPosition(pos.x, minY, pos.z);
305
+ }
306
+ }
307
  if (!window._pcFirstUpdate) {
308
  window._pcFirstUpdate = true;
309
  logAndAlert('[viewer.js] Entered update/render loop!');
310
  }
311
  });
312
 
313
+ // Final resize
314
  app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
315
+
316
+ // Tooltips
317
+ try {
318
+ const tooltipsModule = await import('./tooltips.js');
319
+ tooltipsModule.initializeTooltips({
320
+ app,
321
+ cameraEntity,
322
+ modelEntity,
323
+ tooltipsUrl: config.tooltips_url,
324
+ defaultVisible: !!config.showTooltipsDefault,
325
+ moveDuration: config.tooltipMoveDuration || 0.6
326
+ });
327
+ } catch (e) {
328
+ console.error("Error loading tooltips.js:", e);
329
+ }
330
+
331
  progressDialog.style.display = 'none';
332
+ viewerInitialized = true;
333
  logAndAlert('[viewer.js] Viewer fully initialized and running!');
334
  });
335
 
336
  } catch (error) {
337
+ if (loadTimeout) { clearTimeout(loadTimeout); loadTimeout = null; }
338
  logAndAlert("[viewer.js] Error initializing PlayCanvas viewer: " + error.message);
339
+ progressDialog.innerHTML = <p style="color: red">Error loading viewer: ${error.message}</p>;
 
340
  }
341
  }
342
 
343
  export function resetViewerCamera() {
344
+ try {
345
+ if (!cameraEntity || !modelEntity || !app) return;
346
+ const orbitCam = cameraEntity.script.orbitCamera;
347
+ if (!orbitCam) return;
348
+
349
+ const modelPos = modelEntity.getPosition();
350
+ const tempEnt = new pc.Entity();
351
+ tempEnt.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
352
+ tempEnt.lookAt(modelPos);
353
+
354
+ const dist = new pc.Vec3().sub2(
355
+ new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ),
356
+ modelPos
357
+ ).length();
358
+
359
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
360
+ cameraEntity.lookAt(modelPos);
361
+
362
+ orbitCam.pivotPoint = modelPos.clone();
363
+ orbitCam._targetDistance = dist;
364
+ orbitCam._distance = dist;
365
+
366
+ const rot = tempEnt.getRotation();
367
+ const fwd = new pc.Vec3();
368
+ rot.transformVector(pc.Vec3.FORWARD, fwd);
369
+
370
+ const yaw = Math.atan2(-fwd.x, -fwd.z) * pc.math.RAD_TO_DEG;
371
+ const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);
372
+ const rotNoYaw = new pc.Quat().mul2(yawQuat, rot);
373
+ const fNoYaw = new pc.Vec3();
374
+ rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw);
375
+ const pitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
376
+
377
+ orbitCam._targetYaw = yaw;
378
+ orbitCam._yaw = yaw;
379
+ orbitCam._targetPitch = pitch;
380
+ orbitCam._pitch = pitch;
381
+ orbitCam._updatePosition && orbitCam._updatePosition();
382
+
383
+ tempEnt.destroy();
384
+ } catch (e) {
385
+ console.error("Error resetting camera:", e);
386
+ }
387
  }
388
+
389
  export function cleanupViewer() {
390
  if (app) {
391
+ try {
392
+ app.destroy();
393
+ } catch {}
394
  app = null;
395
  }
 
 
 
 
396
  cameraEntity = null;
397
  modelEntity = null;
398
  viewerInitialized = false;
399
+
400
  if (resizeObserver) {
401
  resizeObserver.disconnect();
402
  resizeObserver = null;
403
  }
404
+ }