MikaFil commited on
Commit
96bde52
Β·
verified Β·
1 Parent(s): d10bcd5

Update viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +149 -231
viewer.js CHANGED
@@ -2,7 +2,7 @@
2
  // viewer.js
3
  // ==============================
4
 
5
- let pc; // will hold the PlayCanvas module once imported
6
  export let app = null;
7
  let cameraEntity = null;
8
  let modelEntity = null;
@@ -13,82 +13,67 @@ 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
- * - 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;
52
-
53
- const cameraX = (config.cameraX !== undefined) ? parseFloat(config.cameraX) : 0;
54
- const cameraY = (config.cameraY !== undefined) ? parseFloat(config.cameraY) : 2;
55
- const cameraZ = (config.cameraZ !== undefined) ? parseFloat(config.cameraZ) : 5;
56
- const cameraXPhone = (config.cameraXPhone !== undefined) ? parseFloat(config.cameraXPhone) : cameraX;
57
- const cameraYPhone = (config.cameraYPhone !== undefined) ? parseFloat(config.cameraYPhone) : cameraY;
58
- const cameraZPhone = (config.cameraZPhone !== undefined) ? parseFloat(config.cameraZPhone) : (cameraZ * 1.5);
59
-
60
- chosenCameraX = isMobile ? cameraXPhone : cameraX;
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 +82,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,160 +111,124 @@ 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: "https://mikafil-viewer-gs.static.hf.space/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);
206
  modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
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
239
- ? parseInt(config.canvas_background.substr(1, 2), 16) / 255
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: { inertiaFactor: 0.2, focusEntity: modelEntity, distanceMax: maxZoom, distanceMin: minZoom, pitchAngleMax: maxAngle, pitchAngleMin: minAngle, yawAngleMax: maxAzimuth, yawAngleMin: minAzimuth, minPivotY, frameOnStart: false }
 
 
 
 
 
 
 
 
 
 
 
259
  });
260
- cameraEntity.script.create('orbitCameraInputMouse', {
261
- attributes: { orbitSensitivity: isMobile ? 0.6 : 0.3, distanceSensitivity: isMobile ? 0.5 : 0.4 }
262
  });
263
- if (cameraEntity.script.orbitCameraInputMouse) {
264
- cameraEntity.script.orbitCameraInputMouse.onMouseWheel = function() {};
265
- }
266
- cameraEntity.script.create('orbitCameraInputTouch', {
267
- attributes: { orbitSensitivity: 0.6, distanceSensitivity: 0.5 }
268
  });
269
  app.root.addChild(cameraEntity);
270
 
271
- // Ensure camera is reset on the first frame
272
  app.once('update', () => resetViewerCamera());
273
 
274
- // Constrain camera’s Y so it never dips below minY
275
- app.on('update', dt => {
276
- const pos = cameraEntity.getPosition();
277
- if (pos.y < minY) cameraEntity.setPosition(pos.x, minY, pos.z);
278
  });
279
 
280
  // Final resize
281
- app.resizeCanvas(viewerContainerElem.clientWidth, viewerContainerElem.clientHeight);
282
 
283
  // Initialize tooltips
284
  try {
285
- const tooltipsModule = await import('./tooltips.js');
286
- tooltipsModule.initializeTooltips({
287
  app,
288
  cameraEntity,
289
  modelEntity,
@@ -299,80 +244,53 @@ export async function initializeViewer(config, instanceId) {
299
  viewerInitialized = true;
300
  });
301
 
302
- } catch (error) {
303
- console.error("Error initializing PlayCanvas viewer:", error);
304
- progressDialog.innerHTML = `<p style="color: red">Error loading viewer: ${error.message}</p>`;
305
  }
306
- } // ← Added this closing brace to balance initializeViewer
307
 
308
- /**
309
- * resetViewerCamera()
310
- *
311
- * Re‐centers the camera on the model, using the chosenCameraXYZ from config.
312
- */
313
  export function resetViewerCamera() {
314
  try {
315
  if (!cameraEntity || !modelEntity || !app) return;
316
  const orbitCam = cameraEntity.script.orbitCamera;
317
  if (!orbitCam) return;
318
 
319
- const modelPos = modelEntity.getPosition();
320
- const tempEntity = new pc.Entity();
321
- tempEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
322
- tempEntity.lookAt(modelPos);
323
 
324
- const distance = new pc.Vec3().sub2(
325
- new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ),
326
- modelPos
327
  ).length();
328
 
329
  cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
330
- cameraEntity.lookAt(modelPos);
331
 
332
- orbitCam.pivotPoint = new pc.Vec3(modelPos.x, modelPos.y, modelPos.z);
333
- orbitCam._targetDistance = distance;
334
- orbitCam._distance = distance;
335
 
336
- const rotation = tempEntity.getRotation();
337
- const tempForward = new pc.Vec3();
338
- rotation.transformVector(pc.Vec3.FORWARD, tempForward);
339
-
340
- const yaw = Math.atan2(-tempForward.x, -tempForward.z) * pc.math.RAD_TO_DEG;
341
  const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);
342
- const rotWithoutYaw = new pc.Quat().mul2(yawQuat, rotation);
343
- const forwardWithoutYaw = new pc.Vec3();
344
- rotWithoutYaw.transformVector(pc.Vec3.FORWARD, forwardWithoutYaw);
345
- const pitch = Math.atan2(forwardWithoutYaw.y, -forwardWithoutYaw.z) * pc.math.RAD_TO_DEG;
346
 
347
- orbitCam._targetYaw = yaw;
348
  orbitCam._yaw = yaw;
349
- orbitCam._targetPitch = pitch;
350
  orbitCam._pitch = pitch;
351
- if (typeof orbitCam._updatePosition === 'function') {
352
- orbitCam._updatePosition();
353
- }
354
-
355
- tempEntity.destroy();
356
- } catch (error) {
357
- console.error("Error resetting camera:", error);
358
- }
359
- }
360
 
361
- /**
362
- * cleanupViewer()
363
- *
364
- * Destroys the PlayCanvas `app` and clears references.
365
- */
366
- export function cleanupViewer() {
367
- if (app) {
368
- try {
369
- app.destroy();
370
- } catch (e) {
371
- console.error("Error destroying PlayCanvas app:", e);
372
- }
373
- app = null;
374
  }
375
- cameraEntity = null;
376
- modelEntity = null;
377
- viewerInitialized = false;
378
  }
 
2
  // viewer.js
3
  // ==============================
4
 
5
+ let pc; // PlayCanvas module
6
  export let app = null;
7
  let cameraEntity = null;
8
  let modelEntity = null;
 
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
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
20
  const isMobile = isIOS || /Android/i.test(navigator.userAgent);
21
 
22
+ // 1. Read config
23
  plyUrl = config.ply_url;
24
  glbUrl = config.glb_url;
25
 
26
+ minZoom = parseFloat(config.minZoom || "1");
27
+ maxZoom = parseFloat(config.maxZoom || "20");
28
+ minAngle = parseFloat(config.minAngle || "-45");
29
+ maxAngle = parseFloat(config.maxAngle || "90");
30
+ minAzimuth = config.minAzimuth !== undefined ? parseFloat(config.minAzimuth) : -360;
31
+ maxAzimuth = config.maxAzimuth !== undefined ? parseFloat(config.maxAzimuth) : 360;
32
+ minPivotY = parseFloat(config.minPivotY || "0");
33
+ minY = config.minY !== undefined ? parseFloat(config.minY) : 0;
34
+
35
+ modelX = parseFloat(config.modelX || "0");
36
+ modelY = parseFloat(config.modelY || "0");
37
+ modelZ = parseFloat(config.modelZ || "0");
38
+ modelScale = parseFloat(config.modelScale || "1");
39
+ modelRotationX = parseFloat(config.modelRotationX || "0");
40
+ modelRotationY = parseFloat(config.modelRotationY || "0");
41
+ modelRotationZ = parseFloat(config.modelRotationZ || "0");
42
+
43
+ const camX = parseFloat(config.cameraX || "0");
44
+ const camY = parseFloat(config.cameraY || "2");
45
+ const camZ = parseFloat(config.cameraZ || "5");
46
+ const camXPhone = parseFloat(config.cameraXPhone || camX);
47
+ const camYPhone = parseFloat(config.cameraYPhone || camY);
48
+ const camZPhone = parseFloat(config.cameraZPhone || (camZ * 1.5));
49
+
50
+ chosenCameraX = isMobile ? camXPhone : camX;
51
+ chosenCameraY = isMobile ? camYPhone : camY;
52
+ chosenCameraZ = isMobile ? camZPhone : camZ;
53
+
54
+ // 2. Grab DOM
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. Create <canvas>
61
+ let old = document.getElementById(canvasId);
62
+ if (old) old.remove();
63
+ const canvas = document.createElement('canvas');
64
+ canvas.id = canvasId;
65
+ canvas.className = 'ply-canvas';
66
+ viewerContainer.insertBefore(canvas, progressDialog);
67
+
68
+ // 4. Wheel zoom
69
+ canvas.addEventListener('wheel', e => {
 
 
 
 
70
  if (cameraEntity && cameraEntity.script && cameraEntity.script.orbitCamera) {
71
+ const oc = cameraEntity.script.orbitCamera;
72
+ const sens = (cameraEntity.script.orbitCameraInputMouse?.distanceSensitivity) || 0.4;
 
73
  if (cameraEntity.camera.projection === pc.PROJECTION_PERSPECTIVE) {
74
+ oc.distance -= e.deltaY * 0.01 * sens * (oc.distance * 0.1);
75
  } else {
76
+ oc.orthoHeight -= e.deltaY * 0.01 * sens * (oc.orthoHeight * 0.1);
77
  }
78
  e.preventDefault();
79
  e.stopPropagation();
 
82
 
83
  progressDialog.style.display = 'block';
84
 
85
+ // 5. Import PlayCanvas
86
  if (!pc) {
87
  pc = await import("https://esm.run/playcanvas");
88
  window.pc = pc;
89
  }
90
 
91
  try {
92
+ // 6. Create device & app
93
+ const device = await pc.createGraphicsDevice(canvas, {
94
+ deviceTypes: ["webgl2"],
 
95
  glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
96
  twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
97
  antialias: false
98
+ });
 
99
  device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
100
 
101
+ const opts = new pc.AppOptions();
102
+ opts.graphicsDevice = device;
103
+ opts.mouse = new pc.Mouse(canvas);
104
+ opts.touch = new pc.TouchDevice(canvas);
105
+ opts.componentSystems = [
 
 
106
  pc.RenderComponentSystem,
107
  pc.CameraComponentSystem,
108
  pc.LightComponentSystem,
 
111
  pc.CollisionComponentSystem,
112
  pc.RigidbodyComponentSystem
113
  ];
114
+ opts.resourceHandlers = [
115
  pc.TextureHandler,
116
  pc.ContainerHandler,
117
  pc.ScriptHandler,
118
  pc.GSplatHandler
119
  ];
120
 
121
+ app = new pc.Application(canvas, opts);
 
 
 
122
  app.setCanvasFillMode(pc.FILLMODE_NONE);
123
  app.setCanvasResolution(pc.RESOLUTION_AUTO);
124
  app.scene.exposure = 0.5;
125
  app.scene.toneMapping = pc.TONEMAP_ACES;
126
 
127
+ // 7. Resize on window resize
128
  const resizeCanvas = () => {
129
+ app.resizeCanvas(canvas.clientWidth, canvas.clientHeight);
 
 
130
  };
131
  window.addEventListener('resize', resizeCanvas);
132
  app.on('destroy', () => {
133
  window.removeEventListener('resize', resizeCanvas);
134
  });
135
 
136
+ // 8. Load assets
137
  const assets = {
138
+ model: new pc.Asset('gsplat','gsplat',{url: plyUrl}),
139
+ orbit: new pc.Asset('script','script',{url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js"}),
140
+ galerie: new pc.Asset('galerie','container',{url: glbUrl}),
141
+ hdr: new pc.Asset('hdr','texture',{url: `https://huggingface.co/datasets/bilca/ply_files/resolve/main/galeries/blanc.png`},{ type: pc.TEXTURETYPE_RGBP, mipmaps: false })
 
 
 
 
 
142
  };
143
+ const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
144
+ let lastProg = 0;
145
+ assets.model.on('load', () => progressDialog.style.display = 'none');
146
+ assets.model.on('error', e => {
147
+ console.error("Error loading PLY file:", e);
148
+ progressDialog.innerHTML = `<p style="color:red">Error loading model: ${e}</p>`;
 
 
 
 
149
  });
150
+ const checkProg = () => {
 
151
  if (assets.model.resource) {
152
  progressIndicator.value = 100;
153
+ clearInterval(interval);
154
  progressDialog.style.display = 'none';
155
  } else if (assets.model.loading) {
156
+ lastProg = Math.min(lastProg + 2, 90);
157
+ progressIndicator.value = lastProg;
 
158
  }
159
  };
160
+ const interval = setInterval(checkProg, 100);
161
 
162
+ // 9. Build scene
163
+ loader.load(async () => {
164
  app.start();
165
  app.scene.envAtlas = assets.hdr.resource;
166
 
167
+ // Model
168
  modelEntity = new pc.Entity('model');
169
+ modelEntity.addComponent('gsplat',{asset:assets.model});
170
  modelEntity.setLocalPosition(modelX, modelY, modelZ);
171
  modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
172
  modelEntity.setLocalScale(modelScale, modelScale, modelScale);
173
  app.root.addChild(modelEntity);
174
 
175
+ // Light
176
+ const light = new pc.Entity('light');
177
+ light.addComponent('light',{ type:'directional', color:pc.Color.WHITE,
178
+ intensity:1, shadowType:pc.SHADOW_PCF3, castShadows:true });
179
+ app.root.addChild(light);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
+ // Galerie GLB
182
+ const gal = assets.galerie.resource.instantiateRenderEntity();
183
+ app.root.addChild(gal);
184
 
185
+ // Camera
186
  cameraEntity = new pc.Entity('camera');
187
+ cameraEntity.addComponent('camera',{
188
+ clearColor: config.canvas_background ? parseInt(config.canvas_background.substr(1,2),16)/255 : 0,
 
 
189
  toneMapping: pc.TONEMAP_ACES
190
  });
 
191
  cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
192
  cameraEntity.lookAt(modelEntity.getPosition());
 
 
 
 
 
 
 
 
 
 
193
  cameraEntity.addComponent('script');
194
+ cameraEntity.script.create('orbitCamera',{
195
+ attributes:{
196
+ inertiaFactor:0.2,
197
+ focusEntity:modelEntity,
198
+ distanceMax:maxZoom,
199
+ distanceMin:minZoom,
200
+ pitchAngleMax:maxAngle,
201
+ pitchAngleMin:minAngle,
202
+ yawAngleMax:maxAzimuth,
203
+ yawAngleMin:minAzimuth,
204
+ minPivotY:minPivotY,
205
+ frameOnStart:false
206
+ }
207
  });
208
+ cameraEntity.script.create('orbitCameraInputMouse',{
209
+ attributes:{ orbitSensitivity:isMobile?0.6:0.3, distanceSensitivity:isMobile?0.5:0.4 }
210
  });
211
+ cameraEntity.script.create('orbitCameraInputTouch',{
212
+ attributes:{ orbitSensitivity:0.6, distanceSensitivity:0.5 }
 
 
 
213
  });
214
  app.root.addChild(cameraEntity);
215
 
216
+ // First‐frame reset
217
  app.once('update', () => resetViewerCamera());
218
 
219
+ // Constrain Y
220
+ app.on('update', () => {
221
+ const p = cameraEntity.getPosition();
222
+ if (p.y < minY) cameraEntity.setPosition(p.x, minY, p.z);
223
  });
224
 
225
  // Final resize
226
+ app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
227
 
228
  // Initialize tooltips
229
  try {
230
+ const tt = await import('./tooltips.js');
231
+ tt.initializeTooltips({
232
  app,
233
  cameraEntity,
234
  modelEntity,
 
244
  viewerInitialized = true;
245
  });
246
 
247
+ } catch (err) {
248
+ console.error("Error initializing PlayCanvas viewer:", err);
249
+ progressDialog.innerHTML = `<p style="color:red">${err.message}</p>`;
250
  }
251
+ }
252
 
 
 
 
 
 
253
  export function resetViewerCamera() {
254
  try {
255
  if (!cameraEntity || !modelEntity || !app) return;
256
  const orbitCam = cameraEntity.script.orbitCamera;
257
  if (!orbitCam) return;
258
 
259
+ const mp = modelEntity.getPosition();
260
+ const temp = new pc.Entity();
261
+ temp.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
262
+ temp.lookAt(mp);
263
 
264
+ const dist = new pc.Vec3().sub2(
265
+ new pc.Vec3(chosenCameraX,chosenCameraY,chosenCameraZ),
266
+ mp
267
  ).length();
268
 
269
  cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
270
+ cameraEntity.lookAt(mp);
271
 
272
+ orbitCam.pivotPoint = new pc.Vec3(mp.x,mp.y,mp.z);
273
+ orbitCam._distance = dist;
274
+ orbitCam._targetDistance = dist;
275
 
276
+ const rot = temp.getRotation();
277
+ const fw = new pc.Vec3();
278
+ rot.transformVector(pc.Vec3.FORWARD, fw);
279
+ const yaw = Math.atan2(-fw.x,-fw.z) * pc.math.RAD_TO_DEG;
 
280
  const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);
281
+ const noYaw = new pc.Quat().mul2(yawQuat, rot);
282
+ const fny = new pc.Vec3();
283
+ noYaw.transformVector(pc.Vec3.FORWARD, fny);
284
+ const pitch = Math.atan2(fny.y, -fny.z) * pc.math.RAD_TO_DEG;
285
 
 
286
  orbitCam._yaw = yaw;
287
+ orbitCam._targetYaw = yaw;
288
  orbitCam._pitch = pitch;
289
+ orbitCam._targetPitch = pitch;
290
+ orbitCam._updatePosition();
 
 
 
 
 
 
 
291
 
292
+ temp.destroy();
293
+ } catch(e) {
294
+ console.error("Error resetting camera:", e);
 
 
 
 
 
 
 
 
 
 
295
  }
 
 
 
296
  }