MikaFil commited on
Commit
32d74ef
·
verified ·
1 Parent(s): 62ebf50

Update viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +122 -498
viewer.js CHANGED
@@ -1,589 +1,213 @@
1
- // viewer.js
2
- // ==============================
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();
12
- img.crossOrigin = "anonymous";
13
- img.onload = function () {
14
- const tex = new pc.Texture(app.graphicsDevice, {
15
- width: img.width,
16
- height: img.height,
17
- format: pc.PIXELFORMAT_R8_G8_B8_A8
18
- });
19
- tex.setSource(img);
20
- resolve(tex);
21
- };
22
- img.onerror = reject;
23
- img.src = url;
24
  });
25
  }
26
-
27
- // Patch global Image -> force CORS
28
  (function () {
29
  const OriginalImage = window.Image;
30
  window.Image = function (...args) {
31
- const img = new OriginalImage(...args);
32
- img.crossOrigin = "anonymous";
33
- return img;
34
- };
35
- })();
36
-
37
- function hexToRgbaArray(hex) {
38
- try {
39
- hex = String(hex || "").replace("#", "");
40
- if (hex.length === 6) hex += "FF";
41
- if (hex.length !== 8) return [1, 1, 1, 1];
42
- const num = parseInt(hex, 16);
43
- return [
44
- ((num >> 24) & 0xff) / 255,
45
- ((num >> 16) & 0xff) / 255,
46
- ((num >> 8) & 0xff) / 255,
47
- (num & 0xff) / 255
48
- ];
49
- } catch (e) {
50
- console.warn("hexToRgbaArray error:", e);
51
- return [1, 1, 1, 1];
52
- }
53
- }
54
-
55
- // Parcours récursif d'une hiérarchie d'entités
56
- function traverse(entity, callback) {
57
- callback(entity);
58
- if (entity.children) {
59
- entity.children.forEach((child) => traverse(child, callback));
60
- }
61
- }
62
-
63
- /* -------------------------------------------
64
- Chargement unique de ctrl_camera_pr_env.js
65
- -------------------------------------------- */
66
-
67
- async function ensureOrbitScriptsLoaded() {
68
- if (window.__PLY_ORBIT_LOADED__) return;
69
- if (window.__PLY_ORBIT_LOADING__) {
70
- await window.__PLY_ORBIT_LOADING__;
71
- return;
72
- }
73
-
74
- window.__PLY_ORBIT_LOADING__ = new Promise((resolve, reject) => {
75
- const s = document.createElement("script");
76
- // IMPORTANT : charger le script free-camera + collisions (pas l'ancienne orbit-camera)
77
- s.src = "https://mikafil-viewer-sgos.static.hf.space/deplacement_dans_env/ctrl_camera_pr_env.js";
78
- s.async = true;
79
- s.onload = () => {
80
- window.__PLY_ORBIT_LOADED__ = true;
81
- resolve();
82
- };
83
- s.onerror = (e) => {
84
- console.error("[viewer.js] Failed to load ctrl_camera_pr_env.js", e);
85
- reject(e);
86
- };
87
- document.head.appendChild(s);
88
- });
89
-
90
- await window.__PLY_ORBIT_LOADING__;
91
- }
92
-
93
- /* -------------------------------------------
94
- State (par module = par instance importée)
95
- -------------------------------------------- */
96
-
97
- let pc;
98
- export let app = null;
99
- let cameraEntity = null;
100
  let modelEntity = null;
101
- let envEntity = null; // <<< GLB instancié (focusEntity collisions)
102
  let viewerInitialized = false;
103
  let resizeObserver = null;
104
- let resizeTimeout = null;
105
 
106
  // paramètres courants de l'instance
107
  let chosenCameraX, chosenCameraY, chosenCameraZ;
108
- let minZoom, maxZoom, minAngle, maxAngle, minAzimuth, maxAzimuth, minY;
109
- let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRotationZ;
110
- let presentoirScaleX, presentoirScaleY, presentoirScaleZ;
111
  let sogUrl, glbUrl, presentoirUrl;
112
  let color_bg_hex, color_bg, espace_expo_bool;
113
 
114
- // perf dynamique
115
- let maxDevicePixelRatio = 1.75; // plafond par défaut (configurable)
116
- let interactDpr = 1.0; // DPR pendant interaction
117
- let idleRestoreDelay = 350; // ms avant de restaurer le DPR
118
- let idleTimer = null;
119
-
120
  /* -------------------------------------------
121
  Initialisation
122
  -------------------------------------------- */
123
 
124
  export async function initializeViewer(config, instanceId) {
 
125
  if (viewerInitialized) return;
126
 
127
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
128
  const isMobile = isIOS || /Android/i.test(navigator.userAgent);
129
 
130
  // --- Configuration ---
131
- // Nouveau format .sog (et compat si seul sogs_json_url est présent)
 
132
  sogUrl = config.sog_url || config.sogs_json_url;
133
 
134
  glbUrl =
135
- config.glb_url !== undefined
136
- ? config.glb_url
137
- : "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/ressources/espace_expo/sol_blanc_2.glb";
138
-
139
- presentoirUrl =
140
- config.presentoir_url !== undefined
141
- ? config.presentoir_url
142
- : "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/ressources/espace_expo/sol_blanc_2.glb";
143
-
144
- minZoom = parseFloat(config.minZoom || "1");
145
- maxZoom = parseFloat(config.maxZoom || "20");
146
- minAngle = parseFloat(config.minAngle || "-2000");
147
- maxAngle = parseFloat(config.maxAngle || "2000");
148
- minAzimuth = config.minAzimuth !== undefined ? parseFloat(config.minAzimuth) : -360;
149
- maxAzimuth = config.maxAzimuth !== undefined ? parseFloat(config.maxAzimuth) : 360;
150
- minY = config.minY !== undefined ? parseFloat(config.minY) : 0;
151
-
152
- modelX = config.modelX !== undefined ? parseFloat(config.modelX) : 0;
153
- modelY = config.modelY !== undefined ? parseFloat(config.modelY) : 0;
154
- modelZ = config.modelZ !== undefined ? parseFloat(config.modelZ) : 0;
155
- modelScale = config.modelScale !== undefined ? parseFloat(config.modelScale) : 1;
156
- modelRotationX = config.modelRotationX !== undefined ? parseFloat(config.modelRotationX) : 0;
157
- modelRotationY = config.modelRotationY !== undefined ? parseFloat(config.modelRotationY) : 0;
158
- modelRotationZ = config.modelRotationZ !== undefined ? parseFloat(config.modelRotationZ) : 0;
159
-
160
- presentoirScaleX = config.presentoirScaleX !== undefined ? parseFloat(config.presentoirScaleX) : 0;
161
- presentoirScaleY = config.presentoirScaleY !== undefined ? parseFloat(config.presentoirScaleY) : 0;
162
- presentoirScaleZ = config.presentoirScaleZ !== undefined ? parseFloat(config.presentoirScaleZ) : 0;
163
-
164
- const cameraX = config.cameraX !== undefined ? parseFloat(config.cameraX) : 0;
165
- const cameraY = config.cameraY !== undefined ? parseFloat(config.cameraY) : 2;
166
- const cameraZ = config.cameraZ !== undefined ? parseFloat(config.cameraZ) : 5;
167
-
168
- const cameraXPhone = config.cameraXPhone !== undefined ? parseFloat(config.cameraXPhone) : cameraX;
169
- const cameraYPhone = config.cameraYPhone !== undefined ? parseFloat(config.cameraYPhone) : cameraY;
170
- const cameraZPhone = config.cameraZPhone !== undefined ? parseFloat(config.cameraZPhone) : cameraZ * 1.5;
171
-
172
- color_bg_hex = config.canvas_background !== undefined ? config.canvas_background : "#FFFFFF";
173
- espace_expo_bool = config.espace_expo_bool !== undefined ? config.espace_expo_bool : false;
174
- color_bg = hexToRgbaArray(color_bg_hex);
175
-
176
- chosenCameraX = isMobile ? cameraXPhone : cameraX;
177
  chosenCameraY = isMobile ? cameraYPhone : cameraY;
178
  chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
179
 
180
- // Options perf configurables
181
- if (config.maxDevicePixelRatio !== undefined) {
182
- maxDevicePixelRatio = Math.max(0.75, parseFloat(config.maxDevicePixelRatio) || maxDevicePixelRatio);
183
- }
184
- if (config.interactionPixelRatio !== undefined) {
185
- interactDpr = Math.max(0.75, parseFloat(config.interactionPixelRatio) || interactDpr);
186
- }
187
- if (config.idleRestoreDelayMs !== undefined) {
188
- idleRestoreDelay = Math.max(120, parseInt(config.idleRestoreDelayMs, 10) || idleRestoreDelay);
189
- }
190
 
191
  // --- Prépare le canvas unique à cette instance ---
192
  const canvasId = "canvas-" + instanceId;
193
  const progressDialog = document.getElementById("progress-dialog-" + instanceId);
194
- const viewerContainer = document.getElementById("viewer-container-" + instanceId);
195
-
196
- const old = document.getElementById(canvasId);
197
- if (old) old.remove();
198
-
199
- const canvas = document.createElement("canvas");
200
- canvas.id = canvasId;
201
- canvas.className = "ply-canvas";
202
- canvas.style.width = "100%";
203
- canvas.style.height = "100%";
204
- canvas.setAttribute("tabindex", "0");
205
- viewerContainer.insertBefore(canvas, progressDialog);
206
-
207
- // interactions de base
208
- canvas.style.touchAction = "none";
209
- canvas.style.webkitTouchCallout = "none";
210
- canvas.addEventListener("gesturestart", (e) => e.preventDefault());
211
- canvas.addEventListener("gesturechange", (e) => e.preventDefault());
212
- canvas.addEventListener("gestureend", (e) => e.preventDefault());
213
- canvas.addEventListener("dblclick", (e) => e.preventDefault());
214
- canvas.addEventListener(
215
- "touchstart",
216
- (e) => {
217
- if (e.touches.length > 1) e.preventDefault();
218
- },
219
- { passive: false }
220
- );
221
- canvas.addEventListener(
222
- "wheel",
223
- (e) => {
224
- e.preventDefault();
225
- },
226
- { passive: false }
227
- );
228
-
229
- // Bloque le scroll page uniquement quand le pointeur est sur le canvas
230
- const scrollKeys = new Set([
231
- "ArrowUp",
232
- "ArrowDown",
233
- "ArrowLeft",
234
- "ArrowRight",
235
- "PageUp",
236
- "PageDown",
237
- "Home",
238
- "End",
239
- " ",
240
- "Space",
241
- "Spacebar"
242
- ]);
243
- let isPointerOverCanvas = false;
244
- const focusCanvas = () => canvas.focus({ preventScroll: true });
245
-
246
- const onPointerEnter = () => {
247
- isPointerOverCanvas = true;
248
- focusCanvas();
249
- };
250
- const onPointerLeave = () => {
251
- isPointerOverCanvas = false;
252
- if (document.activeElement === canvas) canvas.blur();
253
- };
254
- const onCanvasBlur = () => {
255
- isPointerOverCanvas = false;
256
- };
257
-
258
- canvas.addEventListener("pointerenter", onPointerEnter);
259
- canvas.addEventListener("pointerleave", onPointerLeave);
260
- canvas.addEventListener("mouseenter", onPointerEnter);
261
- canvas.addEventListener("mouseleave", onPointerLeave);
262
- canvas.addEventListener("mousedown", focusCanvas);
263
- canvas.addEventListener(
264
- "touchstart",
265
- () => {
266
- focusCanvas();
267
- },
268
- { passive: false }
269
- );
270
- canvas.addEventListener("blur", onCanvasBlur);
271
-
272
- const onKeyDownCapture = (e) => {
273
- if (!isPointerOverCanvas) return;
274
- if (scrollKeys.has(e.key) || scrollKeys.has(e.code)) {
275
- e.preventDefault();
276
- }
277
- };
278
- window.addEventListener("keydown", onKeyDownCapture, true);
279
-
280
- progressDialog.style.display = "block";
281
-
282
- // --- Charge PlayCanvas lib ESM (une par module/instance) ---
283
- if (!pc) {
284
- pc = await import("https://esm.run/playcanvas");
285
- window.pc = pc; // utiles pour tooltips.js
286
- }
287
-
288
- // --- Crée l'Application ---
289
- const device = await pc.createGraphicsDevice(canvas, {
290
- deviceTypes: ["webgl2"],
291
- glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
292
  twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
293
  antialias: false
294
  });
295
-
296
- // Cap DPR pour limiter le coût CPU/GPU
297
- device.maxPixelRatio = Math.min(window.devicePixelRatio || 1, maxDevicePixelRatio);
298
 
299
  const opts = new pc.AppOptions();
300
  opts.graphicsDevice = device;
301
  opts.mouse = new pc.Mouse(canvas);
302
  opts.touch = new pc.TouchDevice(canvas);
303
- opts.keyboard = new pc.Keyboard(canvas);
304
  opts.componentSystems = [
305
  pc.RenderComponentSystem,
306
  pc.CameraComponentSystem,
307
- pc.LightComponentSystem,
308
- pc.ScriptComponentSystem,
309
- pc.GSplatComponentSystem,
310
  pc.CollisionComponentSystem,
311
  pc.RigidbodyComponentSystem
312
  ];
313
- // GSplatHandler gère nativement les .sog
314
  opts.resourceHandlers = [pc.TextureHandler, pc.ContainerHandler, pc.ScriptHandler, pc.GSplatHandler];
315
 
316
  app = new pc.Application(canvas, opts);
317
  app.setCanvasFillMode(pc.FILLMODE_NONE);
318
  app.setCanvasResolution(pc.RESOLUTION_AUTO);
319
 
320
- // --- Debounce resize (moins de rafales) ---
321
  resizeObserver = new ResizeObserver((entries) => {
322
- if (!entries || !entries.length) return;
323
- if (resizeTimeout) clearTimeout(resizeTimeout);
324
- resizeTimeout = setTimeout(() => {
325
- app.resizeCanvas(entries[0].contentRect.width, entries[0].contentRect.height);
326
- }, 60);
327
  });
328
  resizeObserver.observe(viewerContainer);
329
 
330
- window.addEventListener("resize", () => {
331
- if (resizeTimeout) clearTimeout(resizeTimeout);
332
- resizeTimeout = setTimeout(() => {
333
- app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
334
- }, 60);
335
- });
336
-
337
  // Nettoyage complet
338
  app.on("destroy", () => {
339
- try {
340
  resizeObserver.disconnect();
341
  } catch {}
342
  if (opts.keyboard && opts.keyboard.detach) opts.keyboard.detach();
 
343
  window.removeEventListener("keydown", onKeyDownCapture, true);
344
 
345
  canvas.removeEventListener("pointerenter", onPointerEnter);
346
- canvas.removeEventListener("pointerleave", onPointerLeave);
347
- canvas.removeEventListener("mouseenter", onPointerEnter);
348
- canvas.removeEventListener("mouseleave", onPointerLeave);
349
- canvas.removeEventListener("mousedown", focusCanvas);
350
- canvas.removeEventListener("touchstart", focusCanvas);
351
  canvas.removeEventListener("blur", onCanvasBlur);
352
  });
353
-
354
- // --- Enregistre et charge les assets en 1 phase pour SOG + GLB (focusEntity prêt avant la caméra) ---
355
- const sogAsset = new pc.Asset("gsplat", "gsplat", { url: sogUrl });
356
- const glbAsset = new pc.Asset("glb", "container", { url: glbUrl });
357
- app.assets.add(sogAsset);
358
- app.assets.add(glbAsset);
359
-
360
- // Assure la free-cam + collisions
361
- await ensureOrbitScriptsLoaded();
362
-
363
- // ---------- CHARGEMENT SOG + GLB AVANT CREATION CAMERA ----------
364
- await new Promise((resolve, reject) => {
365
- const loader = new pc.AssetListLoader([sogAsset, glbAsset], app.assets);
366
- loader.load(() => resolve());
367
- loader.on('error', reject);
368
- });
369
-
370
- app.start(); // démarre l'update loop dès que possible
371
- progressDialog.style.display = "none";
372
-
373
- // --- Modèle principal (GSplat via .sog) ---
374
- modelEntity = new pc.Entity("model");
375
- modelEntity.addComponent("gsplat", { asset: sogAsset });
376
- modelEntity.setLocalPosition(modelX, modelY, modelZ);
377
- modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
378
- modelEntity.setLocalScale(modelScale, modelScale, modelScale);
379
- app.root.addChild(modelEntity);
380
-
381
- // --- Instancier le GLB d’environnement (collision) ---
382
- envEntity = glbAsset.resource ? glbAsset.resource.instantiateRenderEntity() : null;
383
- if (envEntity) {
384
- envEntity.name = "ENV_GLTF";
385
- app.root.addChild(envEntity);
386
- // NOTE : évite de changer l'échelle ici, sauf si nécessaire. Si tu DOIS :
387
- // envEntity.setLocalScale(1,1,1); // garde l'échelle d'import cohérente
388
- } else {
389
- console.warn("[viewer.js] GLB resource missing: collisions will fallback on GSplat (aucun mesh).");
390
- }
391
-
392
- // --- Caméra + scripts d’input (free-cam nommée 'orbitCamera' pour compat) ---
393
- cameraEntity = new pc.Entity("camera");
394
- cameraEntity.addComponent("camera", {
395
- clearColor: new pc.Color(color_bg),
396
- nearClip: 0.001,
397
- farClip: 100
398
- });
399
- cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
400
- cameraEntity.lookAt(modelEntity.getPosition());
401
- cameraEntity.addComponent("script");
402
-
403
- // >>> focusEntity = GLB en priorité (sinon fallback gsplat) <<<
404
- cameraEntity.script.create("orbitCamera", {
405
- attributes: {
406
- focusEntity: envEntity || modelEntity,
407
- // Attributs hérités pour compat, mais seront interprétés par la free-cam
408
- inertiaFactor: 0.2,
409
- distanceMax: maxZoom,
410
- distanceMin: minZoom,
411
- pitchAngleMax: maxAngle,
412
- pitchAngleMin: minAngle,
413
- yawAngleMax: maxAzimuth,
414
- yawAngleMin: minAzimuth,
415
- minY: minY,
416
- frameOnStart: false
417
- }
418
- });
419
- cameraEntity.script.create("orbitCameraInputMouse");
420
- cameraEntity.script.create("orbitCameraInputTouch");
421
- cameraEntity.script.create("orbitCameraInputKeyboard", {
422
- attributes: {
423
- forwardSpeed: 1.2,
424
- strafeSpeed: 1.2
425
- }
426
- });
427
- app.root.addChild(cameraEntity);
428
-
429
- // Taille initiale
430
- app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
431
-
432
- // IMPORTANT : si la free-cam est active, ne pas "forcer" un reset d'orbite.
433
- app.once("update", () => resetViewerCamera());
434
-
435
- // ---------- Perf dynamique : DPR temporairement réduit pendant interaction ----------
436
- const setDpr = (val) => {
437
- const clamped = Math.max(0.5, Math.min(val, maxDevicePixelRatio));
438
- if (app.graphicsDevice.maxPixelRatio !== clamped) {
439
- app.graphicsDevice.maxPixelRatio = clamped;
440
- app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
441
- }
442
  };
 
443
 
444
- const bumpInteraction = () => {
445
- setDpr(interactDpr);
446
- if (idleTimer) clearTimeout(idleTimer);
447
- idleTimer = setTimeout(() => {
448
- setDpr(Math.min(window.devicePixelRatio || 1, maxDevicePixelRatio));
449
- }, idleRestoreDelay);
450
- };
451
 
452
- const interactionEvents = ["mousedown", "mousemove", "mouseup", "wheel", "touchstart", "touchmove", "keydown"];
453
- interactionEvents.forEach((ev) => {
454
- canvas.addEventListener(ev, bumpInteraction, { passive: true });
455
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
456
 
457
- // ---------- CHARGEMENT DIFFÉRÉ : présentoir et tooltips ----------
458
- setTimeout(async () => {
459
- try {
460
- const presentoirAsset = new pc.Asset("presentoir", "container", { url: presentoirUrl });
461
- app.assets.add(presentoirAsset);
462
- await new Promise((resolve) => {
463
- const loader2 = new pc.AssetListLoader([presentoirAsset], app.assets);
464
- loader2.load(() => resolve());
465
  });
466
 
467
- const presentoirEntity =
468
- presentoirAsset.resource ? presentoirAsset.resource.instantiateRenderEntity() : null;
469
- if (presentoirEntity) {
470
- presentoirEntity.setLocalScale(presentoirScaleX, presentoirScaleY, presentoirScaleZ);
471
- app.root.addChild(presentoirEntity);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
472
  }
 
 
 
 
 
 
 
 
 
 
473
 
474
- // Si pas d'espace expo : recolore GLB & présentoir pour fond uni
475
- if (!espace_expo_bool) {
476
- const matSol = new pc.StandardMaterial();
477
- matSol.blendType = pc.BLEND_NONE;
478
- matSol.emissive = new pc.Color(color_bg);
479
- matSol.emissiveIntensity = 1;
480
- matSol.useLighting = false;
481
- matSol.update();
482
-
483
- if (presentoirEntity) {
484
- traverse(presentoirEntity, (node) => {
485
- if (node.render && node.render.meshInstances) {
486
- for (const mi of node.render.meshInstances) mi.material = matSol;
487
- }
488
- });
489
- }
490
 
491
- if (envEntity) {
492
- traverse(envEntity, (node) => {
493
- if (node.render && node.render.meshInstances) {
494
- for (const mi of node.render.meshInstances) mi.material = matSol;
495
- }
 
 
 
 
 
 
 
 
 
 
 
496
  });
497
- }
498
  }
499
-
500
- // Tooltips (optionnels)
501
- try {
502
- if (config.tooltips_url) {
503
- import("./tooltips.js")
504
- .then((tooltipsModule) => {
505
- tooltipsModule.initializeTooltips({
506
- app,
507
- cameraEntity,
508
- modelEntity,
509
- tooltipsUrl: config.tooltips_url,
510
- defaultVisible: !!config.showTooltipsDefault,
511
- moveDuration: config.tooltipMoveDuration || 0.6
512
- });
513
- })
514
- .catch(() => { /* optional */ });
515
- }
516
- } catch (e) { /* optional */ }
517
  } catch (e) {
518
- console.warn("[viewer.js] Deferred assets load failed:", e);
519
  }
520
- }, 0);
521
-
522
- viewerInitialized = true;
523
  }
524
 
525
- /* -------------------------------------------
526
- Reset caméra (API)
527
- -------------------------------------------- */
528
-
529
- export function resetViewerCamera() {
530
- try {
531
- if (!cameraEntity || !modelEntity || !app) return;
532
- const camScript = cameraEntity.script && cameraEntity.script.orbitCamera;
533
- if (!camScript) return;
534
-
535
- const modelPos = modelEntity.getPosition();
536
-
537
- // Si c'est une FREE-CAM (notre script), ne pas toucher à des champs d'orbite.
538
- // On se contente éventuellement de réaligner le regard.
539
- const looksLikeOrbit =
540
- ("pivotPoint" in camScript) ||
541
- ("_distance" in camScript) ||
542
- ("_updatePosition" in camScript);
543
-
544
- if (!looksLikeOrbit) {
545
- // Free camera : juste orienter vers le modèle si souhaité
546
- cameraEntity.lookAt(modelPos);
547
- return;
548
- }
549
-
550
- // --- Cas d'une vraie orbit-camera (compat héritée) ---
551
- const orbitCam = camScript;
552
-
553
- const tempEnt = new pc.Entity();
554
- tempEnt.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
555
- tempEnt.lookAt(modelPos);
556
-
557
- const dist = new pc.Vec3()
558
- .sub2(new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ), modelPos)
559
- .length();
560
-
561
- cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
562
- cameraEntity.lookAt(modelPos);
563
-
564
- orbitCam.pivotPoint = modelPos.clone();
565
- orbitCam._targetDistance = dist;
566
- orbitCam._distance = dist;
567
-
568
- const rot = tempEnt.getRotation();
569
- const fwd = new pc.Vec3();
570
- rot.transformVector(pc.Vec3.FORWARD, fwd);
571
-
572
- const yaw = Math.atan2(-fwd.x, -fwd.z) * pc.math.RAD_TO_DEG;
573
- const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);
574
- const rotNoYaw = new pc.Quat().mul2(yawQuat, rot);
575
- const fNoYaw = new pc.Vec3();
576
- rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw);
577
- const pitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
578
-
579
- orbitCam._targetYaw = yaw;
580
- orbitCam._yaw = yaw;
581
- orbitCam._targetPitch = pitch;
582
- orbitCam._pitch = pitch;
583
- if (orbitCam._updatePosition) orbitCam._updatePosition();
584
-
585
- tempEnt.destroy();
586
- } catch (e) {
587
- console.error("[viewer.js] resetViewerCamera error:", e);
588
- }
589
- }
 
 
 
 
1
  /* -------------------------------------------
2
  Utils
3
+ (les helpers image ne sont plus nécessaires pour .sog,
4
+ mais on les garde sans effet de bord pour compat ascendante)
5
  -------------------------------------------- */
6
 
 
7
  async function loadImageAsTexture(url, app) {
8
  return new Promise((resolve, reject) => {
9
  const img = new window.Image();
 
 
 
 
 
 
 
 
 
 
 
 
10
  });
11
  }
12
+ // Patch global Image -> force CORS (sans incidence pour .sog)
 
13
  (function () {
14
  const OriginalImage = window.Image;
15
  window.Image = function (...args) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  let modelEntity = null;
 
17
  let viewerInitialized = false;
18
  let resizeObserver = null;
 
19
 
20
  // paramètres courants de l'instance
21
  let chosenCameraX, chosenCameraY, chosenCameraZ;
 
 
 
22
  let sogUrl, glbUrl, presentoirUrl;
23
  let color_bg_hex, color_bg, espace_expo_bool;
24
 
 
 
 
 
 
 
25
  /* -------------------------------------------
26
  Initialisation
27
  -------------------------------------------- */
28
 
29
  export async function initializeViewer(config, instanceId) {
30
+ // ce module ES est importé avec un param unique ?inst=..., donc 1 instance par import
31
  if (viewerInitialized) return;
32
 
33
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
34
  const isMobile = isIOS || /Android/i.test(navigator.userAgent);
35
 
36
  // --- Configuration ---
37
+ // Nouveau : utiliser un .sog "bundled" (format SOG PlayCanvas)
38
+ // Compat ascendante : on accepte encore sogs_json_url si sog_url absent
39
  sogUrl = config.sog_url || config.sogs_json_url;
40
 
41
  glbUrl =
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  chosenCameraY = isMobile ? cameraYPhone : cameraY;
43
  chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
44
 
 
 
 
 
 
 
 
 
 
 
45
 
46
  // --- Prépare le canvas unique à cette instance ---
47
  const canvasId = "canvas-" + instanceId;
48
  const progressDialog = document.getElementById("progress-dialog-" + instanceId);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
50
  antialias: false
51
  });
52
+ device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
 
 
53
 
54
  const opts = new pc.AppOptions();
55
  opts.graphicsDevice = device;
56
  opts.mouse = new pc.Mouse(canvas);
57
  opts.touch = new pc.TouchDevice(canvas);
58
+ opts.keyboard = new pc.Keyboard(canvas); // clavier scoping canvas
59
  opts.componentSystems = [
60
  pc.RenderComponentSystem,
61
  pc.CameraComponentSystem,
 
 
 
62
  pc.CollisionComponentSystem,
63
  pc.RigidbodyComponentSystem
64
  ];
65
+ // GSplatHandler gère nativement les .sog (bundled SOG)
66
  opts.resourceHandlers = [pc.TextureHandler, pc.ContainerHandler, pc.ScriptHandler, pc.GSplatHandler];
67
 
68
  app = new pc.Application(canvas, opts);
69
  app.setCanvasFillMode(pc.FILLMODE_NONE);
70
  app.setCanvasResolution(pc.RESOLUTION_AUTO);
71
 
72
+
73
  resizeObserver = new ResizeObserver((entries) => {
74
+ entries.forEach((entry) => {
75
+ app.resizeCanvas(entry.contentRect.width, entry.contentRect.height);
76
+ });
 
 
77
  });
78
  resizeObserver.observe(viewerContainer);
79
 
80
+ window.addEventListener("resize", () =>
81
+ app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight)
82
+ );
 
 
 
 
83
  // Nettoyage complet
84
  app.on("destroy", () => {
 
85
  resizeObserver.disconnect();
86
  } catch {}
87
  if (opts.keyboard && opts.keyboard.detach) opts.keyboard.detach();
88
+
89
  window.removeEventListener("keydown", onKeyDownCapture, true);
90
 
91
  canvas.removeEventListener("pointerenter", onPointerEnter);
 
 
 
 
 
92
  canvas.removeEventListener("blur", onCanvasBlur);
93
  });
94
+ // --- Enregistre les assets ---
95
+ // IMPORTANT : pour .sog on déclare un asset de type "gsplat" avec l'URL .sog
96
+ const assets = {
97
+ sog: new pc.Asset("gsplat", "gsplat", { url: sogUrl }),
98
+ glb: new pc.Asset("glb", "container", { url: glbUrl }),
99
+ presentoir: new pc.Asset("presentoir", "container", { url: presentoirUrl })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  };
101
+ for (const k in assets) app.assets.add(assets[k]);
102
 
103
+ const loader = new pc.AssetListLoader(Object.values(assets), app.assets);
104
+ // Assure orbit-camera.js une seule fois
105
+ await ensureOrbitScriptsLoaded();
 
 
 
 
106
 
107
+ loader.load(() => {
108
+ app.start();
109
+ progressDialog.style.display = "none";
110
+
111
+ // --- Modèle principal (GSplat via .sog) ---
112
+ modelEntity = new pc.Entity("model");
113
+ modelEntity.addComponent("gsplat", { asset: assets.sog });
114
+ modelEntity.setLocalPosition(modelX, modelY, modelZ);
115
+ modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
116
+ modelEntity.setLocalScale(modelScale, modelScale, modelScale);
117
+ app.root.addChild(modelEntity);
118
+
119
+ // --- Sol / environnement ---
120
+ const glbEntity = assets.glb.resource.instantiateRenderEntity();
121
+ app.root.addChild(glbEntity);
122
+
123
+ const presentoirEntity = assets.presentoir.resource.instantiateRenderEntity();
124
+ presentoirEntity.setLocalScale(presentoirScaleX, presentoirScaleY, presentoirScaleZ);
125
+ app.root.addChild(presentoirEntity);
126
+
127
+ if (!espace_expo_bool) {
128
+ const matSol = new pc.StandardMaterial();
129
+ matSol.blendType = pc.BLEND_NONE;
130
+ matSol.emissive = new pc.Color(color_bg);
131
+ matSol.emissiveIntensity = 1;
132
+ matSol.useLighting = false;
133
+ matSol.update();
134
+
135
+ traverse(presentoirEntity, (node) => {
136
+ if (node.render && node.render.meshInstances) {
137
+ for (const mi of node.render.meshInstances) mi.material = matSol;
138
+ }
139
+ });
140
 
141
+ traverse(glbEntity, (node) => {
142
+ if (node.render && node.render.meshInstances) {
143
+ for (const mi of node.render.meshInstances) mi.material = matSol;
144
+ }
 
 
 
 
145
  });
146
 
147
+ ////// MODIFIE A LA MANO FAIRE GAFFE //////
148
+ glbEntity.setLocalScale(10, 10, 10);
149
+ }
150
+ // --- Caméra + scripts d’input (disponibles car orbit chargé globalement) ---
151
+ cameraEntity = new pc.Entity("camera");
152
+ cameraEntity.addComponent("camera", {
153
+ clearColor: new pc.Color(color_bg),
154
+ nearClip: 0.001,
155
+ farClip: 100
156
+ });
157
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
158
+ cameraEntity.lookAt(modelEntity.getPosition());
159
+ cameraEntity.addComponent("script");
160
+
161
+ cameraEntity.script.create("orbitCamera", {
162
+ attributes: {
163
+ focusEntity: modelEntity,
164
+ inertiaFactor: 0.2,
165
+ distanceMax: maxZoom,
166
+ distanceMin: minZoom,
167
+ pitchAngleMax: maxAngle,
168
+ pitchAngleMin: minAngle,
169
+ yawAngleMax: maxAzimuth,
170
+ yawAngleMin: minAzimuth,
171
+ minY: minY,
172
+ frameOnStart: false
173
  }
174
+ });
175
+ cameraEntity.script.create("orbitCameraInputMouse");
176
+ cameraEntity.script.create("orbitCameraInputTouch");
177
+ cameraEntity.script.create("orbitCameraInputKeyboard", {
178
+ attributes: {
179
+ forwardSpeed: 1.2,
180
+ strafeSpeed: 1.2
181
+ }
182
+ });
183
+ app.root.addChild(cameraEntity);
184
 
185
+ app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
186
+ app.once("update", () => resetViewerCamera());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
 
188
+ // --- Tooltips (optionnels) ---
189
+ try {
190
+ if (config.tooltips_url) {
191
+ import("./tooltips.js")
192
+ .then((tooltipsModule) => {
193
+ tooltipsModule.initializeTooltips({
194
+ app,
195
+ cameraEntity,
196
+ modelEntity,
197
+ tooltipsUrl: config.tooltips_url,
198
+ defaultVisible: !!config.showTooltipsDefault,
199
+ moveDuration: config.tooltipMoveDuration || 0.6
200
+ });
201
+ })
202
+ .catch(() => {
203
+ /* optional */
204
  });
 
205
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  } catch (e) {
207
+ /* optional */
208
  }
209
+ viewerInitialized = true;
210
+ });
 
211
  }
212
 
213
+ /* -------------------------------------------