MikaFil commited on
Commit
1488910
·
verified ·
1 Parent(s): 28158ac

Update viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +192 -120
viewer.js CHANGED
@@ -3,10 +3,9 @@
3
 
4
  /* -------------------------------------------
5
  Utils
6
- (les helpers image ne sont plus nécessaires pour .sog,
7
- mais on les garde sans effet de bord pour compat ascendante)
8
  -------------------------------------------- */
9
 
 
10
  async function loadImageAsTexture(url, app) {
11
  return new Promise((resolve, reject) => {
12
  const img = new window.Image();
@@ -25,7 +24,7 @@ async function loadImageAsTexture(url, app) {
25
  });
26
  }
27
 
28
- // Patch global Image -> force CORS (sans incidence pour .sog)
29
  (function () {
30
  const OriginalImage = window.Image;
31
  window.Image = function (...args) {
@@ -100,6 +99,7 @@ let cameraEntity = null;
100
  let modelEntity = null;
101
  let viewerInitialized = false;
102
  let resizeObserver = null;
 
103
 
104
  // paramètres courants de l'instance
105
  let chosenCameraX, chosenCameraY, chosenCameraZ;
@@ -109,20 +109,24 @@ let presentoirScaleX, presentoirScaleY, presentoirScaleZ;
109
  let sogUrl, glbUrl, presentoirUrl;
110
  let color_bg_hex, color_bg, espace_expo_bool;
111
 
 
 
 
 
 
 
112
  /* -------------------------------------------
113
  Initialisation
114
  -------------------------------------------- */
115
 
116
  export async function initializeViewer(config, instanceId) {
117
- // ce module ES est importé avec un param unique ?inst=..., donc 1 instance par import
118
  if (viewerInitialized) return;
119
 
120
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
121
  const isMobile = isIOS || /Android/i.test(navigator.userAgent);
122
 
123
  // --- Configuration ---
124
- // Nouveau : utiliser un .sog "bundled" (format SOG PlayCanvas)
125
- // Compat ascendante : on accepte encore sogs_json_url si sog_url absent
126
  sogUrl = config.sog_url || config.sogs_json_url;
127
 
128
  glbUrl =
@@ -171,6 +175,17 @@ export async function initializeViewer(config, instanceId) {
171
  chosenCameraY = isMobile ? cameraYPhone : cameraY;
172
  chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
173
 
 
 
 
 
 
 
 
 
 
 
 
174
  // --- Prépare le canvas unique à cette instance ---
175
  const canvasId = "canvas-" + instanceId;
176
  const progressDialog = document.getElementById("progress-dialog-" + instanceId);
@@ -275,13 +290,15 @@ export async function initializeViewer(config, instanceId) {
275
  twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
276
  antialias: false
277
  });
278
- device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
 
 
279
 
280
  const opts = new pc.AppOptions();
281
  opts.graphicsDevice = device;
282
  opts.mouse = new pc.Mouse(canvas);
283
  opts.touch = new pc.TouchDevice(canvas);
284
- opts.keyboard = new pc.Keyboard(canvas); // clavier scoping canvas
285
  opts.componentSystems = [
286
  pc.RenderComponentSystem,
287
  pc.CameraComponentSystem,
@@ -291,23 +308,29 @@ export async function initializeViewer(config, instanceId) {
291
  pc.CollisionComponentSystem,
292
  pc.RigidbodyComponentSystem
293
  ];
294
- // GSplatHandler gère nativement les .sog (bundled SOG)
295
  opts.resourceHandlers = [pc.TextureHandler, pc.ContainerHandler, pc.ScriptHandler, pc.GSplatHandler];
296
 
297
  app = new pc.Application(canvas, opts);
298
  app.setCanvasFillMode(pc.FILLMODE_NONE);
299
  app.setCanvasResolution(pc.RESOLUTION_AUTO);
300
 
 
301
  resizeObserver = new ResizeObserver((entries) => {
302
- entries.forEach((entry) => {
303
- app.resizeCanvas(entry.contentRect.width, entry.contentRect.height);
304
- });
 
 
305
  });
306
  resizeObserver.observe(viewerContainer);
307
 
308
- window.addEventListener("resize", () =>
309
- app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight)
310
- );
 
 
 
311
 
312
  // Nettoyage complet
313
  app.on("destroy", () => {
@@ -315,7 +338,6 @@ export async function initializeViewer(config, instanceId) {
315
  resizeObserver.disconnect();
316
  } catch {}
317
  if (opts.keyboard && opts.keyboard.detach) opts.keyboard.detach();
318
-
319
  window.removeEventListener("keydown", onKeyDownCapture, true);
320
 
321
  canvas.removeEventListener("pointerenter", onPointerEnter);
@@ -327,126 +349,176 @@ export async function initializeViewer(config, instanceId) {
327
  canvas.removeEventListener("blur", onCanvasBlur);
328
  });
329
 
330
- // --- Enregistre les assets ---
331
- // IMPORTANT : pour .sog on déclare un asset de type "gsplat" avec l'URL .sog
332
- const assets = {
333
- sog: new pc.Asset("gsplat", "gsplat", { url: sogUrl }),
334
- glb: new pc.Asset("glb", "container", { url: glbUrl }),
335
- presentoir: new pc.Asset("presentoir", "container", { url: presentoirUrl })
336
- };
337
- for (const k in assets) app.assets.add(assets[k]);
338
 
339
- const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
 
 
 
 
340
 
341
- // Assure orbit-camera.js une seule fois
342
  await ensureOrbitScriptsLoaded();
343
 
344
- loader.load(() => {
345
- app.start();
346
- progressDialog.style.display = "none";
347
-
348
- // --- Modèle principal (GSplat via .sog) ---
349
- modelEntity = new pc.Entity("model");
350
- modelEntity.addComponent("gsplat", { asset: assets.sog });
351
- modelEntity.setLocalPosition(modelX, modelY, modelZ);
352
- modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
353
- modelEntity.setLocalScale(modelScale, modelScale, modelScale);
354
- app.root.addChild(modelEntity);
355
-
356
- // --- Sol / environnement ---
357
- const glbEntity = assets.glb.resource.instantiateRenderEntity();
358
- app.root.addChild(glbEntity);
359
-
360
- const presentoirEntity = assets.presentoir.resource.instantiateRenderEntity();
361
- presentoirEntity.setLocalScale(presentoirScaleX, presentoirScaleY, presentoirScaleZ);
362
- app.root.addChild(presentoirEntity);
363
-
364
- if (!espace_expo_bool) {
365
- const matSol = new pc.StandardMaterial();
366
- matSol.blendType = pc.BLEND_NONE;
367
- matSol.emissive = new pc.Color(color_bg);
368
- matSol.emissiveIntensity = 1;
369
- matSol.useLighting = false;
370
- matSol.update();
371
-
372
- traverse(presentoirEntity, (node) => {
373
- if (node.render && node.render.meshInstances) {
374
- for (const mi of node.render.meshInstances) mi.material = matSol;
375
- }
376
- });
377
-
378
- traverse(glbEntity, (node) => {
379
- if (node.render && node.render.meshInstances) {
380
- for (const mi of node.render.meshInstances) mi.material = matSol;
381
- }
382
- });
383
 
384
- ////// MODIFIE A LA MANO FAIRE GAFFE //////
385
- glbEntity.setLocalScale(10, 10, 10);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
 
388
- // --- Caméra + scripts d’input (disponibles car orbit chargé globalement) ---
389
- cameraEntity = new pc.Entity("camera");
390
- cameraEntity.addComponent("camera", {
391
- clearColor: new pc.Color(color_bg),
392
- nearClip: 0.001,
393
- farClip: 100
394
- });
395
- cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
396
- cameraEntity.lookAt(modelEntity.getPosition());
397
- cameraEntity.addComponent("script");
398
-
399
- cameraEntity.script.create("orbitCamera", {
400
- attributes: {
401
- focusEntity: modelEntity,
402
- inertiaFactor: 0.2,
403
- distanceMax: maxZoom,
404
- distanceMin: minZoom,
405
- pitchAngleMax: maxAngle,
406
- pitchAngleMin: minAngle,
407
- yawAngleMax: maxAzimuth,
408
- yawAngleMin: minAzimuth,
409
- minY: minY,
410
- frameOnStart: false
 
 
 
 
 
 
411
  }
412
- });
413
- cameraEntity.script.create("orbitCameraInputMouse");
414
- cameraEntity.script.create("orbitCameraInputTouch");
415
- cameraEntity.script.create("orbitCameraInputKeyboard", {
416
- attributes: {
417
- forwardSpeed: 1.2,
418
- strafeSpeed: 1.2
419
  }
420
- });
421
- app.root.addChild(cameraEntity);
422
 
423
- app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
424
- app.once("update", () => resetViewerCamera());
 
 
 
 
 
 
 
 
 
 
 
 
 
425
 
426
- // --- Tooltips (optionnels) ---
427
- try {
428
- if (config.tooltips_url) {
429
- import("./tooltips.js")
430
- .then((tooltipsModule) => {
431
- tooltipsModule.initializeTooltips({
432
- app,
433
- cameraEntity,
434
- modelEntity,
435
- tooltipsUrl: config.tooltips_url,
436
- defaultVisible: !!config.showTooltipsDefault,
437
- moveDuration: config.tooltipMoveDuration || 0.6
438
- });
439
- })
440
- .catch(() => {
441
- /* optional */
442
  });
 
 
 
 
443
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
  } catch (e) {
445
- /* optional */
446
  }
 
447
 
448
- viewerInitialized = true;
449
- });
450
  }
451
 
452
  /* -------------------------------------------
 
3
 
4
  /* -------------------------------------------
5
  Utils
 
 
6
  -------------------------------------------- */
7
 
8
+ // (Conservé pour compat, même si .sog n'en a pas besoin)
9
  async function loadImageAsTexture(url, app) {
10
  return new Promise((resolve, reject) => {
11
  const img = new window.Image();
 
24
  });
25
  }
26
 
27
+ // Patch global Image -> force CORS
28
  (function () {
29
  const OriginalImage = window.Image;
30
  window.Image = function (...args) {
 
99
  let modelEntity = null;
100
  let viewerInitialized = false;
101
  let resizeObserver = null;
102
+ let resizeTimeout = null;
103
 
104
  // paramètres courants de l'instance
105
  let chosenCameraX, chosenCameraY, chosenCameraZ;
 
109
  let sogUrl, glbUrl, presentoirUrl;
110
  let color_bg_hex, color_bg, espace_expo_bool;
111
 
112
+ // perf dynamique
113
+ let maxDevicePixelRatio = 1.75; // plafond par défaut (configurable)
114
+ let interactDpr = 1.0; // DPR pendant interaction
115
+ let idleRestoreDelay = 350; // ms avant de restaurer le DPR
116
+ let idleTimer = null;
117
+
118
  /* -------------------------------------------
119
  Initialisation
120
  -------------------------------------------- */
121
 
122
  export async function initializeViewer(config, instanceId) {
 
123
  if (viewerInitialized) return;
124
 
125
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
126
  const isMobile = isIOS || /Android/i.test(navigator.userAgent);
127
 
128
  // --- Configuration ---
129
+ // Nouveau format .sog (et compat si seul sogs_json_url est présent)
 
130
  sogUrl = config.sog_url || config.sogs_json_url;
131
 
132
  glbUrl =
 
175
  chosenCameraY = isMobile ? cameraYPhone : cameraY;
176
  chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
177
 
178
+ // Options perf configurables
179
+ if (config.maxDevicePixelRatio !== undefined) {
180
+ maxDevicePixelRatio = Math.max(0.75, parseFloat(config.maxDevicePixelRatio) || maxDevicePixelRatio);
181
+ }
182
+ if (config.interactionPixelRatio !== undefined) {
183
+ interactDpr = Math.max(0.75, parseFloat(config.interactionPixelRatio) || interactDpr);
184
+ }
185
+ if (config.idleRestoreDelayMs !== undefined) {
186
+ idleRestoreDelay = Math.max(120, parseInt(config.idleRestoreDelayMs, 10) || idleRestoreDelay);
187
+ }
188
+
189
  // --- Prépare le canvas unique à cette instance ---
190
  const canvasId = "canvas-" + instanceId;
191
  const progressDialog = document.getElementById("progress-dialog-" + instanceId);
 
290
  twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
291
  antialias: false
292
  });
293
+
294
+ // Cap DPR pour limiter le coût CPU/GPU
295
+ device.maxPixelRatio = Math.min(window.devicePixelRatio || 1, maxDevicePixelRatio);
296
 
297
  const opts = new pc.AppOptions();
298
  opts.graphicsDevice = device;
299
  opts.mouse = new pc.Mouse(canvas);
300
  opts.touch = new pc.TouchDevice(canvas);
301
+ opts.keyboard = new pc.Keyboard(canvas);
302
  opts.componentSystems = [
303
  pc.RenderComponentSystem,
304
  pc.CameraComponentSystem,
 
308
  pc.CollisionComponentSystem,
309
  pc.RigidbodyComponentSystem
310
  ];
311
+ // GSplatHandler gère nativement les .sog
312
  opts.resourceHandlers = [pc.TextureHandler, pc.ContainerHandler, pc.ScriptHandler, pc.GSplatHandler];
313
 
314
  app = new pc.Application(canvas, opts);
315
  app.setCanvasFillMode(pc.FILLMODE_NONE);
316
  app.setCanvasResolution(pc.RESOLUTION_AUTO);
317
 
318
+ // --- Debounce resize (moins de rafales) ---
319
  resizeObserver = new ResizeObserver((entries) => {
320
+ if (!entries || !entries.length) return;
321
+ if (resizeTimeout) clearTimeout(resizeTimeout);
322
+ resizeTimeout = setTimeout(() => {
323
+ app.resizeCanvas(entries[0].contentRect.width, entries[0].contentRect.height);
324
+ }, 60);
325
  });
326
  resizeObserver.observe(viewerContainer);
327
 
328
+ window.addEventListener("resize", () => {
329
+ if (resizeTimeout) clearTimeout(resizeTimeout);
330
+ resizeTimeout = setTimeout(() => {
331
+ app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
332
+ }, 60);
333
+ });
334
 
335
  // Nettoyage complet
336
  app.on("destroy", () => {
 
338
  resizeObserver.disconnect();
339
  } catch {}
340
  if (opts.keyboard && opts.keyboard.detach) opts.keyboard.detach();
 
341
  window.removeEventListener("keydown", onKeyDownCapture, true);
342
 
343
  canvas.removeEventListener("pointerenter", onPointerEnter);
 
349
  canvas.removeEventListener("blur", onCanvasBlur);
350
  });
351
 
352
+ // --- Enregistre les assets en 2 phases ---
353
+ // Phase 1 : .sog (priorité) time-to-first-pixel
354
+ const sogAsset = new pc.Asset("gsplat", "gsplat", { url: sogUrl });
355
+ app.assets.add(sogAsset);
 
 
 
 
356
 
357
+ // Phase 2 (déférée) : glb + présentoir
358
+ const glbAsset = new pc.Asset("glb", "container", { url: glbUrl });
359
+ const presentoirAsset = new pc.Asset("presentoir", "container", { url: presentoirUrl });
360
+ app.assets.add(glbAsset);
361
+ app.assets.add(presentoirAsset);
362
 
363
+ // Assure orbit-camera.js (nécessaire pour les scripts d'input)
364
  await ensureOrbitScriptsLoaded();
365
 
366
+ // ---------- PHASE 1 : charger le .sog, démarrer tout de suite ----------
367
+ await new Promise((resolve, reject) => {
368
+ const loader1 = new pc.AssetListLoader([sogAsset], app.assets);
369
+ loader1.load(() => resolve());
370
+ loader1.on('error', reject);
371
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
 
373
+ app.start(); // démarre l'update loop le plus tôt possible
374
+ progressDialog.style.display = "none";
375
+
376
+ // --- Modèle principal (GSplat via .sog) ---
377
+ modelEntity = new pc.Entity("model");
378
+ modelEntity.addComponent("gsplat", { asset: sogAsset });
379
+ modelEntity.setLocalPosition(modelX, modelY, modelZ);
380
+ modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
381
+ modelEntity.setLocalScale(modelScale, modelScale, modelScale);
382
+ app.root.addChild(modelEntity);
383
+
384
+ // --- Caméra + scripts d’input ---
385
+ cameraEntity = new pc.Entity("camera");
386
+ cameraEntity.addComponent("camera", {
387
+ clearColor: new pc.Color(color_bg),
388
+ nearClip: 0.001,
389
+ farClip: 100
390
+ });
391
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
392
+ cameraEntity.lookAt(modelEntity.getPosition());
393
+ cameraEntity.addComponent("script");
394
+
395
+ cameraEntity.script.create("orbitCamera", {
396
+ attributes: {
397
+ focusEntity: modelEntity,
398
+ inertiaFactor: 0.2,
399
+ distanceMax: maxZoom,
400
+ distanceMin: minZoom,
401
+ pitchAngleMax: maxAngle,
402
+ pitchAngleMin: minAngle,
403
+ yawAngleMax: maxAzimuth,
404
+ yawAngleMin: minAzimuth,
405
+ minY: minY,
406
+ frameOnStart: false
407
  }
408
+ });
409
+ cameraEntity.script.create("orbitCameraInputMouse");
410
+ cameraEntity.script.create("orbitCameraInputTouch");
411
+ cameraEntity.script.create("orbitCameraInputKeyboard", {
412
+ attributes: {
413
+ forwardSpeed: 1.2,
414
+ strafeSpeed: 1.2
415
+ }
416
+ });
417
+ app.root.addChild(cameraEntity);
418
+
419
+ // Taille initiale
420
+ app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
421
+ app.once("update", () => resetViewerCamera());
422
+
423
+ // ---------- Perf dynamique : DPR temporairement réduit pendant interaction ----------
424
+ const setDpr = (val) => {
425
+ const clamped = Math.max(0.5, Math.min(val, maxDevicePixelRatio));
426
+ if (app.graphicsDevice.maxPixelRatio !== clamped) {
427
+ app.graphicsDevice.maxPixelRatio = clamped;
428
+ // Force un resize pour appliquer immédiatement
429
+ app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
430
+ }
431
+ };
432
 
433
+ const bumpInteraction = () => {
434
+ // baisse DPR pendant l'interaction
435
+ setDpr(interactDpr);
436
+ if (idleTimer) clearTimeout(idleTimer);
437
+ idleTimer = setTimeout(() => {
438
+ // restaure DPR max quand l'utilisateur s'arrête
439
+ setDpr(Math.min(window.devicePixelRatio || 1, maxDevicePixelRatio));
440
+ }, idleRestoreDelay);
441
+ };
442
+
443
+ // Hooks d'interaction
444
+ const interactionEvents = ["mousedown", "mousemove", "mouseup", "wheel", "touchstart", "touchmove", "keydown"];
445
+ interactionEvents.forEach((ev) => {
446
+ canvas.addEventListener(ev, bumpInteraction, { passive: true });
447
+ });
448
+
449
+ // ---------- PHASE 2 (déférée) : charger sol/environnement et tooltips ----------
450
+ // On charge le reste sans bloquer l’affichage initial
451
+ setTimeout(async () => {
452
+ try {
453
+ await new Promise((resolve) => {
454
+ const loader2 = new pc.AssetListLoader([glbAsset, presentoirAsset], app.assets);
455
+ loader2.load(() => resolve());
456
+ });
457
+
458
+ // Sol / environnement
459
+ const glbEntity = glbAsset.resource ? glbAsset.resource.instantiateRenderEntity() : null;
460
+ if (glbEntity) {
461
+ app.root.addChild(glbEntity);
462
  }
463
+
464
+ const presentoirEntity =
465
+ presentoirAsset.resource ? presentoirAsset.resource.instantiateRenderEntity() : null;
466
+ if (presentoirEntity) {
467
+ presentoirEntity.setLocalScale(presentoirScaleX, presentoirScaleY, presentoirScaleZ);
468
+ app.root.addChild(presentoirEntity);
 
469
  }
 
 
470
 
471
+ if (!espace_expo_bool) {
472
+ const matSol = new pc.StandardMaterial();
473
+ matSol.blendType = pc.BLEND_NONE;
474
+ matSol.emissive = new pc.Color(color_bg);
475
+ matSol.emissiveIntensity = 1;
476
+ matSol.useLighting = false;
477
+ matSol.update();
478
+
479
+ if (presentoirEntity) {
480
+ traverse(presentoirEntity, (node) => {
481
+ if (node.render && node.render.meshInstances) {
482
+ for (const mi of node.render.meshInstances) mi.material = matSol;
483
+ }
484
+ });
485
+ }
486
 
487
+ if (glbEntity) {
488
+ traverse(glbEntity, (node) => {
489
+ if (node.render && node.render.meshInstances) {
490
+ for (const mi of node.render.meshInstances) mi.material = matSol;
491
+ }
 
 
 
 
 
 
 
 
 
 
 
492
  });
493
+
494
+ // ////// MODIFIE A LA MANO FAIRE GAFFE //////
495
+ glbEntity.setLocalScale(10, 10, 10);
496
+ }
497
  }
498
+
499
+ // Tooltips (optionnels) — chargés après premier rendu
500
+ try {
501
+ if (config.tooltips_url) {
502
+ import("./tooltips.js")
503
+ .then((tooltipsModule) => {
504
+ tooltipsModule.initializeTooltips({
505
+ app,
506
+ cameraEntity,
507
+ modelEntity,
508
+ tooltipsUrl: config.tooltips_url,
509
+ defaultVisible: !!config.showTooltipsDefault,
510
+ moveDuration: config.tooltipMoveDuration || 0.6
511
+ });
512
+ })
513
+ .catch(() => { /* optional */ });
514
+ }
515
+ } catch (e) { /* optional */ }
516
  } catch (e) {
517
+ console.warn("[viewer.js] Deferred assets load failed:", e);
518
  }
519
+ }, 0);
520
 
521
+ viewerInitialized = true;
 
522
  }
523
 
524
  /* -------------------------------------------