MikaFil commited on
Commit
8c64c02
·
verified ·
1 Parent(s): 6c5f286

Update viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +112 -138
viewer.js CHANGED
@@ -1,22 +1,26 @@
1
  // viewer.js
2
  // ==============================
3
- // (Space B: iPhone/desktop/Android compatible, matches Space A + minimal iOS fix)
 
4
 
5
- let pc; // PlayCanvas module
6
  export let app = null;
7
  let cameraEntity = null;
8
  let modelEntity = null;
9
  let viewerInitialized = false;
 
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
  export async function initializeViewer(config, instanceId) {
17
  if (viewerInitialized) return;
18
 
19
- // Detect iOS/mobile for camera parameters and debug alert
20
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
21
  const isMobile = isIOS || /Android/i.test(navigator.userAgent);
22
 
@@ -51,13 +55,13 @@ export async function initializeViewer(config, instanceId) {
51
  chosenCameraY = isMobile ? cameraYPhone : cameraY;
52
  chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
53
 
54
- // 2. DOM references
55
  const canvasId = 'canvas-' + instanceId;
56
  const progressDialog = document.getElementById('progress-dialog-' + instanceId);
57
  const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
58
  const viewerContainer = document.getElementById('viewer-container-' + instanceId);
59
 
60
- // 3. Canvas creation (replace old if present)
61
  let oldCanvas = document.getElementById(canvasId);
62
  if (oldCanvas) oldCanvas.remove();
63
  const canvas = document.createElement('canvas');
@@ -66,8 +70,8 @@ export async function initializeViewer(config, instanceId) {
66
  canvas.style.zIndex = "1";
67
  viewerContainer.insertBefore(canvas, progressDialog);
68
 
69
- // 4. Mouse wheel (zoom) handler
70
- canvas.addEventListener('wheel', function(e) {
71
  if (cameraEntity && cameraEntity.script && cameraEntity.script.orbitCamera) {
72
  const orbitCam = cameraEntity.script.orbitCamera;
73
  const sens = (cameraEntity.script.orbitCameraInputMouse?.distanceSensitivity) || 0.4;
@@ -83,41 +87,22 @@ export async function initializeViewer(config, instanceId) {
83
 
84
  progressDialog.style.display = 'block';
85
 
86
- // 5. Load PlayCanvas module if needed
87
  if (!pc) {
88
  pc = await import("https://esm.run/playcanvas");
89
  window.pc = pc;
90
  }
91
 
92
  try {
93
- // 6. Setup graphics device (WebGL2 → WebGL1 fallback for iPhone)
94
- async function getGraphicsDevice(preferWebgl2 = true) {
95
- const baseOpts = {
96
- glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
97
- twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
98
- antialias: false
99
- };
100
- try {
101
- const dev = await pc.createGraphicsDevice(canvas, {
102
- ...baseOpts,
103
- deviceTypes: preferWebgl2 ? ["webgl2"] : ["webgl1"]
104
- });
105
- // Optionally alert for iPhone debug
106
- // if (isIOS) alert(preferWebgl2 ? "WebGL2 context OK" : "WebGL1 context OK");
107
- return dev;
108
- } catch (err) {
109
- if (preferWebgl2) {
110
- // if (isIOS) alert("WebGL2 unavailable; falling back to WebGL1");
111
- return getGraphicsDevice(false);
112
- }
113
- throw err;
114
- }
115
- }
116
-
117
- const device = await getGraphicsDevice(true);
118
  device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
119
 
120
- // Use the *Space A* method: AppBase + .init(options)
121
  const opts = new pc.AppOptions();
122
  opts.graphicsDevice = device;
123
  opts.mouse = new pc.Mouse(canvas);
@@ -127,7 +112,9 @@ export async function initializeViewer(config, instanceId) {
127
  pc.CameraComponentSystem,
128
  pc.LightComponentSystem,
129
  pc.ScriptComponentSystem,
130
- pc.GSplatComponentSystem
 
 
131
  ];
132
  opts.resourceHandlers = [
133
  pc.TextureHandler,
@@ -136,23 +123,31 @@ export async function initializeViewer(config, instanceId) {
136
  pc.GSplatHandler
137
  ];
138
 
139
- app = new pc.AppBase(canvas);
140
- app.init(opts);
141
  app.setCanvasFillMode(pc.FILLMODE_NONE);
142
  app.setCanvasResolution(pc.RESOLUTION_AUTO);
143
- app.scene.exposure = 0.5;
144
- app.scene.toneMapping = pc.TONEMAP_ACES;
145
-
146
- // 7. Resize logic (like Space A)
147
- function resize() {
148
- if (app) {
149
- app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
 
 
 
150
  }
151
- }
152
- window.addEventListener('resize', resize);
153
- app.on('destroy', () => window.removeEventListener('resize', resize));
 
 
 
 
 
 
154
 
155
- // 8. Assets
156
  const assets = {
157
  model: new pc.Asset('gsplat', 'gsplat', { url: plyUrl }),
158
  orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
@@ -164,16 +159,13 @@ export async function initializeViewer(config, instanceId) {
164
 
165
  const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
166
  let lastProg = 0;
167
-
168
- assets.model.on('load', function() {
169
- progressDialog.style.display = 'none';
170
- });
171
- assets.model.on('error', function(err) {
172
- progressDialog.innerHTML = `<p style="color: red">Error loading model: ${err}</p>`;
173
  console.error("Error loading PLY file:", err);
 
174
  });
175
 
176
- const progCheck = setInterval(function() {
177
  if (assets.model.resource) {
178
  progressIndicator.value = 100;
179
  clearInterval(progCheck);
@@ -184,12 +176,11 @@ export async function initializeViewer(config, instanceId) {
184
  }
185
  }, 100);
186
 
187
- loader.load(function() {
188
  app.start();
189
-
190
  app.scene.envAtlas = assets.hdr.resource;
191
 
192
- // Model
193
  modelEntity = new pc.Entity('model');
194
  modelEntity.addComponent('gsplat', { asset: assets.model });
195
  modelEntity.setLocalPosition(modelX, modelY, modelZ);
@@ -215,24 +206,20 @@ export async function initializeViewer(config, instanceId) {
215
  shadowType: pc.SHADOW_PCSS_32F,
216
  shadowDistance: 1000
217
  });
218
- app.root.addChild(dirLight);
219
  dirLight.setLocalEulerAngles(0, 0, 0);
 
220
 
221
- // Gallery (GLB)
222
  const galleryEntity = assets.galerie.resource.instantiateRenderEntity();
223
  app.root.addChild(galleryEntity);
224
 
225
- // Camera
226
  cameraEntity = new pc.Entity('camera');
227
  cameraEntity.addComponent('camera', {
228
  clearColor: config.canvas_background
229
- ? new pc.Color(
230
- parseInt(config.canvas_background.substr(1, 2), 16) / 255,
231
- parseInt(config.canvas_background.substr(3, 2), 16) / 255,
232
- parseInt(config.canvas_background.substr(5, 2), 16) / 255
233
- )
234
- : new pc.Color(0, 0, 0),
235
- toneMapping: pc.TONEMAP_ACES
236
  });
237
  cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
238
  cameraEntity.lookAt(modelEntity.getPosition());
@@ -269,24 +256,9 @@ export async function initializeViewer(config, instanceId) {
269
  });
270
  app.root.addChild(cameraEntity);
271
 
272
- // Initial camera reset
273
- setTimeout(function() {
274
- if (cameraEntity && cameraEntity.script && cameraEntity.script.orbitCamera) {
275
- const modelPos = modelEntity.getPosition();
276
- const camPos = cameraEntity.getPosition();
277
- const distanceVec = new pc.Vec3();
278
- distanceVec.sub2(camPos, modelPos);
279
- const distance = distanceVec.length();
280
- cameraEntity.script.orbitCamera.pivotPoint.copy(modelPos);
281
- cameraEntity.script.orbitCamera.distance = distance;
282
- if (typeof cameraEntity.script.orbitCamera._removeInertia === 'function') {
283
- cameraEntity.script.orbitCamera._removeInertia();
284
- }
285
- }
286
- }, 100);
287
-
288
- // Per-frame Y guard (for camera)
289
- app.on('update', function(dt) {
290
  if (cameraEntity) {
291
  const pos = cameraEntity.getPosition();
292
  if (pos.y < minY) {
@@ -295,79 +267,75 @@ export async function initializeViewer(config, instanceId) {
295
  }
296
  });
297
 
298
- // Initial resize
299
- resize();
300
-
301
- // Tooltips support (optional)
302
- if (config.tooltips_url) {
303
- import('./tooltips.js').then(tooltipsModule => {
304
- tooltipsModule.initializeTooltips({
305
- app,
306
- cameraEntity,
307
- modelEntity,
308
- tooltipsUrl: config.tooltips_url,
309
- defaultVisible: !!config.showTooltipsDefault,
310
- moveDuration: config.tooltipMoveDuration || 0.6
311
- });
312
- }).catch(e => {
313
- console.error("Error loading tooltips.js:", e);
314
  });
 
 
315
  }
316
 
 
317
  viewerInitialized = true;
318
-
319
- // Debug: show an alert on iPhone only on first frame rendered
320
- if (isIOS) {
321
- let firstDraw = true;
322
- app.on('frameend', function() {
323
- if (firstDraw) {
324
- alert("First draw call reached (iOS)");
325
- firstDraw = false;
326
- }
327
- });
328
- }
329
  });
330
 
331
  } catch (error) {
332
- progressDialog.innerHTML = `<p style="color: red">Error loading viewer: ${error.message}</p>`;
333
  console.error("Error initializing PlayCanvas viewer:", error);
 
334
  }
335
  }
336
 
337
- // -----------------------------------------------------------------------------
338
- // resetViewerCamera: (unchanged; matches Space A)
339
  export function resetViewerCamera() {
340
  try {
341
  if (!cameraEntity || !modelEntity || !app) return;
342
  const orbitCam = cameraEntity.script.orbitCamera;
343
  if (!orbitCam) return;
 
344
  const modelPos = modelEntity.getPosition();
345
- const tempEntity = new pc.Entity();
346
- tempEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
347
- tempEntity.lookAt(modelPos);
348
- const distance = new pc.Vec3().sub2(new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ), modelPos).length();
 
 
 
 
 
349
  cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
350
  cameraEntity.lookAt(modelPos);
 
351
  orbitCam.pivotPoint = modelPos.clone();
352
- orbitCam._targetDistance = distance;
353
- orbitCam._distance = distance;
354
- const rotation = tempEntity.getRotation();
355
- const tempForward = new pc.Vec3();
356
- rotation.transformVector(pc.Vec3.FORWARD, tempForward);
357
- const yaw = Math.atan2(-tempForward.x, -tempForward.z) * pc.math.RAD_TO_DEG;
 
 
358
  const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);
359
- const rotWithoutYaw = new pc.Quat().mul2(yawQuat, rotation);
360
- const forwardWithoutYaw = new pc.Vec3();
361
- rotWithoutYaw.transformVector(pc.Vec3.FORWARD, forwardWithoutYaw);
362
- const pitch = Math.atan2(forwardWithoutYaw.y, -forwardWithoutYaw.z) * pc.math.RAD_TO_DEG;
363
- orbitCam._targetYaw = yaw;
364
- orbitCam._yaw = yaw;
 
365
  orbitCam._targetPitch = pitch;
366
- orbitCam._pitch = pitch;
367
- if (typeof orbitCam._updatePosition === 'function') {
368
- orbitCam._updatePosition();
369
- }
370
- tempEntity.destroy();
371
  } catch (e) {
372
  console.error("Error resetting camera:", e);
373
  }
@@ -383,4 +351,10 @@ export function cleanupViewer() {
383
  cameraEntity = null;
384
  modelEntity = null;
385
  viewerInitialized = false;
386
- }
 
 
 
 
 
 
 
1
  // viewer.js
2
  // ==============================
3
+ // viewer.js
4
+ // ==============================
5
 
6
+ let pc; // will hold the PlayCanvas module once imported
7
  export let app = null;
8
  let cameraEntity = null;
9
  let modelEntity = null;
10
  let viewerInitialized = false;
11
+ let resizeObserver = null;
12
 
13
  let chosenCameraX, chosenCameraY, chosenCameraZ;
14
  let minZoom, maxZoom, minAngle, maxAngle, minAzimuth, maxAzimuth, minPivotY, minY;
15
  let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRotationZ;
16
  let plyUrl, glbUrl;
17
 
18
+ /**
19
+ * initializeViewer(config, instanceId)
20
+ */
21
  export async function initializeViewer(config, instanceId) {
22
  if (viewerInitialized) return;
23
 
 
24
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
25
  const isMobile = isIOS || /Android/i.test(navigator.userAgent);
26
 
 
55
  chosenCameraY = isMobile ? cameraYPhone : cameraY;
56
  chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
57
 
58
+ // 2. Grab DOM
59
  const canvasId = 'canvas-' + instanceId;
60
  const progressDialog = document.getElementById('progress-dialog-' + instanceId);
61
  const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
62
  const viewerContainer = document.getElementById('viewer-container-' + instanceId);
63
 
64
+ // 3. Create <canvas>
65
  let oldCanvas = document.getElementById(canvasId);
66
  if (oldCanvas) oldCanvas.remove();
67
  const canvas = document.createElement('canvas');
 
70
  canvas.style.zIndex = "1";
71
  viewerContainer.insertBefore(canvas, progressDialog);
72
 
73
+ // 4. Wheel listener
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;
 
87
 
88
  progressDialog.style.display = 'block';
89
 
90
+ // 5. Import PlayCanvas
91
  if (!pc) {
92
  pc = await import("https://esm.run/playcanvas");
93
  window.pc = pc;
94
  }
95
 
96
  try {
97
+ // 6. Setup device & app
98
+ const device = await pc.createGraphicsDevice(canvas, {
99
+ deviceTypes: ["webgl2"],
100
+ glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
101
+ twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
102
+ antialias: false
103
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
105
 
 
106
  const opts = new pc.AppOptions();
107
  opts.graphicsDevice = device;
108
  opts.mouse = new pc.Mouse(canvas);
 
112
  pc.CameraComponentSystem,
113
  pc.LightComponentSystem,
114
  pc.ScriptComponentSystem,
115
+ pc.GSplatComponentSystem,
116
+ pc.CollisionComponentSystem,
117
+ pc.RigidbodyComponentSystem
118
  ];
119
  opts.resourceHandlers = [
120
  pc.TextureHandler,
 
123
  pc.GSplatHandler
124
  ];
125
 
126
+ app = new pc.Application(canvas, opts);
 
127
  app.setCanvasFillMode(pc.FILLMODE_NONE);
128
  app.setCanvasResolution(pc.RESOLUTION_AUTO);
129
+ //app.scene.exposure = 0.5;
130
+ //app.scene.toneMapping = pc.TONEMAP_ACES;
131
+
132
+ // Attach ResizeObserver to keep canvas in sync with container size
133
+ resizeObserver = new ResizeObserver(entries => {
134
+ for (const entry of entries) {
135
+ const { width, height } = entry.contentRect;
136
+ if (app) {
137
+ app.resizeCanvas(width, height);
138
+ }
139
  }
140
+ });
141
+ resizeObserver.observe(viewerContainer);
142
+
143
+ window.addEventListener('resize', () => {
144
+ if (app) app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
145
+ });
146
+ app.on('destroy', () => {
147
+ window.removeEventListener('resize', resizeCanvas);
148
+ });
149
 
150
+ // 7. Assets
151
  const assets = {
152
  model: new pc.Asset('gsplat', 'gsplat', { url: plyUrl }),
153
  orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
 
159
 
160
  const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
161
  let lastProg = 0;
162
+ assets.model.on('load', () => progressDialog.style.display = 'none');
163
+ assets.model.on('error', err => {
 
 
 
 
164
  console.error("Error loading PLY file:", err);
165
+ progressDialog.innerHTML = `<p style="color: red">Error loading model: ${err}</p>`;
166
  });
167
 
168
+ const progCheck = setInterval(() => {
169
  if (assets.model.resource) {
170
  progressIndicator.value = 100;
171
  clearInterval(progCheck);
 
176
  }
177
  }, 100);
178
 
179
+ loader.load(async () => {
180
  app.start();
 
181
  app.scene.envAtlas = assets.hdr.resource;
182
 
183
+ // Model entity
184
  modelEntity = new pc.Entity('model');
185
  modelEntity.addComponent('gsplat', { asset: assets.model });
186
  modelEntity.setLocalPosition(modelX, modelY, modelZ);
 
206
  shadowType: pc.SHADOW_PCSS_32F,
207
  shadowDistance: 1000
208
  });
 
209
  dirLight.setLocalEulerAngles(0, 0, 0);
210
+ app.root.addChild(dirLight);
211
 
212
+ // Gallery GLB
213
  const galleryEntity = assets.galerie.resource.instantiateRenderEntity();
214
  app.root.addChild(galleryEntity);
215
 
216
+ // Camera setup
217
  cameraEntity = new pc.Entity('camera');
218
  cameraEntity.addComponent('camera', {
219
  clearColor: config.canvas_background
220
+ ? parseInt(config.canvas_background.substr(1, 2), 16) / 255
221
+ : 0,
222
+ //toneMapping: pc.TONEMAP_ACES
 
 
 
 
223
  });
224
  cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
225
  cameraEntity.lookAt(modelEntity.getPosition());
 
256
  });
257
  app.root.addChild(cameraEntity);
258
 
259
+ // Reset & constrain updates
260
+ app.once('update', () => resetViewerCamera());
261
+ app.on('update', dt => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  if (cameraEntity) {
263
  const pos = cameraEntity.getPosition();
264
  if (pos.y < minY) {
 
267
  }
268
  });
269
 
270
+ // Final resize
271
+ app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
272
+
273
+ // Tooltips
274
+ try {
275
+ const tooltipsModule = await import('./tooltips.js');
276
+ tooltipsModule.initializeTooltips({
277
+ app,
278
+ cameraEntity,
279
+ modelEntity,
280
+ tooltipsUrl: config.tooltips_url,
281
+ defaultVisible: !!config.showTooltipsDefault,
282
+ moveDuration: config.tooltipMoveDuration || 0.6
 
 
 
283
  });
284
+ } catch (e) {
285
+ console.error("Error loading tooltips.js:", e);
286
  }
287
 
288
+ progressDialog.style.display = 'none';
289
  viewerInitialized = true;
 
 
 
 
 
 
 
 
 
 
 
290
  });
291
 
292
  } catch (error) {
 
293
  console.error("Error initializing PlayCanvas viewer:", error);
294
+ progressDialog.innerHTML = `<p style="color: red">Error loading viewer: ${error.message}</p>`;
295
  }
296
  }
297
 
 
 
298
  export function resetViewerCamera() {
299
  try {
300
  if (!cameraEntity || !modelEntity || !app) return;
301
  const orbitCam = cameraEntity.script.orbitCamera;
302
  if (!orbitCam) return;
303
+
304
  const modelPos = modelEntity.getPosition();
305
+ const tempEnt = new pc.Entity();
306
+ tempEnt.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
307
+ tempEnt.lookAt(modelPos);
308
+
309
+ const dist = new pc.Vec3().sub2(
310
+ new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ),
311
+ modelPos
312
+ ).length();
313
+
314
  cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
315
  cameraEntity.lookAt(modelPos);
316
+
317
  orbitCam.pivotPoint = modelPos.clone();
318
+ orbitCam._targetDistance = dist;
319
+ orbitCam._distance = dist;
320
+
321
+ const rot = tempEnt.getRotation();
322
+ const fwd = new pc.Vec3();
323
+ rot.transformVector(pc.Vec3.FORWARD, fwd);
324
+
325
+ const yaw = Math.atan2(-fwd.x, -fwd.z) * pc.math.RAD_TO_DEG;
326
  const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);
327
+ const rotNoYaw = new pc.Quat().mul2(yawQuat, rot);
328
+ const fNoYaw = new pc.Vec3();
329
+ rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw);
330
+ const pitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
331
+
332
+ orbitCam._targetYaw = yaw;
333
+ orbitCam._yaw = yaw;
334
  orbitCam._targetPitch = pitch;
335
+ orbitCam._pitch = pitch;
336
+ orbitCam._updatePosition && orbitCam._updatePosition();
337
+
338
+ tempEnt.destroy();
 
339
  } catch (e) {
340
  console.error("Error resetting camera:", e);
341
  }
 
351
  cameraEntity = null;
352
  modelEntity = null;
353
  viewerInitialized = false;
354
+
355
+ // Disconnect the ResizeObserver to avoid leaks
356
+ if (resizeObserver) {
357
+ resizeObserver.disconnect();
358
+ resizeObserver = null;
359
+ }
360
+ }