MikaFil commited on
Commit
fe15952
·
verified ·
1 Parent(s): 17015fd

Update viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +188 -216
viewer.js CHANGED
@@ -1,7 +1,7 @@
1
  // viewer.js
2
  // ==============================
3
 
4
- let pc; // PlayCanvas module (will be loaded dynamically)
5
  export let app = null;
6
  let cameraEntity = null;
7
  let modelEntity = null;
@@ -13,10 +13,12 @@ let minZoom, maxZoom, minAngle, maxAngle, minAzimuth, maxAzimuth, minPivotY, min
13
  let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRotationZ;
14
  let plyUrl, glbUrl;
15
 
 
 
 
16
  export async function initializeViewer(config, instanceId) {
17
  if (viewerInitialized) return;
18
 
19
- // Device detection
20
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
21
  const isMobile = isIOS || /Android/i.test(navigator.userAgent);
22
 
@@ -64,29 +66,9 @@ export async function initializeViewer(config, instanceId) {
64
  canvas.id = canvasId;
65
  canvas.className = 'ply-canvas';
66
  canvas.style.zIndex = "1";
67
- canvas.style.width = "100%";
68
- canvas.style.height = "100%";
69
- canvas.setAttribute('tabindex', '0');
70
  viewerContainer.insertBefore(canvas, progressDialog);
71
 
72
- // Remove touch-action/pointer-events from overlays if present (to not block iOS events)
73
- // (This is safe as overlays/panels should not interfere with viewer canvas input)
74
- if (viewerContainer.style.touchAction) viewerContainer.style.touchAction = "";
75
- if (viewerContainer.style.pointerEvents) viewerContainer.style.pointerEvents = "";
76
- if (canvas.style.pointerEvents) canvas.style.pointerEvents = "";
77
-
78
- // Prevent iOS Safari zoom gesture conflicts
79
- canvas.style.touchAction = "none";
80
- canvas.style.webkitTouchCallout = "none";
81
- canvas.addEventListener('gesturestart', e => e.preventDefault());
82
- canvas.addEventListener('gesturechange', e => e.preventDefault());
83
- canvas.addEventListener('gestureend', e => e.preventDefault());
84
- canvas.addEventListener('dblclick', e => e.preventDefault());
85
- canvas.addEventListener('touchstart', function(e) {
86
- if (e.touches.length > 1) e.preventDefault();
87
- }, { passive: false });
88
-
89
- // 4. Wheel/zoom listener (desktop)
90
  canvas.addEventListener('wheel', e => {
91
  if (cameraEntity && cameraEntity.script && cameraEntity.script.orbitCamera) {
92
  const orbitCam = cameraEntity.script.orbitCamera;
@@ -101,213 +83,194 @@ export async function initializeViewer(config, instanceId) {
101
  }
102
  }, { passive: false });
103
 
104
- // Show loading
105
  progressDialog.style.display = 'block';
106
 
107
- // 5. Import PlayCanvas if not present
108
  if (!pc) {
109
  pc = await import("https://esm.run/playcanvas");
110
  window.pc = pc;
111
  }
112
 
113
- // 6. Setup device & app
114
- const device = await pc.createGraphicsDevice(canvas, {
115
- deviceTypes: ["webgl2"],
116
- glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
117
- twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
118
- antialias: false
119
- });
120
- device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
121
-
122
- const opts = new pc.AppOptions();
123
- opts.graphicsDevice = device;
124
- // Attach input to document.body (not canvas) for iOS and match PlayCanvas example!
125
- opts.mouse = new pc.Mouse(document.body);
126
- opts.touch = new pc.TouchDevice(document.body);
127
- opts.componentSystems = [
128
- pc.RenderComponentSystem,
129
- pc.CameraComponentSystem,
130
- pc.LightComponentSystem,
131
- pc.ScriptComponentSystem,
132
- pc.GSplatComponentSystem,
133
- pc.CollisionComponentSystem,
134
- pc.RigidbodyComponentSystem
135
- ];
136
- opts.resourceHandlers = [
137
- pc.TextureHandler,
138
- pc.ContainerHandler,
139
- pc.ScriptHandler,
140
- pc.GSplatHandler
141
- ];
142
-
143
- app = new pc.Application(canvas, opts);
144
- app.setCanvasFillMode(pc.FILLMODE_NONE);
145
- app.setCanvasResolution(pc.RESOLUTION_AUTO);
146
-
147
- // Attach ResizeObserver to keep canvas in sync with container size
148
- resizeObserver = new ResizeObserver(entries => {
149
- for (const entry of entries) {
150
- const { width, height } = entry.contentRect;
151
- if (app) {
152
- app.resizeCanvas(width, height);
 
153
  }
154
- }
155
- });
156
- resizeObserver.observe(viewerContainer);
157
-
158
- window.addEventListener('resize', () => {
159
- if (app) app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
160
- });
161
 
162
- app.on('destroy', () => {
163
- if (resizeObserver) resizeObserver.disconnect();
164
- resizeObserver = null;
165
- });
166
-
167
- // --- iOS-specific fix: robust binary loader for PLY ---
168
- // If window.PLY_FORCE_ARRAYBUFFER is set, override asset loader for PLY
169
- if (window.PLY_FORCE_ARRAYBUFFER) {
170
- const origRequest = pc.Http.request;
171
- pc.Http.request = function(options) {
172
- if (options && options.url && (options.url.endsWith('.ply') || options.responseType === 'gsplat')) {
173
- options.responseType = 'arraybuffer';
174
- }
175
- return origRequest.call(this, options);
176
- };
177
- }
178
 
179
- // 7. Asset loading (progress bar)
180
- const assets = {
181
- model: new pc.Asset('gsplat', 'gsplat', { url: plyUrl }),
182
- orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
183
- galerie: new pc.Asset('galerie', 'container', { url: glbUrl }),
184
- hdr: new pc.Asset('hdr', 'texture', {
185
- url: "https://huggingface.co/datasets/bilca/ply_files/resolve/main/galeries/blanc.png"
186
- }, { type: pc.TEXTURETYPE_RGBP, mipmaps: false })
187
- };
188
-
189
- let lastProg = 0;
190
- assets.model.on('load', () => { progressDialog.style.display = 'none'; });
191
- assets.model.on('error', err => {
192
- progressDialog.innerHTML = `<p style="color: red">Error loading model: ${err}</p>`;
193
- });
194
-
195
- const progCheck = setInterval(() => {
196
- if (assets.model.resource) {
197
- progressIndicator.value = 100;
198
- clearInterval(progCheck);
199
- progressDialog.style.display = 'none';
200
- } else if (assets.model.loading) {
201
- lastProg = Math.min(lastProg + 2, 90);
202
- progressIndicator.value = lastProg;
203
- }
204
- }, 100);
205
-
206
- // Wait for PlayCanvas app and input to be ready, then load assets
207
- const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
208
- loader.load(async () => {
209
- app.start();
210
- app.scene.envAtlas = assets.hdr.resource;
211
-
212
- // Main model entity
213
- modelEntity = new pc.Entity('model');
214
- modelEntity.addComponent('gsplat', { asset: assets.model });
215
- modelEntity.setLocalPosition(modelX, modelY, modelZ);
216
- modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
217
- modelEntity.setLocalScale(modelScale, modelScale, modelScale);
218
- app.root.addChild(modelEntity);
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 (optional)
242
- if (assets.galerie && assets.galerie.resource && assets.galerie.resource.instantiateRenderEntity) {
243
- const galleryEntity = assets.galerie.resource.instantiateRenderEntity();
244
- app.root.addChild(galleryEntity);
245
- }
 
 
 
 
246
 
247
- // Camera setup
248
- cameraEntity = new pc.Entity('camera');
249
- cameraEntity.addComponent('camera', {
250
- clearColor: config.canvas_background
251
- ? new pc.Color(
252
- parseInt(config.canvas_background.substr(1, 2), 16) / 255,
253
- parseInt(config.canvas_background.substr(3, 2), 16) / 255,
254
- parseInt(config.canvas_background.substr(5, 2), 16) / 255,
255
- 1
256
- )
257
- : new pc.Color(0.2, 0.2, 0.2, 1)
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
- minY: minY,
275
- frameOnStart: false
276
- }
277
  });
278
- cameraEntity.script.create('orbitCameraInputMouse', {
279
- attributes: {
280
- orbitSensitivity: isMobile ? 0.6 : 0.3,
281
- distanceSensitivity: isMobile ? 0.5 : 0.4
 
 
 
 
 
282
  }
283
- });
284
- cameraEntity.script.create('orbitCameraInputTouch', {
285
- attributes: {
286
- orbitSensitivity: 0.6,
287
- distanceSensitivity: 0.5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  }
289
- });
290
- app.root.addChild(cameraEntity);
291
 
292
- // On first update, reset camera to proper config
293
- app.once('update', () => { resetViewerCamera(); });
294
-
295
- // Clamp Y so camera never goes below floor
296
- app.on('update', dt => {
297
- if (cameraEntity) {
298
- const pos = cameraEntity.getPosition();
299
- if (pos.y < minY) cameraEntity.setPosition(pos.x, minY, pos.z);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  }
301
- });
302
-
303
- // Final resize
304
- app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
305
-
306
- // Tooltips
307
- if (config.tooltips_url) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  try {
309
  const tooltipsModule = await import('./tooltips.js');
310
- await tooltipsModule.initializeTooltips({
311
  app,
312
  cameraEntity,
313
  modelEntity,
@@ -315,12 +278,17 @@ export async function initializeViewer(config, instanceId) {
315
  defaultVisible: !!config.showTooltipsDefault,
316
  moveDuration: config.tooltipMoveDuration || 0.6
317
  });
318
- } catch (e) {/* Fail quietly */}
319
- }
 
 
 
 
 
320
 
321
- // Ready!
322
- viewerInitialized = true;
323
- });
324
  }
325
 
326
  export function resetViewerCamera() {
@@ -364,7 +332,9 @@ export function resetViewerCamera() {
364
  if (orbitCam._updatePosition) orbitCam._updatePosition();
365
 
366
  tempEnt.destroy();
367
- } catch (e) {/* Fail quietly */}
 
 
368
  }
369
 
370
  export function cleanupViewer() {
@@ -377,6 +347,8 @@ export function cleanupViewer() {
377
  cameraEntity = null;
378
  modelEntity = null;
379
  viewerInitialized = false;
 
 
380
  if (resizeObserver) {
381
  resizeObserver.disconnect();
382
  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;
 
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
 
 
66
  canvas.id = canvasId;
67
  canvas.className = 'ply-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;
 
83
  }
84
  }, { passive: false });
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();
105
+ opts.graphicsDevice = device;
106
+ opts.mouse = new pc.Mouse(canvas);
107
+ opts.touch = new pc.TouchDevice(canvas);
108
+ opts.componentSystems = [
109
+ pc.RenderComponentSystem,
110
+ pc.CameraComponentSystem,
111
+ pc.LightComponentSystem,
112
+ pc.ScriptComponentSystem,
113
+ pc.GSplatComponentSystem,
114
+ pc.CollisionComponentSystem,
115
+ pc.RigidbodyComponentSystem
116
+ ];
117
+ opts.resourceHandlers = [
118
+ pc.TextureHandler,
119
+ pc.ContainerHandler,
120
+ pc.ScriptHandler,
121
+ pc.GSplatHandler
122
+ ];
123
+
124
+ app = new pc.Application(canvas, opts);
125
+ app.setCanvasFillMode(pc.FILLMODE_NONE);
126
+ app.setCanvasResolution(pc.RESOLUTION_AUTO);
127
+
128
+ // Attach ResizeObserver to keep canvas in sync with container size
129
+ resizeObserver = new ResizeObserver(entries => {
130
+ for (const entry of entries) {
131
+ const { width, height } = entry.contentRect;
132
+ if (app) {
133
+ app.resizeCanvas(width, height);
134
+ }
135
  }
136
+ });
137
+ resizeObserver.observe(viewerContainer);
 
 
 
 
 
138
 
139
+ window.addEventListener('resize', () => {
140
+ if (app) app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
141
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
+ app.on('destroy', () => {
144
+ window.removeEventListener('resize', resizeCanvas);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  });
 
 
146
 
147
+ // 7. Assets
148
+ const assets = {
149
+ model: new pc.Asset('gsplat', 'gsplat', { url: plyUrl }),
150
+ orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
151
+ galerie: new pc.Asset('galerie', 'container', { url: glbUrl }),
152
+ hdr: new pc.Asset('hdr', 'texture', {
153
+ url: "https://huggingface.co/datasets/bilca/ply_files/resolve/main/galeries/blanc.png"
154
+ }, { type: pc.TEXTURETYPE_RGBP, mipmaps: false })
155
+ };
156
 
157
+ const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
158
+ let lastProg = 0;
159
+ assets.model.on('load', () => progressDialog.style.display = 'none');
160
+ assets.model.on('error', err => {
161
+ progressDialog.innerHTML = `<p style="color: red">Error loading model: ${err}</p>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  });
163
+
164
+ const progCheck = setInterval(() => {
165
+ if (assets.model.resource) {
166
+ progressIndicator.value = 100;
167
+ clearInterval(progCheck);
168
+ progressDialog.style.display = 'none';
169
+ } else if (assets.model.loading) {
170
+ lastProg = Math.min(lastProg + 2, 90);
171
+ progressIndicator.value = lastProg;
172
  }
173
+ }, 100);
174
+
175
+ loader.load(async () => {
176
+ app.start();
177
+ app.scene.envAtlas = assets.hdr.resource;
178
+
179
+ // Model entity
180
+ modelEntity = new pc.Entity('model');
181
+ modelEntity.addComponent('gsplat', { asset: assets.model });
182
+ modelEntity.setLocalPosition(modelX, modelY, modelZ);
183
+ modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
184
+ modelEntity.setLocalScale(modelScale, modelScale, modelScale);
185
+ app.root.addChild(modelEntity);
186
+
187
+ // Light
188
+ const dirLight = new pc.Entity('Cascaded Light');
189
+ dirLight.addComponent('light', {
190
+ type: 'directional',
191
+ color: pc.Color.WHITE,
192
+ shadowBias: 0.3,
193
+ normalOffsetBias: 0.2,
194
+ intensity: 1.0,
195
+ soft: true,
196
+ shadowResolution: 4096,
197
+ penumbraSize: 7,
198
+ penumbraFalloff: 1.5,
199
+ shadowSamples: 128,
200
+ shadowBlockerSamples: 16,
201
+ castShadows: true,
202
+ shadowType: pc.SHADOW_PCSS_32F,
203
+ shadowDistance: 1000
204
+ });
205
+ dirLight.setLocalEulerAngles(0, 0, 0);
206
+ app.root.addChild(dirLight);
207
+
208
+ // Gallery GLB
209
+ if (assets.galerie && assets.galerie.resource && assets.galerie.resource.instantiateRenderEntity) {
210
+ const galleryEntity = assets.galerie.resource.instantiateRenderEntity();
211
+ app.root.addChild(galleryEntity);
212
  }
 
 
213
 
214
+ // Camera setup
215
+ cameraEntity = new pc.Entity('camera');
216
+ cameraEntity.addComponent('camera', {
217
+ clearColor: config.canvas_background
218
+ ? parseInt(config.canvas_background.substr(1, 2), 16) / 255
219
+ : 0
220
+ });
221
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
222
+ cameraEntity.lookAt(modelEntity.getPosition());
223
+
224
+ cameraEntity.addComponent('script');
225
+ cameraEntity.script.create('orbitCamera', {
226
+ attributes: {
227
+ inertiaFactor: 0.2,
228
+ focusEntity: modelEntity,
229
+ distanceMax: maxZoom,
230
+ distanceMin: minZoom,
231
+ pitchAngleMax: maxAngle,
232
+ pitchAngleMin: minAngle,
233
+ yawAngleMax: maxAzimuth,
234
+ yawAngleMin: minAzimuth,
235
+ minPivotY: minPivotY,
236
+ frameOnStart: false
237
+ }
238
+ });
239
+ cameraEntity.script.create('orbitCameraInputMouse', {
240
+ attributes: {
241
+ orbitSensitivity: isMobile ? 0.6 : 0.3,
242
+ distanceSensitivity: isMobile ? 0.5 : 0.4
243
+ }
244
+ });
245
+ if (cameraEntity.script.orbitCameraInputMouse) {
246
+ cameraEntity.script.orbitCameraInputMouse.onMouseWheel = function() {};
247
  }
248
+ cameraEntity.script.create('orbitCameraInputTouch', {
249
+ attributes: {
250
+ orbitSensitivity: 0.6,
251
+ distanceSensitivity: 0.5
252
+ }
253
+ });
254
+ app.root.addChild(cameraEntity);
255
+
256
+ // Reset & constrain updates
257
+ app.once('update', () => resetViewerCamera());
258
+ app.on('update', dt => {
259
+ if (cameraEntity) {
260
+ const pos = cameraEntity.getPosition();
261
+ if (pos.y < minY) {
262
+ cameraEntity.setPosition(pos.x, minY, pos.z);
263
+ }
264
+ }
265
+ });
266
+
267
+ // Final resize
268
+ app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
269
+
270
+ // Tooltips
271
  try {
272
  const tooltipsModule = await import('./tooltips.js');
273
+ tooltipsModule.initializeTooltips({
274
  app,
275
  cameraEntity,
276
  modelEntity,
 
278
  defaultVisible: !!config.showTooltipsDefault,
279
  moveDuration: config.tooltipMoveDuration || 0.6
280
  });
281
+ } catch (e) {
282
+ // Tooltips are optional
283
+ }
284
+
285
+ progressDialog.style.display = 'none';
286
+ viewerInitialized = true;
287
+ });
288
 
289
+ } catch (error) {
290
+ progressDialog.innerHTML = `<p style="color: red">Error loading viewer: ${error && error.message ? error.message : error}</p>`;
291
+ }
292
  }
293
 
294
  export function resetViewerCamera() {
 
332
  if (orbitCam._updatePosition) orbitCam._updatePosition();
333
 
334
  tempEnt.destroy();
335
+ } catch (e) {
336
+ // Silent fail: resetting camera should never crash app
337
+ }
338
  }
339
 
340
  export function cleanupViewer() {
 
347
  cameraEntity = null;
348
  modelEntity = null;
349
  viewerInitialized = false;
350
+
351
+ // Disconnect the ResizeObserver to avoid leaks
352
  if (resizeObserver) {
353
  resizeObserver.disconnect();
354
  resizeObserver = null;