MikaFil commited on
Commit
9b4125c
·
verified ·
1 Parent(s): 66cc1a4

Update viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +96 -76
viewer.js CHANGED
@@ -1,70 +1,68 @@
1
  // viewer.js
2
 
3
- let pc; // will hold the PlayCanvas module once imported
4
  export let app = null;
5
- let cameraEntity = null;
6
- let modelEntity = null;
7
  let viewerInitialized = false;
8
  let resizeObserver = null;
 
9
 
10
  let chosenCameraX, chosenCameraY, chosenCameraZ;
11
  let minZoom, maxZoom, minAngle, maxAngle, minAzimuth, maxAzimuth, minPivotY, minY;
12
  let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRotationZ;
13
  let plyUrl, glbUrl;
14
 
15
- // iOS & Safari detection
16
  function isIOS() {
17
  return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
18
  }
19
- function isSafari() {
20
- return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
 
21
  }
22
 
23
- /**
24
- * initializeViewer(config, instanceId)
25
- */
26
  export async function initializeViewer(config, instanceId) {
27
  if (viewerInitialized) return;
28
 
29
- // 1. Read config
30
  plyUrl = config.ply_url;
31
  glbUrl = config.glb_url;
32
- minZoom = parseFloat(config.minZoom || "1");
33
- maxZoom = parseFloat(config.maxZoom || "20");
34
- minAngle = parseFloat(config.minAngle || "-45");
35
- maxAngle = parseFloat(config.maxAngle || "90");
36
- minAzimuth = (config.minAzimuth !== undefined) ? parseFloat(config.minAzimuth) : -360;
37
- maxAzimuth = (config.maxAzimuth !== undefined) ? parseFloat(config.maxAzimuth) : 360;
38
- minPivotY = parseFloat(config.minPivotY || "0");
39
- minY = (config.minY !== undefined) ? parseFloat(config.minY) : 0;
40
-
41
- modelX = (config.modelX !== undefined) ? parseFloat(config.modelX) : 0;
42
- modelY = (config.modelY !== undefined) ? parseFloat(config.modelY) : 0;
43
- modelZ = (config.modelZ !== undefined) ? parseFloat(config.modelZ) : 0;
44
- modelScale = (config.modelScale !== undefined) ? parseFloat(config.modelScale) : 1;
45
- modelRotationX = (config.modelRotationX !== undefined) ? parseFloat(config.modelRotationX) : 0;
46
- modelRotationY = (config.modelRotationY !== undefined) ? parseFloat(config.modelRotationY) : 0;
47
- modelRotationZ = (config.modelRotationZ !== undefined) ? parseFloat(config.modelRotationZ) : 0;
48
-
49
- const cameraX = (config.cameraX !== undefined) ? parseFloat(config.cameraX) : 0;
50
- const cameraY = (config.cameraY !== undefined) ? parseFloat(config.cameraY) : 2;
51
- const cameraZ = (config.cameraZ !== undefined) ? parseFloat(config.cameraZ) : 5;
52
- const cameraXPhone = (config.cameraXPhone !== undefined) ? parseFloat(config.cameraXPhone) : cameraX;
53
- const cameraYPhone = (config.cameraYPhone !== undefined) ? parseFloat(config.cameraYPhone) : cameraY;
54
- const cameraZPhone = (config.cameraZPhone !== undefined) ? parseFloat(config.cameraZPhone) : (cameraZ * 1.5);
55
 
56
  const mobile = isIOS() || /Android/i.test(navigator.userAgent);
57
  chosenCameraX = mobile ? cameraXPhone : cameraX;
58
  chosenCameraY = mobile ? cameraYPhone : cameraY;
59
  chosenCameraZ = mobile ? cameraZPhone : cameraZ;
60
 
61
- // 2. Grab DOM
62
- const canvasId = 'canvas-' + instanceId;
63
- const progressDialog = document.getElementById('progress-dialog-' + instanceId);
64
  const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
65
- const viewerContainer = document.getElementById('viewer-container-' + instanceId);
66
 
67
- // 3. Create <canvas>
68
  let oldCanvas = document.getElementById(canvasId);
69
  if (oldCanvas) oldCanvas.remove();
70
  const canvas = document.createElement('canvas');
@@ -73,7 +71,6 @@ export async function initializeViewer(config, instanceId) {
73
  canvas.style.zIndex = "1";
74
  viewerContainer.insertBefore(canvas, progressDialog);
75
 
76
- // 4. Wheel listener
77
  canvas.addEventListener('wheel', e => {
78
  if (cameraEntity && cameraEntity.script && cameraEntity.script.orbitCamera) {
79
  const orbitCam = cameraEntity.script.orbitCamera;
@@ -90,25 +87,24 @@ export async function initializeViewer(config, instanceId) {
90
 
91
  progressDialog.style.display = 'block';
92
 
93
- // 5. Import PlayCanvas
94
  if (!pc) {
95
  pc = await import("https://esm.run/playcanvas");
96
  window.pc = pc;
97
- alert('[viewer.js] PlayCanvas module loaded.');
98
  }
99
 
100
  try {
101
- // 6. Setup device & app
102
- alert('[viewer.js] Creating graphics device...');
103
  const device = await pc.createGraphicsDevice(canvas, {
104
  deviceTypes: ["webgl2"],
105
- glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
106
- twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
107
- antialias: false,
108
- preserveDrawingBuffer: isIOS() || isSafari() ? true : false,
109
  alpha: false
110
  });
111
- alert('[viewer.js] Graphics device created!');
112
  device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
113
 
114
  const opts = new pc.AppOptions();
@@ -135,9 +131,8 @@ export async function initializeViewer(config, instanceId) {
135
  app.setCanvasFillMode(pc.FILLMODE_NONE);
136
  app.setCanvasResolution(pc.RESOLUTION_AUTO);
137
 
138
- alert('[viewer.js] PlayCanvas application created!');
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;
@@ -155,7 +150,7 @@ export async function initializeViewer(config, instanceId) {
155
  window.removeEventListener('resize', resizeCanvas);
156
  });
157
 
158
- // 7. Assets
159
  const assets = {
160
  model: new pc.Asset('gsplat', 'gsplat', { url: plyUrl }),
161
  orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
@@ -167,12 +162,30 @@ export async function initializeViewer(config, instanceId) {
167
 
168
  const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
169
  let lastProg = 0;
 
 
 
 
 
 
 
 
 
 
170
  assets.model.on('load', () => {
 
171
  progressDialog.style.display = 'none';
172
- alert('[viewer.js] PLY/GSplat asset loaded!');
 
 
 
 
 
173
  });
 
174
  assets.model.on('error', err => {
175
- alert('[viewer.js] Error loading model: ' + err);
 
176
  progressDialog.innerHTML = `<p style="color: red">Error loading model: ${err}</p>`;
177
  });
178
 
@@ -188,18 +201,23 @@ export async function initializeViewer(config, instanceId) {
188
  }, 100);
189
 
190
  loader.load(async () => {
191
- alert('[viewer.js] All assets loaded; starting app.');
 
192
  app.start();
193
  app.scene.envAtlas = assets.hdr.resource;
194
 
195
  // Model entity
196
  modelEntity = new pc.Entity('model');
197
- modelEntity.addComponent('gsplat', { asset: assets.model });
198
- modelEntity.setLocalPosition(modelX, modelY, modelZ);
199
- modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
200
- modelEntity.setLocalScale(modelScale, modelScale, modelScale);
201
- app.root.addChild(modelEntity);
202
- alert('[viewer.js] Model entity added to scene!');
 
 
 
 
203
 
204
  // Light
205
  const dirLight = new pc.Entity('Cascaded Light');
@@ -224,9 +242,13 @@ export async function initializeViewer(config, instanceId) {
224
 
225
  // Gallery GLB
226
  if (assets.galerie && assets.galerie.resource) {
227
- const galleryEntity = assets.galerie.resource.instantiateRenderEntity();
228
- app.root.addChild(galleryEntity);
229
- alert('[viewer.js] GLB entity added to scene!');
 
 
 
 
230
  }
231
 
232
  // Camera setup
@@ -270,11 +292,11 @@ export async function initializeViewer(config, instanceId) {
270
  }
271
  });
272
  app.root.addChild(cameraEntity);
273
- alert('[viewer.js] Camera entity added to scene!');
274
 
275
  // Reset & constrain updates
276
  app.once('update', () => {
277
- alert('[viewer.js] First app update! Resetting camera.');
278
  resetViewerCamera();
279
  });
280
  app.on('update', dt => {
@@ -284,10 +306,9 @@ export async function initializeViewer(config, instanceId) {
284
  cameraEntity.setPosition(pos.x, minY, pos.z);
285
  }
286
  }
287
- // Diagnostics: Only show once
288
  if (!window._pcFirstUpdate) {
289
  window._pcFirstUpdate = true;
290
- alert('[viewer.js] Entered update/render loop!');
291
  }
292
  });
293
 
@@ -311,12 +332,12 @@ export async function initializeViewer(config, instanceId) {
311
 
312
  progressDialog.style.display = 'none';
313
  viewerInitialized = true;
314
- alert('[viewer.js] Viewer fully initialized and running!');
315
  });
316
 
317
  } catch (error) {
318
- alert("[viewer.js] Error initializing PlayCanvas viewer: " + error.message);
319
- console.error("Error initializing PlayCanvas viewer:", error);
320
  progressDialog.innerHTML = `<p style="color: red">Error loading viewer: ${error.message}</p>`;
321
  }
322
  }
@@ -342,7 +363,7 @@ export function resetViewerCamera() {
342
 
343
  orbitCam.pivotPoint = modelPos.clone();
344
  orbitCam._targetDistance = dist;
345
- orbitCam._distance = dist;
346
 
347
  const rot = tempEnt.getRotation();
348
  const fwd = new pc.Vec3();
@@ -355,10 +376,10 @@ export function resetViewerCamera() {
355
  rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw);
356
  const pitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
357
 
358
- orbitCam._targetYaw = yaw;
359
- orbitCam._yaw = yaw;
360
  orbitCam._targetPitch = pitch;
361
- orbitCam._pitch = pitch;
362
  orbitCam._updatePosition && orbitCam._updatePosition();
363
 
364
  tempEnt.destroy();
@@ -375,10 +396,9 @@ export function cleanupViewer() {
375
  app = null;
376
  }
377
  cameraEntity = null;
378
- modelEntity = null;
379
  viewerInitialized = false;
380
 
381
- // Disconnect the ResizeObserver to avoid leaks
382
  if (resizeObserver) {
383
  resizeObserver.disconnect();
384
  resizeObserver = null;
 
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
  }
20
+ function logAndAlert(msg) {
21
+ console.log(msg);
22
+ if (isIOS()) alert(msg);
23
  }
24
 
 
 
 
25
  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');
 
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;
 
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!');
108
  device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
109
 
110
  const opts = new pc.AppOptions();
 
131
  app.setCanvasFillMode(pc.FILLMODE_NONE);
132
  app.setCanvasResolution(pc.RESOLUTION_AUTO);
133
 
134
+ logAndAlert('[viewer.js] PlayCanvas application created!');
135
 
 
136
  resizeObserver = new ResizeObserver(entries => {
137
  for (const entry of entries) {
138
  const { width, height } = entry.contentRect;
 
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" }),
 
162
 
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
 
 
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');
 
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) {
250
+ logAndAlert('[viewer.js] Error adding GLB entity: ' + e.message);
251
+ }
252
  }
253
 
254
  // Camera setup
 
292
  }
293
  });
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 => {
 
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
 
 
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
  }
 
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();
 
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();
 
396
  app = null;
397
  }
398
  cameraEntity = null;
399
+ modelEntity = null;
400
  viewerInitialized = false;
401
 
 
402
  if (resizeObserver) {
403
  resizeObserver.disconnect();
404
  resizeObserver = null;