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

Update viewer.js

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