MikaFil commited on
Commit
f232316
·
verified ·
1 Parent(s): 47d3c74

Update viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +59 -67
viewer.js CHANGED
@@ -1,7 +1,7 @@
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;
@@ -13,16 +13,17 @@ let minZoom, maxZoom, minAngle, maxAngle, minAzimuth, maxAzimuth, minPivotY, min
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");
@@ -53,37 +54,39 @@ export async function initializeViewer(config, instanceId) {
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');
66
  canvas.id = canvasId;
67
  canvas.className = 'ply-canvas';
68
  canvas.style.zIndex = "1";
 
 
 
 
 
 
69
  viewerContainer.insertBefore(canvas, progressDialog);
70
 
71
- // --- PATCH: Prevent pinch/double-tap zoom for iOS (in addition to interface.js) ---
72
- if (isIOS) {
73
- canvas.style.touchAction = "none";
74
- canvas.addEventListener('gesturestart', e => e.preventDefault());
75
- canvas.addEventListener('gesturechange', e => e.preventDefault());
76
- canvas.addEventListener('gestureend', e => e.preventDefault());
77
- // Extra: prevent rubber-band scroll on iOS Safari inside canvas
78
- canvas.addEventListener('touchmove', function(e) {
79
- if (e.touches.length > 1) e.preventDefault();
80
- }, { passive: false });
81
  }
82
- // --------------------------------------------------------------------------
83
 
84
- // 4. Wheel listener
 
85
  canvas.addEventListener('wheel', e => {
86
- // Only prevent default if wheel is over canvas (don't scroll webpage)
87
  if (cameraEntity && cameraEntity.script && cameraEntity.script.orbitCamera) {
88
  const orbitCam = cameraEntity.script.orbitCamera;
89
  const sens = (cameraEntity.script.orbitCameraInputMouse?.distanceSensitivity) || 0.4;
@@ -97,16 +100,23 @@ export async function initializeViewer(config, instanceId) {
97
  }
98
  }, { passive: false });
99
 
 
 
 
 
 
 
100
  progressDialog.style.display = 'block';
101
 
102
- // 5. Import PlayCanvas
103
  if (!pc) {
104
  pc = await import("https://esm.run/playcanvas");
105
  window.pc = pc;
106
  }
107
 
108
  try {
109
- // 6. Setup device & app
 
110
  const device = await pc.createGraphicsDevice(canvas, {
111
  deviceTypes: ["webgl2"],
112
  glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
@@ -139,26 +149,24 @@ export async function initializeViewer(config, instanceId) {
139
  app.setCanvasFillMode(pc.FILLMODE_NONE);
140
  app.setCanvasResolution(pc.RESOLUTION_AUTO);
141
 
142
- // Attach ResizeObserver to keep canvas in sync with container size
143
- resizeObserver = new ResizeObserver(entries => {
144
- for (const entry of entries) {
145
- const { width, height } = entry.contentRect;
146
- if (app) {
147
- app.resizeCanvas(width, height);
148
- }
149
- }
150
- });
151
  resizeObserver.observe(viewerContainer);
152
 
153
- window.addEventListener('resize', () => {
154
- if (app) app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
155
- });
156
 
157
  app.on('destroy', () => {
158
- window.removeEventListener('resize', resizeCanvas);
 
159
  });
160
 
161
- // 7. Assets
162
  const assets = {
163
  model: new pc.Asset('gsplat', 'gsplat', { url: plyUrl }),
164
  orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
@@ -168,10 +176,10 @@ export async function initializeViewer(config, instanceId) {
168
  }, { type: pc.TEXTURETYPE_RGBP, mipmaps: false })
169
  };
170
 
171
- // --- PATCH: PlayCanvas bug on iOS: forcibly wait for all assets before first render ---
172
- let allLoaded = false;
 
173
 
174
- const loader = new pc.AssetListLoader(Object.values(assets).filter(Boolean), app.assets);
175
  let lastProg = 0;
176
  assets.model.on('load', () => progressDialog.style.display = 'none');
177
  assets.model.on('error', err => {
@@ -190,7 +198,6 @@ export async function initializeViewer(config, instanceId) {
190
  }, 100);
191
 
192
  loader.load(async () => {
193
- allLoaded = true;
194
  app.start();
195
  app.scene.envAtlas = assets.hdr.resource;
196
 
@@ -223,11 +230,10 @@ export async function initializeViewer(config, instanceId) {
223
  dirLight.setLocalEulerAngles(0, 0, 0);
224
  app.root.addChild(dirLight);
225
 
226
- // Gallery GLB (floor)
227
  let galleryEntity = null;
228
  if (assets.galerie && assets.galerie.resource && assets.galerie.resource.instantiateRenderEntity) {
229
  galleryEntity = assets.galerie.resource.instantiateRenderEntity();
230
- galleryEntity.enabled = true; // Ensure enabled at start
231
  app.root.addChild(galleryEntity);
232
  }
233
 
@@ -263,6 +269,7 @@ export async function initializeViewer(config, instanceId) {
263
  distanceSensitivity: isMobile ? 0.5 : 0.4
264
  }
265
  });
 
266
  if (cameraEntity.script.orbitCameraInputMouse) {
267
  cameraEntity.script.orbitCameraInputMouse.onMouseWheel = function() {};
268
  }
@@ -274,33 +281,19 @@ export async function initializeViewer(config, instanceId) {
274
  });
275
  app.root.addChild(cameraEntity);
276
 
277
- // --- iOS Black Screen Bug Fix: force at least one frame after all assets are loaded ---
278
- // Wait for WebGL context to be ready before first render
279
- app.once('frameupdate', () => {
280
- if (!canvas.width || !canvas.height) {
281
- app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
282
- }
283
- });
284
-
285
  // Reset & constrain updates
286
  app.once('update', () => resetViewerCamera());
287
  app.on('update', dt => {
288
  if (cameraEntity) {
289
  const pos = cameraEntity.getPosition();
290
- if (pos.y < minY) {
291
- cameraEntity.setPosition(pos.x, minY, pos.z);
292
- }
293
  }
294
  });
295
 
296
- // Final resize
297
- app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
298
 
299
- // --- Gallery GLB (floor) visibility bug on iPhone ---
300
- // Defensive: never toggle galerie (floor) visibility on tooltip toggle!
301
- // If you ever use tooltips code that disables/enables galleryEntity, remove it.
302
-
303
- // Tooltips
304
  try {
305
  const tooltipsModule = await import('./tooltips.js');
306
  tooltipsModule.initializeTooltips({
@@ -311,14 +304,16 @@ export async function initializeViewer(config, instanceId) {
311
  defaultVisible: !!config.showTooltipsDefault,
312
  moveDuration: config.tooltipMoveDuration || 0.6
313
  });
314
- } catch (e) {
315
- // Tooltips are optional; do nothing if failed
316
- }
317
 
318
  progressDialog.style.display = 'none';
319
  viewerInitialized = true;
320
  });
321
 
 
 
 
 
322
  } catch (error) {
323
  progressDialog.innerHTML = `<p style="color: red">Error loading viewer: ${error && error.message ? error.message : error}</p>`;
324
  }
@@ -365,9 +360,7 @@ export function resetViewerCamera() {
365
  if (orbitCam._updatePosition) orbitCam._updatePosition();
366
 
367
  tempEnt.destroy();
368
- } catch (e) {
369
- // Silent fail: resetting camera should never crash app
370
- }
371
  }
372
 
373
  export function cleanupViewer() {
@@ -381,7 +374,6 @@ export function cleanupViewer() {
381
  modelEntity = null;
382
  viewerInitialized = false;
383
 
384
- // Disconnect the ResizeObserver to avoid leaks
385
  if (resizeObserver) {
386
  resizeObserver.disconnect();
387
  resizeObserver = null;
 
1
  // viewer.js
2
  // ==============================
3
 
4
+ let pc; // PlayCanvas module
5
  export let app = null;
6
  let cameraEntity = null;
7
  let modelEntity = null;
 
13
  let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRotationZ;
14
  let plyUrl, glbUrl;
15
 
16
+ // Platform flags
17
+ const isIOS = typeof navigator !== "undefined" && /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
18
+ const isMobile = isIOS || (typeof navigator !== "undefined" && /Android/i.test(navigator.userAgent));
19
+
20
  /**
21
  * initializeViewer(config, instanceId)
22
  */
23
  export async function initializeViewer(config, instanceId) {
24
  if (viewerInitialized) return;
25
 
26
+ // 1. Config
 
 
 
27
  plyUrl = config.ply_url;
28
  glbUrl = config.glb_url;
29
  minZoom = parseFloat(config.minZoom || "1");
 
54
  chosenCameraY = isMobile ? cameraYPhone : cameraY;
55
  chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
56
 
57
+ // 2. DOM
58
  const canvasId = 'canvas-' + instanceId;
59
  const progressDialog = document.getElementById('progress-dialog-' + instanceId);
60
  const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
61
  const viewerContainer = document.getElementById('viewer-container-' + instanceId);
62
 
63
+ // 3. Canvas
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
+ canvas.style.display = 'block';
71
+ canvas.style.width = "100%";
72
+ canvas.style.height = "100%";
73
+ canvas.setAttribute('tabindex', '-1');
74
+ // Prevents iOS/Chrome pinch-zoom
75
+ canvas.style.touchAction = 'none';
76
  viewerContainer.insertBefore(canvas, progressDialog);
77
 
78
+ // 3b. iOS fix: force correct initial size on load
79
+ function setCanvasSizeToParent() {
80
+ canvas.width = viewerContainer.clientWidth * window.devicePixelRatio;
81
+ canvas.height = viewerContainer.clientHeight * window.devicePixelRatio;
82
+ canvas.style.width = "100%";
83
+ canvas.style.height = "100%";
 
 
 
 
84
  }
85
+ setCanvasSizeToParent();
86
 
87
+ // 4. Wheel and touch event prevention (for zoom gesture hijack)
88
+ // Wheel = desktop, touchmove = iOS
89
  canvas.addEventListener('wheel', e => {
 
90
  if (cameraEntity && cameraEntity.script && cameraEntity.script.orbitCamera) {
91
  const orbitCam = cameraEntity.script.orbitCamera;
92
  const sens = (cameraEntity.script.orbitCameraInputMouse?.distanceSensitivity) || 0.4;
 
100
  }
101
  }, { passive: false });
102
 
103
+ // Prevent iOS Safari double-finger pinch/zoom on canvas
104
+ canvas.addEventListener('touchmove', function(e) {
105
+ if (e.touches.length > 1) e.preventDefault();
106
+ }, { passive: false });
107
+
108
+ // 5. Show progress
109
  progressDialog.style.display = 'block';
110
 
111
+ // 6. Import PlayCanvas
112
  if (!pc) {
113
  pc = await import("https://esm.run/playcanvas");
114
  window.pc = pc;
115
  }
116
 
117
  try {
118
+ // 7. Setup device & app
119
+ setCanvasSizeToParent(); // ensure correct size before context creation
120
  const device = await pc.createGraphicsDevice(canvas, {
121
  deviceTypes: ["webgl2"],
122
  glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
 
149
  app.setCanvasFillMode(pc.FILLMODE_NONE);
150
  app.setCanvasResolution(pc.RESOLUTION_AUTO);
151
 
152
+ // 7b. Attach ResizeObserver to keep canvas in sync with container size
153
+ function resizeCanvasToParent() {
154
+ if (!viewerContainer) return;
155
+ setCanvasSizeToParent();
156
+ app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
157
+ }
158
+ resizeObserver = new window.ResizeObserver(resizeCanvasToParent);
 
 
159
  resizeObserver.observe(viewerContainer);
160
 
161
+ // Also on window resize (iOS orientation change)
162
+ window.addEventListener('resize', resizeCanvasToParent);
 
163
 
164
  app.on('destroy', () => {
165
+ window.removeEventListener('resize', resizeCanvasToParent);
166
+ resizeObserver && resizeObserver.disconnect();
167
  });
168
 
169
+ // 8. Assets
170
  const assets = {
171
  model: new pc.Asset('gsplat', 'gsplat', { url: plyUrl }),
172
  orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
 
176
  }, { type: pc.TEXTURETYPE_RGBP, mipmaps: false })
177
  };
178
 
179
+ // Remove null assets
180
+ const toLoad = Object.values(assets).filter(Boolean);
181
+ const loader = new pc.AssetListLoader(toLoad, app.assets);
182
 
 
183
  let lastProg = 0;
184
  assets.model.on('load', () => progressDialog.style.display = 'none');
185
  assets.model.on('error', err => {
 
198
  }, 100);
199
 
200
  loader.load(async () => {
 
201
  app.start();
202
  app.scene.envAtlas = assets.hdr.resource;
203
 
 
230
  dirLight.setLocalEulerAngles(0, 0, 0);
231
  app.root.addChild(dirLight);
232
 
233
+ // Gallery GLB (floor) only added if it loads, never touched by UI
234
  let galleryEntity = null;
235
  if (assets.galerie && assets.galerie.resource && assets.galerie.resource.instantiateRenderEntity) {
236
  galleryEntity = assets.galerie.resource.instantiateRenderEntity();
 
237
  app.root.addChild(galleryEntity);
238
  }
239
 
 
269
  distanceSensitivity: isMobile ? 0.5 : 0.4
270
  }
271
  });
272
+ // Prevent PlayCanvas from handling default mousewheel (which can bubble)
273
  if (cameraEntity.script.orbitCameraInputMouse) {
274
  cameraEntity.script.orbitCameraInputMouse.onMouseWheel = function() {};
275
  }
 
281
  });
282
  app.root.addChild(cameraEntity);
283
 
 
 
 
 
 
 
 
 
284
  // Reset & constrain updates
285
  app.once('update', () => resetViewerCamera());
286
  app.on('update', dt => {
287
  if (cameraEntity) {
288
  const pos = cameraEntity.getPosition();
289
+ if (pos.y < minY) cameraEntity.setPosition(pos.x, minY, pos.z);
 
 
290
  }
291
  });
292
 
293
+ // Final resize after all entities
294
+ resizeCanvasToParent();
295
 
296
+ // Tooltips (only ever affect tooltip spheres, not floor/GLB)
 
 
 
 
297
  try {
298
  const tooltipsModule = await import('./tooltips.js');
299
  tooltipsModule.initializeTooltips({
 
304
  defaultVisible: !!config.showTooltipsDefault,
305
  moveDuration: config.tooltipMoveDuration || 0.6
306
  });
307
+ } catch (e) {}
 
 
308
 
309
  progressDialog.style.display = 'none';
310
  viewerInitialized = true;
311
  });
312
 
313
+ // iOS-specific: sometimes context loss (suspend on background)
314
+ // PlayCanvas auto-recovers, but we need to restore canvas size
315
+ canvas.addEventListener('webglcontextrestored', setCanvasSizeToParent, false);
316
+
317
  } catch (error) {
318
  progressDialog.innerHTML = `<p style="color: red">Error loading viewer: ${error && error.message ? error.message : error}</p>`;
319
  }
 
360
  if (orbitCam._updatePosition) orbitCam._updatePosition();
361
 
362
  tempEnt.destroy();
363
+ } catch (e) {}
 
 
364
  }
365
 
366
  export function cleanupViewer() {
 
374
  modelEntity = null;
375
  viewerInitialized = false;
376
 
 
377
  if (resizeObserver) {
378
  resizeObserver.disconnect();
379
  resizeObserver = null;