MikaFil commited on
Commit
4515db5
Β·
verified Β·
1 Parent(s): 65007f7

Update viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +107 -186
viewer.js CHANGED
@@ -1,3 +1,4 @@
 
1
  // ==============================
2
  // viewer.js
3
  // ==============================
@@ -15,37 +16,29 @@ let plyUrl, glbUrl;
15
 
16
  /**
17
  * initializeViewer(config, instanceId)
18
- *
19
- * - Dynamically imports PlayCanvas (once).
20
- * - Creates a new <canvas id="canvas-<instanceId>"> inside the viewer container.
21
- * - Sets up lights, model loading, camera + orbit controls, HDR, etc.
22
- * - Exposes app, cameraEntity, modelEntity, so that interface.js can resize or reset.
23
- * - Imports & initializes tooltip hotspots from tooltips.js.
24
  */
25
  export async function initializeViewer(config, instanceId) {
26
  if (viewerInitialized) return;
27
 
28
- // Detect device type again (for camera coordinates & input sensitivity)
29
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
30
  const isMobile = isIOS || /Android/i.test(navigator.userAgent);
31
 
32
- // ─── 1. Read all relevant config values ───────────────────────────────────────
33
  plyUrl = config.ply_url;
34
  glbUrl = config.glb_url;
35
-
36
- minZoom = parseFloat(config.minZoom || "1");
37
- maxZoom = parseFloat(config.maxZoom || "20");
38
- minAngle = parseFloat(config.minAngle || "-45");
39
- maxAngle = parseFloat(config.maxAngle || "90");
40
- minAzimuth = (config.minAzimuth !== undefined) ? parseFloat(config.minAzimuth) : -360;
41
- maxAzimuth = (config.maxAzimuth !== undefined) ? parseFloat(config.maxAzimuth) : 360;
42
- minPivotY = parseFloat(config.minPivotY || "0");
43
- minY = (config.minY !== undefined) ? parseFloat(config.minY) : 0;
44
-
45
- modelX = (config.modelX !== undefined) ? parseFloat(config.modelX) : 0;
46
- modelY = (config.modelY !== undefined) ? parseFloat(config.modelY) : 0;
47
- modelZ = (config.modelZ !== undefined) ? parseFloat(config.modelZ) : 0;
48
- modelScale = (config.modelScale !== undefined) ? parseFloat(config.modelScale) : 1;
49
  modelRotationX = (config.modelRotationX !== undefined) ? parseFloat(config.modelRotationX) : 0;
50
  modelRotationY = (config.modelRotationY !== undefined) ? parseFloat(config.modelRotationY) : 0;
51
  modelRotationZ = (config.modelRotationZ !== undefined) ? parseFloat(config.modelRotationZ) : 0;
@@ -61,34 +54,30 @@ export async function initializeViewer(config, instanceId) {
61
  chosenCameraY = isMobile ? cameraYPhone : cameraY;
62
  chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
63
 
64
- // ─── 2. Grab DOM elements inside interface.js’s container ─────────────────────
65
- const canvasId = 'canvas-' + instanceId;
66
- const progressDialog = document.getElementById('progress-dialog-' + instanceId);
67
  const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
68
- const viewerContainerElem = document.getElementById('viewer-container-' + instanceId);
69
 
70
- // ─── 3. Create a fresh <canvas> (removing any old one) ────────────────────────
71
  let oldCanvas = document.getElementById(canvasId);
72
- if (oldCanvas) {
73
- oldCanvas.remove();
74
- }
75
- const newCanvas = document.createElement('canvas');
76
- newCanvas.id = canvasId;
77
- newCanvas.className = 'ply-canvas';
78
- newCanvas.style.zIndex = "1";
79
- viewerContainerElem.insertBefore(newCanvas, progressDialog);
80
- const canvas = newCanvas;
81
-
82
- // ─── 4. Add mouse‐wheel listener for zoom adjustments ────────────────────────
83
- canvas.addEventListener('wheel', function(e) {
84
  if (cameraEntity && cameraEntity.script && cameraEntity.script.orbitCamera) {
85
  const orbitCam = cameraEntity.script.orbitCamera;
86
- const sensitivity = (cameraEntity.script.orbitCameraInputMouse && cameraEntity.script.orbitCameraInputMouse.distanceSensitivity) || 0.4;
87
-
88
  if (cameraEntity.camera.projection === pc.PROJECTION_PERSPECTIVE) {
89
- orbitCam.distance -= e.deltaY * 0.01 * sensitivity * (orbitCam.distance * 0.1);
90
  } else {
91
- orbitCam.orthoHeight -= e.deltaY * 0.01 * sensitivity * (orbitCam.orthoHeight * 0.1);
92
  }
93
  e.preventDefault();
94
  e.stopPropagation();
@@ -97,31 +86,27 @@ export async function initializeViewer(config, instanceId) {
97
 
98
  progressDialog.style.display = 'block';
99
 
100
- // ─── 5. Import PlayCanvas (only once) ─────────────────────────────────────────
101
  if (!pc) {
102
  pc = await import("https://esm.run/playcanvas");
103
  window.pc = pc;
104
  }
105
 
106
  try {
107
- // ─── 6. Create a GraphicsDevice + AppOptions ────────────────────────────────
108
- const deviceType = "webgl2";
109
- const gfxOptions = {
110
- deviceTypes: [deviceType],
111
  glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
112
  twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
113
  antialias: false
114
- };
115
- const device = await pc.createGraphicsDevice(canvas, gfxOptions);
116
  device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
117
 
118
- const createOptions = new pc.AppOptions();
119
- createOptions.graphicsDevice = device;
120
- createOptions.mouse = new pc.Mouse(canvas);
121
- createOptions.touch = new pc.TouchDevice(canvas);
122
-
123
- // ─── ADD: include physics systems for collision-based picking ───────────────
124
- createOptions.componentSystems = [
125
  pc.RenderComponentSystem,
126
  pc.CameraComponentSystem,
127
  pc.LightComponentSystem,
@@ -130,76 +115,59 @@ export async function initializeViewer(config, instanceId) {
130
  pc.CollisionComponentSystem,
131
  pc.RigidbodyComponentSystem
132
  ];
133
- createOptions.resourceHandlers = [
134
  pc.TextureHandler,
135
  pc.ContainerHandler,
136
  pc.ScriptHandler,
137
  pc.GSplatHandler
138
  ];
139
 
140
- // ─── REPLACED: use pc.Application (instead of deprecated / removed AppBase) ──
141
- app = new pc.Application(canvas, createOptions);
142
-
143
- // Configure application
144
  app.setCanvasFillMode(pc.FILLMODE_NONE);
145
  app.setCanvasResolution(pc.RESOLUTION_AUTO);
146
  app.scene.exposure = 0.5;
147
  app.scene.toneMapping = pc.TONEMAP_ACES;
148
 
149
- // ─── 7. Handle window‐resize for the PlayCanvas canvas ─────────────────────
150
- const resizeCanvas = () => {
151
- if (app) {
152
- app.resizeCanvas(canvas.clientWidth, canvas.clientHeight);
153
- }
154
- };
155
- window.addEventListener('resize', resizeCanvas);
156
  app.on('destroy', () => {
157
  window.removeEventListener('resize', resizeCanvas);
158
  });
159
 
160
- // ─── 8. Declare assets: PLY + orbit‐camera.js + GLB + HDR ───────────────────
161
  const assets = {
162
  model: new pc.Asset('gsplat', 'gsplat', { url: plyUrl }),
163
- orbit: new pc.Asset('script', 'script', { url: "./orbit-camera.js" }),
164
  galerie: new pc.Asset('galerie', 'container', { url: glbUrl }),
165
- hdr: new pc.Asset(
166
- 'hdr',
167
- 'texture',
168
- { url: https://huggingface.co/datasets/bilca/ply_files/resolve/main/galeries/blanc.png },
169
- { type: pc.TEXTURETYPE_RGBP, mipmaps: false }
170
- )
171
  };
172
 
173
- const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets);
174
- let lastProgress = 0;
175
-
176
- assets.model.on('load', () => {
177
- progressDialog.style.display = 'none';
178
- });
179
- assets.model.on('error', (err) => {
180
  console.error("Error loading PLY file:", err);
181
- progressDialog.innerHTML = <p style="color: red">Error loading model: ${err}</p>;
182
  });
183
 
184
- const checkProgress = () => {
185
  if (assets.model.resource) {
186
  progressIndicator.value = 100;
187
- clearInterval(progressChecker);
188
  progressDialog.style.display = 'none';
189
  } else if (assets.model.loading) {
190
- lastProgress += 2;
191
- if (lastProgress > 90) lastProgress = 90;
192
- progressIndicator.value = lastProgress;
193
  }
194
- };
195
- const progressChecker = setInterval(checkProgress, 100);
196
 
197
- // ─── 9. Once assets are loaded, build the scene ────────────────────────────
198
- assetListLoader.load(async () => {
199
  app.start();
200
  app.scene.envAtlas = assets.hdr.resource;
201
 
202
- // 9a. Create & position the model
203
  modelEntity = new pc.Entity('model');
204
  modelEntity.addComponent('gsplat', { asset: assets.model });
205
  modelEntity.setLocalPosition(modelX, modelY, modelZ);
@@ -207,32 +175,29 @@ export async function initializeViewer(config, instanceId) {
207
  modelEntity.setLocalScale(modelScale, modelScale, modelScale);
208
  app.root.addChild(modelEntity);
209
 
210
- // 9b. Add a directional light
211
  const dirLight = new pc.Entity('Cascaded Light');
212
  dirLight.addComponent('light', {
213
- type: 'directional',
214
- color: pc.Color.WHITE,
215
- shadowBias: 0.3,
216
  normalOffsetBias: 0.2,
217
- intensity: 1.0,
218
- soft: true,
219
  shadowResolution: 4096,
220
- penumbraSize: 7,
221
  penumbraFalloff: 1.5,
222
- shadowSamples: 128,
223
  shadowBlockerSamples: 16,
224
- castShadows: true,
225
- shadowType: pc.SHADOW_PCSS_32F,
226
  shadowDistance: 1000
227
  });
228
  app.root.addChild(dirLight);
229
  dirLight.setLocalEulerAngles(0, 0, 0);
230
 
231
- // 9c. Instantiate the GLB β€œgallery” if any
232
  const galleryEntity = assets.galerie.resource.instantiateRenderEntity();
233
  app.root.addChild(galleryEntity);
234
 
235
- // 9d. Create & configure the camera + orbit‐camera scripts
236
  cameraEntity = new pc.Entity('camera');
237
  cameraEntity.addComponent('camera', {
238
  clearColor: config.canvas_background
@@ -240,32 +205,22 @@ export async function initializeViewer(config, instanceId) {
240
  : 0,
241
  toneMapping: pc.TONEMAP_ACES
242
  });
243
- // *** Set initial position/orientation from config immediately ***
244
  cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
245
  cameraEntity.lookAt(modelEntity.getPosition());
246
 
247
- // Warn if camera is very far
248
- const distanceCheck = new pc.Vec3().sub2(
249
- cameraEntity.getPosition(),
250
- modelEntity.getPosition()
251
- ).length();
252
- if (distanceCheck > 100) {
253
- console.warn("Warning: Camera is far from model; it might not be visible");
254
- }
255
-
256
  cameraEntity.addComponent('script');
257
  cameraEntity.script.create('orbitCamera', {
258
  attributes: {
259
  inertiaFactor: 0.2,
260
- focusEntity: modelEntity,
261
- distanceMax: maxZoom,
262
- distanceMin: minZoom,
263
  pitchAngleMax: maxAngle,
264
  pitchAngleMin: minAngle,
265
- yawAngleMax: maxAzimuth,
266
- yawAngleMin: minAzimuth,
267
- minPivotY: minPivotY,
268
- frameOnStart: false
269
  }
270
  });
271
  cameraEntity.script.create('orbitCameraInputMouse', {
@@ -274,7 +229,6 @@ export async function initializeViewer(config, instanceId) {
274
  distanceSensitivity: isMobile ? 0.5 : 0.4
275
  }
276
  });
277
- // Disable default onMouseWheel in orbitCameraInputMouse so we handle it ourselves
278
  if (cameraEntity.script.orbitCameraInputMouse) {
279
  cameraEntity.script.orbitCameraInputMouse.onMouseWheel = function() {};
280
  }
@@ -284,30 +238,18 @@ export async function initializeViewer(config, instanceId) {
284
  distanceSensitivity: 0.5
285
  }
286
  });
287
-
288
  app.root.addChild(cameraEntity);
289
 
290
- // ─── NEW: Ensure camera is reset on the first frame ───────────────────────
291
- app.once('update', function() {
292
- // Call existing resetViewerCamera() so that pivot, yaw/pitch, and distance get
293
- // recalculated correctly before the first render.
294
- resetViewerCamera();
295
- });
296
-
297
- // 9e. Constrain camera’s Y so it never dips below minY
298
- app.on('update', function(dt) {
299
  if (cameraEntity) {
300
  const pos = cameraEntity.getPosition();
301
- if (pos.y < minY) {
302
- cameraEntity.setPosition(pos.x, minY, pos.z);
303
- }
304
  }
305
  });
306
 
307
- // 9f. Resize canvas one last time to fit container
308
- app.resizeCanvas(viewerContainerElem.clientWidth, viewerContainerElem.clientHeight);
309
 
310
- // ─── NEW: Load & initialize the tooltips module ──────────────────────
311
  try {
312
  const tooltipsModule = await import('./tooltips.js');
313
  tooltipsModule.initializeTooltips({
@@ -316,7 +258,7 @@ export async function initializeViewer(config, instanceId) {
316
  modelEntity,
317
  tooltipsUrl: config.tooltips_url,
318
  defaultVisible: !!config.showTooltipsDefault,
319
- moveDuration: config.tooltipMoveDuration || 0.6 // optional smooth‐move duration
320
  });
321
  } catch (e) {
322
  console.error("Error loading tooltips.js:", e);
@@ -328,16 +270,10 @@ export async function initializeViewer(config, instanceId) {
328
 
329
  } catch (error) {
330
  console.error("Error initializing PlayCanvas viewer:", error);
331
- progressDialog.innerHTML = <p style="color: red">Error loading viewer: ${error.message}</p>;
332
  }
333
  }
334
 
335
- /**
336
- * resetViewerCamera()
337
- *
338
- * Re‐centers the camera on the model, using the chosenCameraXYZ from config.
339
- * Exactly the same math as in the old index.jsβ€˜s resetCamera().
340
- */
341
  export function resetViewerCamera() {
342
  try {
343
  if (!cameraEntity || !modelEntity || !app) return;
@@ -345,66 +281,51 @@ export function resetViewerCamera() {
345
  if (!orbitCam) return;
346
 
347
  const modelPos = modelEntity.getPosition();
348
- // Create a temporary entity at chosenCameraX,Y,Z to compute yaw/pitch
349
- const tempEntity = new pc.Entity();
350
- tempEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
351
- tempEntity.lookAt(modelPos);
352
 
353
- // Compute distance
354
- const distance = new pc.Vec3().sub2(
355
  new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ),
356
  modelPos
357
  ).length();
358
 
359
- // Reposition camera
360
  cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
361
  cameraEntity.lookAt(modelPos);
362
 
363
- orbitCam.pivotPoint = new pc.Vec3(modelPos.x, modelPos.y, modelPos.z);
364
- orbitCam._targetDistance = distance;
365
- orbitCam._distance = distance;
366
 
367
- // Compute yaw & pitch from the β€œtempEntity”
368
- const rotation = tempEntity.getRotation();
369
- const tempForward = new pc.Vec3();
370
- rotation.transformVector(pc.Vec3.FORWARD, tempForward);
371
 
372
- const yaw = Math.atan2(-tempForward.x, -tempForward.z) * pc.math.RAD_TO_DEG;
373
  const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);
374
- const rotWithoutYaw = new pc.Quat().mul2(yawQuat, rotation);
375
- const forwardWithoutYaw = new pc.Vec3();
376
- rotWithoutYaw.transformVector(pc.Vec3.FORWARD, forwardWithoutYaw);
377
- const pitch = Math.atan2(forwardWithoutYaw.y, -forwardWithoutYaw.z) * pc.math.RAD_TO_DEG;
378
 
379
  orbitCam._targetYaw = yaw;
380
  orbitCam._yaw = yaw;
381
  orbitCam._targetPitch = pitch;
382
  orbitCam._pitch = pitch;
383
- if (typeof orbitCam._updatePosition === 'function') {
384
- orbitCam._updatePosition();
385
- }
386
 
387
- tempEntity.destroy();
388
- } catch (error) {
389
- console.error("Error resetting camera:", error);
390
  }
391
  }
392
 
393
- /**
394
- * cleanupViewer()
395
- *
396
- * Destroys the PlayCanvas app (if it exists) and clears references. Called when the user clicks β€œX”.
397
- */
398
  export function cleanupViewer() {
399
  if (app) {
400
- try {
401
- app.destroy();
402
- } catch (e) {
403
- console.error("Error destroying PlayCanvas app:", e);
404
- }
405
  app = null;
406
  }
407
- cameraEntity = null;
408
- modelEntity = null;
409
  viewerInitialized = false;
410
  }
 
1
+ // viewer.js
2
  // ==============================
3
  // viewer.js
4
  // ==============================
 
16
 
17
  /**
18
  * initializeViewer(config, instanceId)
 
 
 
 
 
 
19
  */
20
  export async function initializeViewer(config, instanceId) {
21
  if (viewerInitialized) return;
22
 
 
23
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
24
  const isMobile = isIOS || /Android/i.test(navigator.userAgent);
25
 
26
+ // 1. Read config
27
  plyUrl = config.ply_url;
28
  glbUrl = config.glb_url;
29
+ minZoom = parseFloat(config.minZoom || "1");
30
+ maxZoom = parseFloat(config.maxZoom || "20");
31
+ minAngle = parseFloat(config.minAngle || "-45");
32
+ maxAngle = parseFloat(config.maxAngle || "90");
33
+ minAzimuth = (config.minAzimuth !== undefined) ? parseFloat(config.minAzimuth) : -360;
34
+ maxAzimuth = (config.maxAzimuth !== undefined) ? parseFloat(config.maxAzimuth) : 360;
35
+ minPivotY = parseFloat(config.minPivotY || "0");
36
+ minY = (config.minY !== undefined) ? parseFloat(config.minY) : 0;
37
+
38
+ modelX = (config.modelX !== undefined) ? parseFloat(config.modelX) : 0;
39
+ modelY = (config.modelY !== undefined) ? parseFloat(config.modelY) : 0;
40
+ modelZ = (config.modelZ !== undefined) ? parseFloat(config.modelZ) : 0;
41
+ modelScale = (config.modelScale !== undefined) ? parseFloat(config.modelScale) : 1;
 
42
  modelRotationX = (config.modelRotationX !== undefined) ? parseFloat(config.modelRotationX) : 0;
43
  modelRotationY = (config.modelRotationY !== undefined) ? parseFloat(config.modelRotationY) : 0;
44
  modelRotationZ = (config.modelRotationZ !== undefined) ? parseFloat(config.modelRotationZ) : 0;
 
54
  chosenCameraY = isMobile ? cameraYPhone : cameraY;
55
  chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
56
 
57
+ // 2. Grab 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. Create <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
+ viewerContainer.insertBefore(canvas, progressDialog);
71
+
72
+ // 4. Wheel listener
73
+ canvas.addEventListener('wheel', e => {
 
 
 
74
  if (cameraEntity && cameraEntity.script && cameraEntity.script.orbitCamera) {
75
  const orbitCam = cameraEntity.script.orbitCamera;
76
+ const sens = (cameraEntity.script.orbitCameraInputMouse?.distanceSensitivity) || 0.4;
 
77
  if (cameraEntity.camera.projection === pc.PROJECTION_PERSPECTIVE) {
78
+ orbitCam.distance -= e.deltaY * 0.01 * sens * (orbitCam.distance * 0.1);
79
  } else {
80
+ orbitCam.orthoHeight -= e.deltaY * 0.01 * sens * (orbitCam.orthoHeight * 0.1);
81
  }
82
  e.preventDefault();
83
  e.stopPropagation();
 
86
 
87
  progressDialog.style.display = 'block';
88
 
89
+ // 5. Import PlayCanvas
90
  if (!pc) {
91
  pc = await import("https://esm.run/playcanvas");
92
  window.pc = pc;
93
  }
94
 
95
  try {
96
+ // 6. Setup device & app
97
+ const device = await pc.createGraphicsDevice(canvas, {
98
+ deviceTypes: ["webgl2"],
 
99
  glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
100
  twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
101
  antialias: false
102
+ });
 
103
  device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
104
 
105
+ const opts = new pc.AppOptions();
106
+ opts.graphicsDevice = device;
107
+ opts.mouse = new pc.Mouse(canvas);
108
+ opts.touch = new pc.TouchDevice(canvas);
109
+ opts.componentSystems = [
 
 
110
  pc.RenderComponentSystem,
111
  pc.CameraComponentSystem,
112
  pc.LightComponentSystem,
 
115
  pc.CollisionComponentSystem,
116
  pc.RigidbodyComponentSystem
117
  ];
118
+ opts.resourceHandlers = [
119
  pc.TextureHandler,
120
  pc.ContainerHandler,
121
  pc.ScriptHandler,
122
  pc.GSplatHandler
123
  ];
124
 
125
+ app = new pc.Application(canvas, opts);
 
 
 
126
  app.setCanvasFillMode(pc.FILLMODE_NONE);
127
  app.setCanvasResolution(pc.RESOLUTION_AUTO);
128
  app.scene.exposure = 0.5;
129
  app.scene.toneMapping = pc.TONEMAP_ACES;
130
 
131
+ window.addEventListener('resize', () => {
132
+ if (app) app.resizeCanvas(canvas.clientWidth, canvas.clientHeight);
133
+ });
 
 
 
 
134
  app.on('destroy', () => {
135
  window.removeEventListener('resize', resizeCanvas);
136
  });
137
 
138
+ // 7. Assets
139
  const assets = {
140
  model: new pc.Asset('gsplat', 'gsplat', { url: plyUrl }),
141
+ orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
142
  galerie: new pc.Asset('galerie', 'container', { url: glbUrl }),
143
+ hdr: new pc.Asset('hdr', 'texture', {
144
+ url: `https://huggingface.co/datasets/bilca/ply_files/resolve/main/galeries/blanc.png`
145
+ }, { type: pc.TEXTURETYPE_RGBP, mipmaps: false })
 
 
 
146
  };
147
 
148
+ const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
149
+ let lastProg = 0;
150
+ assets.model.on('load', () => progressDialog.style.display = 'none');
151
+ assets.model.on('error', err => {
 
 
 
152
  console.error("Error loading PLY file:", err);
153
+ progressDialog.innerHTML = `<p style="color: red">Error loading model: ${err}</p>`;
154
  });
155
 
156
+ const progCheck = setInterval(() => {
157
  if (assets.model.resource) {
158
  progressIndicator.value = 100;
159
+ clearInterval(progCheck);
160
  progressDialog.style.display = 'none';
161
  } else if (assets.model.loading) {
162
+ lastProg = Math.min(lastProg + 2, 90);
163
+ progressIndicator.value = lastProg;
 
164
  }
165
+ }, 100);
 
166
 
167
+ loader.load(async () => {
 
168
  app.start();
169
  app.scene.envAtlas = assets.hdr.resource;
170
 
 
171
  modelEntity = new pc.Entity('model');
172
  modelEntity.addComponent('gsplat', { asset: assets.model });
173
  modelEntity.setLocalPosition(modelX, modelY, modelZ);
 
175
  modelEntity.setLocalScale(modelScale, modelScale, modelScale);
176
  app.root.addChild(modelEntity);
177
 
 
178
  const dirLight = new pc.Entity('Cascaded Light');
179
  dirLight.addComponent('light', {
180
+ type: 'directional',
181
+ color: pc.Color.WHITE,
182
+ shadowBias: 0.3,
183
  normalOffsetBias: 0.2,
184
+ intensity: 1.0,
185
+ soft: true,
186
  shadowResolution: 4096,
187
+ penumbraSize: 7,
188
  penumbraFalloff: 1.5,
189
+ shadowSamples: 128,
190
  shadowBlockerSamples: 16,
191
+ castShadows: true,
192
+ shadowType: pc.SHADOW_PCSS_32F,
193
  shadowDistance: 1000
194
  });
195
  app.root.addChild(dirLight);
196
  dirLight.setLocalEulerAngles(0, 0, 0);
197
 
 
198
  const galleryEntity = assets.galerie.resource.instantiateRenderEntity();
199
  app.root.addChild(galleryEntity);
200
 
 
201
  cameraEntity = new pc.Entity('camera');
202
  cameraEntity.addComponent('camera', {
203
  clearColor: config.canvas_background
 
205
  : 0,
206
  toneMapping: pc.TONEMAP_ACES
207
  });
 
208
  cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
209
  cameraEntity.lookAt(modelEntity.getPosition());
210
 
 
 
 
 
 
 
 
 
 
211
  cameraEntity.addComponent('script');
212
  cameraEntity.script.create('orbitCamera', {
213
  attributes: {
214
  inertiaFactor: 0.2,
215
+ focusEntity: modelEntity,
216
+ distanceMax: maxZoom,
217
+ distanceMin: minZoom,
218
  pitchAngleMax: maxAngle,
219
  pitchAngleMin: minAngle,
220
+ yawAngleMax: maxAzimuth,
221
+ yawAngleMin: minAzimuth,
222
+ minPivotY: minPivotY,
223
+ frameOnStart: false
224
  }
225
  });
226
  cameraEntity.script.create('orbitCameraInputMouse', {
 
229
  distanceSensitivity: isMobile ? 0.5 : 0.4
230
  }
231
  });
 
232
  if (cameraEntity.script.orbitCameraInputMouse) {
233
  cameraEntity.script.orbitCameraInputMouse.onMouseWheel = function() {};
234
  }
 
238
  distanceSensitivity: 0.5
239
  }
240
  });
 
241
  app.root.addChild(cameraEntity);
242
 
243
+ app.once('update', () => resetViewerCamera());
244
+ app.on('update', dt => {
 
 
 
 
 
 
 
245
  if (cameraEntity) {
246
  const pos = cameraEntity.getPosition();
247
+ if (pos.y < minY) cameraEntity.setPosition(pos.x, minY, pos.z);
 
 
248
  }
249
  });
250
 
251
+ app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
 
252
 
 
253
  try {
254
  const tooltipsModule = await import('./tooltips.js');
255
  tooltipsModule.initializeTooltips({
 
258
  modelEntity,
259
  tooltipsUrl: config.tooltips_url,
260
  defaultVisible: !!config.showTooltipsDefault,
261
+ moveDuration: config.tooltipMoveDuration || 0.6
262
  });
263
  } catch (e) {
264
  console.error("Error loading tooltips.js:", e);
 
270
 
271
  } catch (error) {
272
  console.error("Error initializing PlayCanvas viewer:", error);
273
+ progressDialog.innerHTML = `<p style="color: red">Error loading viewer: ${error.message}</p>`;
274
  }
275
  }
276
 
 
 
 
 
 
 
277
  export function resetViewerCamera() {
278
  try {
279
  if (!cameraEntity || !modelEntity || !app) return;
 
281
  if (!orbitCam) return;
282
 
283
  const modelPos = modelEntity.getPosition();
284
+ const tempEnt = new pc.Entity();
285
+ tempEnt.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
286
+ tempEnt.lookAt(modelPos);
 
287
 
288
+ const dist = new pc.Vec3().sub2(
 
289
  new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ),
290
  modelPos
291
  ).length();
292
 
 
293
  cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
294
  cameraEntity.lookAt(modelPos);
295
 
296
+ orbitCam.pivotPoint = modelPos.clone();
297
+ orbitCam._targetDistance = dist;
298
+ orbitCam._distance = dist;
299
 
300
+ const rot = tempEnt.getRotation();
301
+ const fwd = new pc.Vec3();
302
+ rot.transformVector(pc.Vec3.FORWARD, fwd);
 
303
 
304
+ const yaw = Math.atan2(-fwd.x, -fwd.z) * pc.math.RAD_TO_DEG;
305
  const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);
306
+ const rotNoYaw = new pc.Quat().mul2(yawQuat, rot);
307
+ const fNoYaw = new pc.Vec3();
308
+ rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw);
309
+ const pitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
310
 
311
  orbitCam._targetYaw = yaw;
312
  orbitCam._yaw = yaw;
313
  orbitCam._targetPitch = pitch;
314
  orbitCam._pitch = pitch;
315
+ orbitCam._updatePosition && orbitCam._updatePosition();
 
 
316
 
317
+ tempEnt.destroy();
318
+ } catch (e) {
319
+ console.error("Error resetting camera:", e);
320
  }
321
  }
322
 
 
 
 
 
 
323
  export function cleanupViewer() {
324
  if (app) {
325
+ try { app.destroy(); } catch {}
 
 
 
 
326
  app = null;
327
  }
328
+ cameraEntity = null;
329
+ modelEntity = null;
330
  viewerInitialized = false;
331
  }