MikaFil commited on
Commit
e9e65d1
·
verified ·
1 Parent(s): a75a7dd

Update viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +74 -122
viewer.js CHANGED
@@ -1,68 +1,65 @@
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,6 +68,7 @@ export async function initializeViewer(config, instanceId) {
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,24 +85,20 @@ export async function initializeViewer(config, instanceId) {
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();
@@ -130,9 +124,10 @@ export async function initializeViewer(config, instanceId) {
130
  app = new pc.Application(canvas, opts);
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,7 +145,7 @@ export async function initializeViewer(config, instanceId) {
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,30 +157,9 @@ export async function initializeViewer(config, instanceId) {
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,23 +175,16 @@ export async function initializeViewer(config, instanceId) {
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');
@@ -241,15 +208,8 @@ export async function initializeViewer(config, instanceId) {
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) {
250
- logAndAlert('[viewer.js] Error adding GLB entity: ' + e.message);
251
- }
252
- }
253
 
254
  // Camera setup
255
  cameraEntity = new pc.Entity('camera');
@@ -257,6 +217,7 @@ export async function initializeViewer(config, instanceId) {
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());
@@ -278,8 +239,8 @@ export async function initializeViewer(config, instanceId) {
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) {
@@ -292,13 +253,9 @@ export async function initializeViewer(config, instanceId) {
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 => {
303
  if (cameraEntity) {
304
  const pos = cameraEntity.getPosition();
@@ -306,10 +263,6 @@ export async function initializeViewer(config, instanceId) {
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
@@ -332,12 +285,10 @@ export async function initializeViewer(config, instanceId) {
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,7 +314,7 @@ export function resetViewerCamera() {
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,10 +327,10 @@ export function resetViewerCamera() {
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,9 +347,10 @@ export function cleanupViewer() {
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
 
4
+ let pc; // will hold the PlayCanvas module once imported
5
  export let app = null;
6
+ let cameraEntity = null;
7
+ let modelEntity = null;
8
  let viewerInitialized = false;
9
  let resizeObserver = null;
 
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
+ /**
17
+ * initializeViewer(config, instanceId)
18
+ */
 
 
 
 
 
 
19
  export async function initializeViewer(config, instanceId) {
20
  if (viewerInitialized) return;
21
 
22
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
23
+ const isMobile = isIOS || /Android/i.test(navigator.userAgent);
24
+
25
+ // 1. Read config
26
  plyUrl = config.ply_url;
27
  glbUrl = config.glb_url;
28
+ minZoom = parseFloat(config.minZoom || "1");
29
+ maxZoom = parseFloat(config.maxZoom || "20");
30
+ minAngle = parseFloat(config.minAngle || "-45");
31
+ maxAngle = parseFloat(config.maxAngle || "90");
32
+ minAzimuth = (config.minAzimuth !== undefined) ? parseFloat(config.minAzimuth) : -360;
33
+ maxAzimuth = (config.maxAzimuth !== undefined) ? parseFloat(config.maxAzimuth) : 360;
34
+ minPivotY = parseFloat(config.minPivotY || "0");
35
+ minY = (config.minY !== undefined) ? parseFloat(config.minY) : 0;
36
+
37
+ modelX = (config.modelX !== undefined) ? parseFloat(config.modelX) : 0;
38
+ modelY = (config.modelY !== undefined) ? parseFloat(config.modelY) : 0;
39
+ modelZ = (config.modelZ !== undefined) ? parseFloat(config.modelZ) : 0;
40
+ modelScale = (config.modelScale !== undefined) ? parseFloat(config.modelScale) : 1;
41
+ modelRotationX = (config.modelRotationX !== undefined) ? parseFloat(config.modelRotationX) : 0;
42
+ modelRotationY = (config.modelRotationY !== undefined) ? parseFloat(config.modelRotationY) : 0;
43
+ modelRotationZ = (config.modelRotationZ !== undefined) ? parseFloat(config.modelRotationZ) : 0;
44
+
45
+ const cameraX = (config.cameraX !== undefined) ? parseFloat(config.cameraX) : 0;
46
+ const cameraY = (config.cameraY !== undefined) ? parseFloat(config.cameraY) : 2;
47
+ const cameraZ = (config.cameraZ !== undefined) ? parseFloat(config.cameraZ) : 5;
48
+ const cameraXPhone = (config.cameraXPhone !== undefined) ? parseFloat(config.cameraXPhone) : cameraX;
49
+ const cameraYPhone = (config.cameraYPhone !== undefined) ? parseFloat(config.cameraYPhone) : cameraY;
50
+ const cameraZPhone = (config.cameraZPhone !== undefined) ? parseFloat(config.cameraZPhone) : (cameraZ * 1.5);
51
+
52
+ chosenCameraX = isMobile ? cameraXPhone : cameraX;
53
+ chosenCameraY = isMobile ? cameraYPhone : cameraY;
54
+ chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
55
+
56
+ // 2. Grab DOM
57
+ const canvasId = 'canvas-' + instanceId;
58
+ const progressDialog = document.getElementById('progress-dialog-' + instanceId);
 
59
  const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
60
+ const viewerContainer = document.getElementById('viewer-container-' + instanceId);
61
 
62
+ // 3. Create <canvas>
63
  let oldCanvas = document.getElementById(canvasId);
64
  if (oldCanvas) oldCanvas.remove();
65
  const canvas = document.createElement('canvas');
 
68
  canvas.style.zIndex = "1";
69
  viewerContainer.insertBefore(canvas, progressDialog);
70
 
71
+ // 4. Wheel listener
72
  canvas.addEventListener('wheel', e => {
73
  if (cameraEntity && cameraEntity.script && cameraEntity.script.orbitCamera) {
74
  const orbitCam = cameraEntity.script.orbitCamera;
 
85
 
86
  progressDialog.style.display = 'block';
87
 
88
+ // 5. Import PlayCanvas
89
  if (!pc) {
90
  pc = await import("https://esm.run/playcanvas");
91
  window.pc = pc;
 
92
  }
93
 
94
  try {
95
+ // 6. Setup device & app
96
  const device = await pc.createGraphicsDevice(canvas, {
97
  deviceTypes: ["webgl2"],
98
+ glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
99
+ twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
100
+ antialias: false
 
 
101
  });
 
102
  device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
103
 
104
  const opts = new pc.AppOptions();
 
124
  app = new pc.Application(canvas, opts);
125
  app.setCanvasFillMode(pc.FILLMODE_NONE);
126
  app.setCanvasResolution(pc.RESOLUTION_AUTO);
127
+ //app.scene.exposure = 0.5;
128
+ //app.scene.toneMapping = pc.TONEMAP_ACES;
129
 
130
+ // Attach ResizeObserver to keep canvas in sync with container size
 
131
  resizeObserver = new ResizeObserver(entries => {
132
  for (const entry of entries) {
133
  const { width, height } = entry.contentRect;
 
145
  window.removeEventListener('resize', resizeCanvas);
146
  });
147
 
148
+ // 7. Assets
149
  const assets = {
150
  model: new pc.Asset('gsplat', 'gsplat', { url: plyUrl }),
151
  orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
 
157
 
158
  const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
159
  let lastProg = 0;
160
+ assets.model.on('load', () => progressDialog.style.display = 'none');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  assets.model.on('error', err => {
162
+ console.error("Error loading PLY file:", err);
 
163
  progressDialog.innerHTML = <p style="color: red">Error loading model: ${err}</p>;
164
  });
165
 
 
175
  }, 100);
176
 
177
  loader.load(async () => {
 
 
178
  app.start();
179
  app.scene.envAtlas = assets.hdr.resource;
180
 
181
  // Model entity
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
 
189
  // Light
190
  const dirLight = new pc.Entity('Cascaded Light');
 
208
  app.root.addChild(dirLight);
209
 
210
  // Gallery GLB
211
+ const galleryEntity = assets.galerie.resource.instantiateRenderEntity();
212
+ app.root.addChild(galleryEntity);
 
 
 
 
 
 
 
213
 
214
  // Camera setup
215
  cameraEntity = new pc.Entity('camera');
 
217
  clearColor: config.canvas_background
218
  ? parseInt(config.canvas_background.substr(1, 2), 16) / 255
219
  : 0,
220
+ //toneMapping: pc.TONEMAP_ACES
221
  });
222
  cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
223
  cameraEntity.lookAt(modelEntity.getPosition());
 
239
  });
240
  cameraEntity.script.create('orbitCameraInputMouse', {
241
  attributes: {
242
+ orbitSensitivity: isMobile ? 0.6 : 0.3,
243
+ distanceSensitivity: isMobile ? 0.5 : 0.4
244
  }
245
  });
246
  if (cameraEntity.script.orbitCameraInputMouse) {
 
253
  }
254
  });
255
  app.root.addChild(cameraEntity);
 
256
 
257
  // Reset & constrain updates
258
+ app.once('update', () => resetViewerCamera());
 
 
 
259
  app.on('update', dt => {
260
  if (cameraEntity) {
261
  const pos = cameraEntity.getPosition();
 
263
  cameraEntity.setPosition(pos.x, minY, pos.z);
264
  }
265
  }
 
 
 
 
266
  });
267
 
268
  // Final resize
 
285
 
286
  progressDialog.style.display = 'none';
287
  viewerInitialized = true;
 
288
  });
289
 
290
  } catch (error) {
291
+ console.error("Error initializing PlayCanvas viewer:", error);
 
292
  progressDialog.innerHTML = <p style="color: red">Error loading viewer: ${error.message}</p>;
293
  }
294
  }
 
314
 
315
  orbitCam.pivotPoint = modelPos.clone();
316
  orbitCam._targetDistance = dist;
317
+ orbitCam._distance = dist;
318
 
319
  const rot = tempEnt.getRotation();
320
  const fwd = new pc.Vec3();
 
327
  rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw);
328
  const pitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
329
 
330
+ orbitCam._targetYaw = yaw;
331
+ orbitCam._yaw = yaw;
332
  orbitCam._targetPitch = pitch;
333
+ orbitCam._pitch = pitch;
334
  orbitCam._updatePosition && orbitCam._updatePosition();
335
 
336
  tempEnt.destroy();
 
347
  app = null;
348
  }
349
  cameraEntity = null;
350
+ modelEntity = null;
351
  viewerInitialized = false;
352
 
353
+ // Disconnect the ResizeObserver to avoid leaks
354
  if (resizeObserver) {
355
  resizeObserver.disconnect();
356
  resizeObserver = null;