MikaFil commited on
Commit
27c4a59
·
verified ·
1 Parent(s): b11ee70

Update deplacement_dans_env/viewer_pr_env.js

Browse files
Files changed (1) hide show
  1. deplacement_dans_env/viewer_pr_env.js +565 -559
deplacement_dans_env/viewer_pr_env.js CHANGED
@@ -1,597 +1,603 @@
1
- // viewer_pr_env.js
2
- // ==============================
3
- // Version 2.3 : Utilisation du MOTEUR PHYSIQUE de PlayCanvas
4
- // - CORRIGÉ (CORS) : Charge les fichiers de physique (Ammo) depuis les
5
- // URL absolues du Space Hugging Face de l'utilisateur pour éviter
6
- // tout blocage de sécurité.
 
 
 
 
7
 
8
  /* -------------------------------------------
9
-    Utils communs (Inchangés)
10
  -------------------------------------------- */
 
 
 
 
 
 
 
11
 
12
- async function loadImageAsTexture(url, app) {
13
-   return new Promise((resolve, reject) => {
14
-     const img = new window.Image();
15
-     img.crossOrigin = "anonymous";
16
-     img.onload = function () {
17
-       const tex = new pc.Texture(app.graphicsDevice, {
18
-         width: img.width,
19
-         height: img.height,
20
-         format: pc.PIXELFORMAT_R8_G8_B8_A8
21
-       });
22
-       tex.setSource(img);
23
-       resolve(tex);
24
-     };
25
-     img.onerror = reject;
26
-     img.src = url;
27
-   });
28
- }
29
-
30
- (function () {
31
-   const OriginalImage = window.Image;
32
-   window.Image = function (...args) {
33
-     const img = new OriginalImage(...args);
34
-     img.crossOrigin = "anonymous";
35
-     return img;
36
-   };
37
- })();
38
-
39
  function hexToRgbaArray(hex) {
40
-   try {
41
-     hex = String(hex || "").replace("#", "");
42
-     if (hex.length === 6) hex += "FF";
43
-     if (hex.length !== 8) return [1, 1, 1, 1];
44
-     const num = parseInt(hex, 16);
45
-     return [
46
-       ((num >> 24) & 0xff) / 255,
47
-       ((num >> 16) & 0xff) / 255,
48
-       ((num >> 8) & 0xff) / 255,
49
-       (num & 0xff) / 255
50
-     ];
51
-   } catch (e) {
52
-     console.warn("hexToRgbaArray error:", e);
53
-     return [1, 1, 1, 1];
54
-   }
55
  }
56
 
57
  function traverse(entity, callback) {
58
-   callback(entity);
59
-   if (entity.children) {
60
-     entity.children.forEach((child) => traverse(child, callback));
61
-   }
 
 
 
 
 
 
62
  }
63
 
64
- // Helpers math (Inchangés)
65
- function vAdd(a,b){ return new pc.Vec3(a.x+b.x,a.y+b.y,a.z+b.z); }
66
- function vSub(a,b){ return new pc.Vec3(a.x-b.x,a.y-b.y,a.z-b.z); }
67
- function vScale(v,s){ return new pc.Vec3(v.x*s,v.y*s,v.z*s); }
68
- function vLen(v){ return Math.sqrt(v.x*v.x+v.y*v.y+v.z*v.z); }
69
- function vDot(a,b){ return a.x*b.x+a.y*b.y+a.z*b.z; }
70
- function clamp(v,a,b){ return Math.max(a, Math.min(b,v)); }
 
 
71
 
72
  /* -------------------------------------------
73
-    NOUVEAU Script Caméra (basé sur le moteur physique)
74
  -------------------------------------------- */
75
- function registerFirstPersonScripts() {
76
-   if (window.__PLY_FIRSTPERSON_REG__) return;
77
-   window.__PLY_FIRSTPERSON_REG__ = true;
78
-
79
-   // Remarque : 'orbitCamera' est conservé pour la compatibilité avec l'ancien nom,
80
-   // mais c'est maintenant un script de caméra 'first-person' (FPS).
81
-   var FirstPersonCamera = pc.createScript('orbitCamera');
82
-
83
-   // --- Attributs ---
84
-   FirstPersonCamera.attributes.add('cameraEntity', {
85
-     type: 'entity',
86
-     title: 'Camera Entity',
87
-     description: 'Entité caméra enfant à contrôler pour le pitch (regard haut/bas)'
88
-   });
89
-   FirstPersonCamera.attributes.add('moveSpeed', {
90
-     type: 'number',
91
-     default: 5, // Vitesse en m/s (ajustez au besoin)
92
-     title: 'Move Speed'
93
-   });
94
-   FirstPersonCamera.attributes.add('lookSpeed', {
95
-     type: 'number',
96
-     default: 0.25,
97
-     title: 'Look Sensitivity'
98
-   });
99
-   FirstPersonCamera.attributes.add('pitchAngleMin', { type: 'number', default: -89, title: 'Pitch Min (deg)' });
100
-   FirstPersonCamera.attributes.add('pitchAngleMax', { type: 'number', default:  89, title: 'Pitch Max (deg)' });
101
-
102
-   // --- Variables internes ---
103
-   FirstPersonCamera.prototype.initialize = function () {
104
-     this.yaw = 0;   // Rotation gauche/droite (autour de Y)
105
-     this.pitch = 0; // Rotation haut/bas (autour de X)
106
-     this.velocity = new pc.Vec3();
107
-     this.force = new pc.Vec3();
108
-
109
-     // Gestion de la souris
110
-     if (this.app.mouse) {
111
-       this.app.mouse.on(pc.EVENT_MOUSEMOVE, this.onMouseMove, this);
112
-       // Pointer Lock (cliquer pour bouger)
113
-       this.app.mouse.on(pc.EVENT_MOUSEDOWN, () => {
114
-         if (!pc.Mouse.isPointerLocked()) {
115
-           this.app.mouse.enablePointerLock();
116
-         }
117
-       }, this);
118
-     }
119
-
120
-     // Angles initiaux
121
-     var angles = this.entity.getLocalEulerAngles();
122
-     this.yaw = angles.y;
123
-     this.pitch = this.cameraEntity ? this.cameraEntity.getLocalEulerAngles().x : 0;
124
-   };
125
-
126
-   FirstPersonCamera.prototype.onMouseMove = function (e) {
127
-     // Uniquement si le pointeur est verrouillé
128
-     if (!pc.Mouse.isPointerLocked()) {
129
-       return;
130
-     }
131
-
132
-     this.yaw -= e.dx * this.lookSpeed;
133
-     this.pitch -= e.dy * this.lookSpeed;
134
-     this.pitch = pc.math.clamp(this.pitch, this.pitchAngleMin, this.pitchAngleMax);
135
-   };
136
-
137
-   FirstPersonCamera.prototype.update = function (dt) {
138
-     // --- 1. Rotation (Look) ---
139
-     // L'entité "Player" (capsule) tourne sur Y (gauche/droite)
140
-     this.entity.setLocalEulerAngles(0, this.yaw, 0);
141
-     // L'entité "Camera" (enfant) tourne sur X (haut/bas)
142
-     if (this.cameraEntity) {
143
-       this.cameraEntity.setLocalEulerAngles(this.pitch, 0, 0);
144
-     }
145
-
146
-     // --- 2. Mouvement (Clavier) ---
147
-     var fwd = 0;
148
-     var strf = 0;
149
-     const kb = this.app.keyboard; // Raccourci
150
-
151
-     if (kb.isPressed(pc.KEY_Z) || kb.isPressed(pc.KEY_W) || kb.isPressed(pc.KEY_UP)) {
152
-       fwd += 1;
153
-     }
154
-     if (kb.isPressed(pc.KEY_S) || kb.isPressed(pc.KEY_DOWN)) {
155
-       fwd -= 1;
156
-     }
157
-     if (kb.isPressed(pc.KEY_Q) || kb.isPressed(pc.KEY_A) || kb.isPressed(pc.KEY_LEFT)) {
158
-       strf -= 1;
159
-     }
160
-     if (kb.isPressed(pc.KEY_D) || kb.isPressed(pc.KEY_RIGHT)) {
161
-       strf += 1;
162
-     }
163
-
164
-     // Réinitialise la force à chaque frame
165
-     this.force.set(0, 0, 0);
166
-
167
-     if (fwd !== 0 || strf !== 0) {
168
-       // On utilise les vecteurs de l'entité (qui a le yaw)
169
-       var forward = this.entity.forward;
170
-       var right = this.entity.right;
171
-
172
-       // Calcule la force de déplacement
173
-       this.force.add(forward.mulScalar(fwd));
174
-       this.force.add(right.mulScalar(strf));
175
-      
176
-       // Normalise pour éviter un mouvement diagonal plus rapide
177
-       if (this.force.length() > 1) {
178
-         this.force.normalize();
179
-       }
180
- _
181
-     // --- 3. Appliquer au Moteur Physique ---
182
-     if (this.entity.rigidbody) {
183
-       // Récupère la vélocité actuelle (pour la gravité)
184
-       this.velocity.copy(this.entity.rigidbody.linearVelocity);
185
-      
186
-       // Définit la vélocité XZ (mouvement)
187
-       // On applique la vitesse au vecteur de force normalisé
188
-       this.velocity.x = this.force.x * this.moveSpeed;
189
-       this.velocity.z = this.force.z * this.moveSpeed;
190
-      
191
-       // Applique la nouvelle vélocité (conserve Y pour la gravité)
192
-       this.entity.rigidbody.linearVelocity = this.velocity;
193
-     }
194
-   };
195
  }
196
 
197
  /* -------------------------------------------
198
-    State (Modifié)
199
  -------------------------------------------- */
200
-
201
  let pc;
202
  export let app = null;
203
- let playerEntity = null; // Renommé pour plus de clarté
204
- let cameraEntity = null; // C'est maintenant un enfant du player
205
- let modelEntity = null;
206
- let envEntity = null;
 
207
  let viewerInitialized = false;
208
  let resizeObserver = null;
209
  let resizeTimeout = null;
210
 
211
- // paramètres courants de l'instance
212
- let chosenCameraX, chosenCameraY, chosenCameraZ;
213
- let minZoom, maxZoom, minAngle, maxAngle, minAzimuth, maxAzimuth, minY;
214
- let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRotationZ;
215
- let presentoirScaleX, presentoirScaleY, presentoirScaleZ;
216
  let sogUrl, glbUrl, presentoirUrl;
217
- let color_bg_hex, color_bg, espace_expo_bool;
 
 
 
 
218
 
219
- // perf dynamique
220
  let maxDevicePixelRatio = 1.75;
221
  let interactDpr = 1.0;
222
  let idleRestoreDelay = 350;
223
  let idleTimer = null;
224
 
 
 
 
 
225
  /* -------------------------------------------
226
-    Initialisation (MODIFIÉE)
 
227
  -------------------------------------------- */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
 
 
 
 
229
  export async function initializeViewer(config, instanceId) {
230
-   if (viewerInitialized) return;
231
-
232
-   const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
233
-   const isMobile = isIOS || /Android/i.test(navigator.userAgent);
234
-
235
-   console.log(`[VIEWER] A: initializeViewer begin`, { instanceId });
236
-
237
-   // --- Configuration (Inchangée) ---
238
-   sogUrl = config.sog_url || config.sogs_json_url;
239
-   glbUrl = config.glb_url ?? "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/ressources/espace_expo/sol_blanc_2.glb";
240
-   presentoirUrl = config.presentoir_url ?? "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/ressources/espace_expo/sol_blanc_2.glb";
241
-
242
-   minZoom = parseFloat(config.minZoom || "1");
243
-   maxZoom = parseFloat(config.maxZoom || "20");
244
-   minAngle = parseFloat(config.minAngle || "-2000");
245
-   maxAngle = parseFloat(config.maxAngle || "2000");
246
-   minAzimuth = config.minAzimuth !== undefined ? parseFloat(config.minAzimuth) : -360;
247
-   maxAzimuth = config.maxAzimuth !== undefined ? parseFloat(config.maxAzimuth) : 360;
248
-   minY = config.minY !== undefined ? parseFloat(config.minY) : 0;
249
-
250
-   modelX = config.modelX !== undefined ? parseFloat(config.modelX) : 0;
251
-   modelY = config.modelY !== undefined ? parseFloat(config.modelY) : 0;
252
-   modelZ = config.modelZ !== undefined ? parseFloat(config.modelZ) : 0;
253
-   modelScale = config.modelScale !== undefined ? parseFloat(config.modelScale) : 1;
254
-   modelRotationX = config.modelRotationX !== undefined ? parseFloat(config.modelRotationX) : 0;
255
-   modelRotationY = config.modelRotationY !== undefined ? parseFloat(config.modelRotationY) : 0;
256
-   modelRotationZ = config.modelRotationZ !== undefined ? parseFloat(config.modelRotationZ) : 0;
257
-
258
-   presentoirScaleX = config.presentoirScaleX !== undefined ? parseFloat(config.presentoirScaleX) : 0;
259
-   presentoirScaleY = config.presentoirScaleY !== undefined ? parseFloat(config.presentoirScaleY) : 0;
260
-   presentoirScaleZ = config.presentoirScaleZ !== undefined ? parseFloat(config.presentoirScaleZ) : 0;
261
-
262
-   // *** Point de spawn du JOUEUR (pas juste la caméra) ***
263
-   const cameraX = config.cameraX !== undefined ? parseFloat(config.cameraX) : 0;
264
-   const cameraY = config.cameraY !== undefined ? parseFloat(config.cameraY) : 2; // Hauteur de spawn
265
-   const cameraZ = config.cameraZ !== undefined ? parseFloat(config.cameraZ) : 5;
266
-
267
-   const cameraXPhone = config.cameraXPhone !== undefined ? parseFloat(config.cameraXPhone) : cameraX;
268
-   const cameraYPhone = config.cameraYPhone !== undefined ? parseFloat(config.cameraYPhone) : cameraY;
269
-   const cameraZPhone = config.cameraZPhone !== undefined ? parseFloat(config.cameraZPhone) : cameraZ * 1.5;
270
-
271
-   color_bg_hex = config.canvas_background !== undefined ? config.canvas_background : "#FFFFFF";
272
-   espace_expo_bool = config.espace_expo_bool !== undefined ? config.espace_expo_bool : false;
273
-   color_bg = hexToRgbaArray(color_bg_hex);
274
-
275
-   chosenCameraX = isMobile ? cameraXPhone : cameraX;
276
-   chosenCameraY = isMobile ? cameraYPhone : cameraY;
277
-   chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
278
-  
279
-   // ... (config perf inchangée) ...
280
-   if (config.maxDevicePixelRatio !== undefined) {
281
-     maxDevicePixelRatio = Math.max(0.75, parseFloat(config.maxDevicePixelRatio) || maxDevicePixelRatio);
282
-   }
283
-   if (config.interactionPixelRatio !== undefined) {
284
-     interactDpr = Math.max(0.75, parseFloat(config.interactionPixelRatio) || interactDpr);
285
-   }
286
-   if (config.idleRestoreDelayMs !== undefined) {
287
-     idleRestoreDelay = Math.max(120, parseInt(config.idleRestoreDelayMs, 10) || idleRestoreDelay);
288
-   }
289
-
290
-   // --- Prépare le canvas (Inchangé) ---
291
-   const canvasId = "canvas-" + instanceId;
292
-   const progressDialog = document.getElementById("progress-dialog-" + instanceId);
293
-   const viewerContainer = document.getElementById("viewer-container-" + instanceId);
294
-
295
-   const old = document.getElementById(canvasId);
296
-   if (old) old.remove();
297
-
298
-   const canvas = document.createElement("canvas");
299
-   canvas.id = canvasId;
300
-   canvas.className = "ply-canvas";
301
-   canvas.style.width = "100%";
302
-   canvas.style.height = "100%";
303
-   canvas.setAttribute("tabindex", "0");
304
-   viewerContainer.insertBefore(canvas, progressDialog);
305
-  
306
-   // ... (tous les listeners de canvas sont conservés) ...
307
-   canvas.style.touchAction = "none";
308
-   canvas.style.webkitTouchCallout = "none";
309
-   canvas.addEventListener("gesturestart", (e) => e.preventDefault());
310
-   canvas.addEventListener("gesturechange", (e) => e.preventDefault());
311
-   canvas.addEventListener("gestureend", (e) => e.preventDefault());
312
-   canvas.addEventListener("dblclick", (e) => e.preventDefault());
313
-   canvas.addEventListener(
314
-     "touchstart",
315
-     (e) => { if (e.touches.length > 1) e.preventDefault(); },
316
-     { passive: false }
317
-   );
318
-   canvas.addEventListener(
319
-     "wheel",
320
-     (e) => { e.preventDefault(); },
321
-     { passive: false }
322
-   );
323
-   const scrollKeys = new Set([
324
-     "ArrowUp","ArrowDown","ArrowLeft","ArrowRight",
325
-     "PageUp","PageDown","Home","End"," ","Space","Spacebar"
326
-   ]);
327
-   let isPointerOverCanvas = false;
328
-   const focusCanvas = () => canvas.focus({ preventScroll: true });
329
-   const onPointerEnter = () => { isPointerOverCanvas = true; focusCanvas(); };
330
-   const onPointerLeave = () => { isPointerOverCanvas = false; if (document.activeElement === canvas) canvas.blur(); };
331
-   const onCanvasBlur = () => { isPointerOverCanvas = false; };
332
-   canvas.addEventListener("pointerenter", onPointerEnter);
333
-   canvas.addEventListener("pointerleave", onPointerLeave);
334
-   canvas.addEventListener("mouseenter", onPointerEnter);
335
-   canvas.addEventListener("mouseleave", onPointerLeave);
336
-   canvas.addEventListener("mousedown", focusCanvas);
337
-   canvas.addEventListener("touchstart", () => { focusCanvas(); }, { passive: false });
338
-   canvas.addEventListener("blur", onCanvasBlur);
339
-   const onKeyDownCapture = (e) => {
340
-     if (!isPointerOverCanvas) return;
341
-     if (scrollKeys.has(e.key) || scrollKeys.has(e.code)) e.preventDefault();
342
-   };
343
-   window.addEventListener("keydown", onKeyDownCapture, true);
344
-   progressDialog.style.display = "block";
345
-
346
-   // --- Charge PlayCanvas lib ESM (Inchangé) ---
347
-   if (!pc) {
348
-     pc = await import("https://esm.run/playcanvas");
349
-     window.pc = pc; // debug
350
-   }
351
-   console.log('[VIEWER] PlayCanvas ESM chargé:', !!pc);
352
-
353
-   // --- Crée l'Application ---
354
-   const device = await pc.createGraphicsDevice(canvas, {
355
-     deviceTypes: ["webgl2"],
356
-     glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
357
-     twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
358
-     antialias: false
359
-   });
360
-   device.maxPixelRatio = Math.min(window.devicePixelRatio || 1, maxDevicePixelRatio);
361
-
362
-   const opts = new pc.AppOptions();
363
-  
364
-   // ++++++++++ CORRECTIF ÉCRAN NOIR (DÉBUT) ++++++++++
365
-   // Charge les fichiers de physique depuis votre Space Hugging Face pour éviter les erreurs CORS
366
-   const ammoUrl = "https://huggingface.co/spaces/MikaFil/viewer_sgos/resolve/main/deplacement_dans_env/ammo.js";
367
-   const ammoWasmUrl = "https://huggingface.co/spaces/MikaFil/viewer_sgos/resolve/main/deplacement_dans_env/ammo.wasm.js";
368
-   const ammoWasmBinaryUrl = "https://huggingface.co/spaces/MikaFil/viewer_sgos/resolve/main/deplacement_dans_env/ammo.wasm.wasm";
369
-
370
-   opts.physics = {
371
-     url: ammoUrl,
372
-     wasmUrl: ammoWasmUrl,
373
-     binaryUrl: ammoWasmBinaryUrl
374
-   };
375
-   // ++++++++++ CORRECTIF ÉCRAN NOIR (FIN) ++++++++++
376
-
377
-   opts.graphicsDevice = device;
378
-   opts.mouse = new pc.Mouse(canvas);
379
-   opts.touch = new pc.TouchDevice(canvas);
380
-   opts.keyboard = new pc.Keyboard(canvas);
381
-   // Note: Cette liste *doit* inclure RigidBody et Collision.
382
-   opts.componentSystems = [
383
-     pc.RenderComponentSystem,
384
-     pc.CameraComponentSystem,
385
-     pc.LightComponentSystem,
386
-     pc.ScriptComponentSystem,
387
-     pc.GSplatComponentSystem,
388
-     pc.CollisionComponentSystem, // <--- NÉCESSAIRE POUR LA PHYSIQUE
389
-     pc.RigidbodyComponentSystem  // <--- NÉCESSAIRE POUR LA PHYSIQUE
390
-   ];
391
-   opts.resourceHandlers = [pc.TextureHandler, pc.ContainerHandler, pc.ScriptHandler, pc.GSplatHandler];
392
-
393
-   app = new pc.Application(canvas, opts);
394
-   app.setCanvasFillMode(pc.FILLMODE_NONE);
395
-   app.setCanvasResolution(pc.RESOLUTION_AUTO);
396
-
397
-   // *** NOUVEAU : Définir la gravité pour le monde physique ***
398
-   app.systems.rigidbody.gravity.set(0, -9.81, 0); // Gravité standard
399
-
400
-   // --- Debounce resize (Inchangé) ---
401
-   resizeObserver = new ResizeObserver((entries) => {
402
-     if (!entries || !entries.length) return;
403
-     if (resizeTimeout) clearTimeout(resizeTimeout);
404
-     resizeTimeout = setTimeout(() => {
405
-       app.resizeCanvas(entries[0].contentRect.width, entries[0].contentRect.height);
406
-     }, 60);
407
-   });
408
-   resizeObserver.observe(viewerContainer);
409
-   window.addEventListener("resize", () => {
410
-     if (resizeTimeout) clearTimeout(resizeTimeout);
411
-     resizeTimeout = setTimeout(() => {
412
-       app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
413
-     }, 60);
414
-   });
415
-   app.on("destroy", () => {
416
-     try { resizeObserver.disconnect(); } catch {}
417
-     if (opts.keyboard && opts.keyboard.detach) opts.keyboard.detach();
418
-     window.removeEventListener("keydown", onKeyDownCapture, true);
419
-     // ... (listeners canvas inchangés) ...
420
-     canvas.removeEventListener("pointerenter", onPointerEnter);
421
-     canvas.removeEventListener("pointerleave", onPointerLeave);
422
-     canvas.removeEventListener("mouseenter", onPointerEnter);
423
-     canvas.removeEventListener("mouseleave", onPointerLeave);
424
-     canvas.removeEventListener("mousedown", focusCanvas);
425
-     canvas.removeEventListener("touchstart", focusCanvas);
426
-     canvas.removeEventListener("blur", onCanvasBlur);
427
-   });
428
-
429
-   // --- Chargement des assets (Inchangé) ---
430
-   const sogAsset = new pc.Asset("gsplat", "gsplat", { url: sogUrl });
431
-   const glbAsset = new pc.Asset("glb", "container", { url: glbUrl });
432
-   app.assets.add(sogAsset);
433
-   app.assets.add(glbAsset);
434
-
435
-   // *** NOUVEAU : Enregistre le script FPS ***
436
-   registerFirstPersonScripts();
437
-
438
-   // --- CHARGEMENT SOG + GLB (Inchangé) ---
439
-   await new Promise((resolve, reject) => {
440
-     const loader = new pc.AssetListLoader([sogAsset, glbAsset], app.assets);
441
-     loader.load(() => resolve());
442
-     loader.on('error', (e)=>{ console.error('[VIEWER] Asset load error:', e); reject(e); });
443
-   });
444
-
445
-   app.start();
446
-   progressDialog.style.display = "none";
447
-   console.log("[VIEWER] app.start OK — assets chargés");
448
-
449
-   // --- Modèle GSplat (Inchangé) ---
450
-   modelEntity = new pc.Entity("model");
451
-   modelEntity.addComponent("gsplat", { asset: sogAsset });
452
-   modelEntity.setLocalPosition(modelX, modelY, modelZ);
453
-   modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
454
-   modelEntity.setLocalScale(modelScale, modelScale, modelScale);
455
-   app.root.addChild(modelEntity);
456
-
457
-   // --- *** NOUVEAU : Configuration de l'environnement GLB (Physique) *** ---
458
-   envEntity = glbAsset.resource ? glbAsset.resource.instantiateRenderEntity() : null;
459
-   if (envEntity) {
460
-     envEntity.name = "ENV_GLTF";
461
-     app.root.addChild(envEntity);
462
-
463
- tr     let meshCount = 0;
464
-     // Parcours le GLB et ajoute des colliders PHYSIQUES à chaque mesh
465
-     traverse(envEntity, (node) => {
466
-       // Si le nœud est un mesh visible (a un 'render')
467
-       if (node.render) {
468
-         meshCount++;
469
-         // 1. Le rend statique (il ne bouge pas)
470
-         node.addComponent('rigidbody', {
471
-           type: pc.BODYTYPE_STATIC,
472
-           restitution: 0.0
473
-         });
474
-         // 2. Lui donne un collider de type 'mesh' (utilise sa vraie géométrie)
475
-         node.addComponent('collision', {
476
-           type: 'mesh',
477
-           asset: node.render.asset // Utilise l'asset du mesh pour la collision
478
- img       });
479
-       }
480
-     });
481
-     console.log("[VIEWER] Environnement GLB (physique) prêt. Meshs collidables:", meshCount);
482
-   } else {
483
-     console.warn("[VIEWER] GLB resource missing. Aucune collision possible.");
484
-   }
485
-
486
-   // --- *** NOUVEAU : Configuration du Joueur (Physique) *** ---
487
-  
488
-   // 1. L'entité "Player" (le corps physique, la capsule)
489
- section   playerEntity = new pc.Entity("Player");
490
-   playerEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
491
-  
492
-   // Ajoute un corps rigide DYNAMIQUE (il bouge et tombe)
493
-   playerEntity.addComponent('rigidbody', {
494
-     type: pc.BODYTYPE_DYNAMIC,
495
-     mass: 70, // Poids d'un humain
496
-     friction: 0.3, // Un peu de friction pour ne pas glisser
497
-     restitution: 0.0, // Pas de rebond
498
-     angularFactor: new pc.Vec3(0, 0, 0) // Empêche la capsule de basculer
499
-   });
500
-  
501
-   // Ajoute une forme de collision CAPSULE
502
-   playerEntity.addComponent('collision', {
503
-     type: 'capsule',
504
-     radius: 0.35, // Rayon de 35cm (diamètre 70cm)
505
-     height: 1.7  // Hauteur totale 1m70
506
-   });
507
-
508
-   // 2. L'entité "Camera" (les yeux)
509
-   cameraEntity = new pc.Entity("Camera");
510
-   cameraEntity.addComponent("camera", {
511
-     clearColor: new pc.Color(color_bg),
512
-     nearClip: 0.02,
513
-     farClip: 250
514
- NT   });
515
-   // Positionne la caméra à hauteur des "yeux" dans la capsule
516
-   // (Hauteur / 2) - Rayon = (1.7 / 2) - 0.35 = 0.85 - 0.35 = 0.5
517
-   cameraEntity.setLocalPosition(0, 0.5, 0); // Hauteur des yeux
518
-
519
-   // 3. Attache la caméra au joueur
520
-   playerEntity.addChild(cameraEntity);
521
-
522
-   // 4. Ajoute le script de contrôle au joueur
523
-   playerEntity.addComponent("script");
524
-   playerEntity.script.create("orbitCamera", {
525
-     attributes: {
526
-       cameraEntity: cameraEntity,
527
-       moveSpeed: 4.0, // Vitesse de 4 m/s (marche rapide)
528
-       lookSpeed: 0.25,
529
-       pitchAngleMax: maxAngle, // Utilise vos configs
530
-       pitchAngleMin: minAngle
531
-     }
532
-   });
533
-  
534
-   // 5. Ajoute le joueur à la scène
535
-   app.root.addChild(playerEntity);
536
-
537
-   // Taille initiale
538
-   app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
539
-
540
-   // --- Reset (Inchangé, mais ne fait plus grand chose) ---
541
-   app.once("update", () => resetViewerCamera());
542
-
543
-   // --- Perf dynamique (Inchangé) ---
544
-   const setDpr = (val) => {
545
-     const clamped = Math.max(0.5, Math.min(val, maxDevicePixelRatio));
546
-     if (app.graphicsDevice.maxPixelRatio !== clamped) {
547
-       app.graphicsDevice.maxPixelRatio = clamped;
548
-       app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
549
- ci   }
550
-   };
551
-   const bumpInteraction = () => {
552
-     setDpr(interactDpr);
553
-     if (idleTimer) clearTimeout(idleTimer);
554
-     idleTimer = setTimeout(() => {
555
-       setDpr(Math.min(window.devicePixelRatio || 1, maxDevicePixelRatio));
556
-     }, idleRestoreDelay);
557
-   };
558
-   const interactionEvents = ["mousedown", "mousemove", "mouseup", "wheel", "touchstart", "touchmove", "keydown"];
559
-   interactionEvents.forEach((ev) => {
560
-     canvas.addEventListener(ev, bumpInteraction, { passive: true });
561
-   });
562
-
563
-   viewerInitialized = true;
564
-   console.log("[VIEWER] READY — physics=ON, env=", !!envEntity, "sog=", !!modelEntity);
 
 
 
 
 
 
 
 
 
 
 
 
565
  }
566
 
567
  /* -------------------------------------------
568
-    Reset caméra (Modifié pour le joueur)
569
  -------------------------------------------- */
570
-
571
- export function resetViewerCamera() {
572
-   try {
573
-     if (!playerEntity || !modelEntity || !app) return;
574
-     const camScript = playerEntity.script && playerEntity.script.orbitCamera;
575
-     if (!camScript) return;
576
-
577
-     const modelPos = modelEntity.getPosition();
578
-    
579
-     // Téléporte le JOUEUR (pas la caméra)
580
-     playerEntity.rigidbody.teleport(chosenCameraX, chosenCameraY, chosenCameraZ);
581
-     playerEntity.rigidbody.linearVelocity = pc.Vec3.ZERO;
582
-     playerEntity.rigidbody.angularVelocity = pc.Vec3.ZERO;
583
-    
584
-     // Réinitialise le regard
585
-     // Calcule le yaw nécessaire pour regarder vers le centre du modèle (modelPos)
586
-     const targetPos = new pc.Vec3(modelPos.x, playerEntity.getPosition().y, modelPos.z);
587
-     playerEntity.lookAt(targetPos);
588
-    
589
-     // Met à jour les angles dans le script
590
-     var angles = playerEntity.getLocalEulerAngles();
591
-     camScript.yaw = angles.y;
592
-     camScript.pitch = 0; // Regarde droit devant
593
-    
594
-   } catch (e) {
595
-     console.error("[viewer.js] resetViewerCamera error:", e);
596
-   }
597
- }
 
1
+ // viewer_pr_env.js — Ammo Physics + Box Colliders from GLB AABBs
2
+ // ============================================================================
3
+ // - Charge PlayCanvas (ESM)
4
+ // - Charge Ammo (WASM) avec fallback JS et timeout (anti “écran noir”)
5
+ // - Instancie le GLB d’environnement et génère des colliders ‘box’ statiques
6
+ // en lisant les AABB des meshInstances (aucun asset collision requis)
7
+ // - Crée un “player” capsule dynamique et attache la caméra en enfant (yeux)
8
+ // - Conserve GSplat (.sog) si fourni
9
+ // - Logs détaillés pour le debug
10
+ // ============================================================================
11
 
12
  /* -------------------------------------------
13
+ Surface d’erreurs globale (utile sur HF/iframes)
14
  -------------------------------------------- */
15
+ window.addEventListener('error', e => {
16
+ console.error('[BOOT] Uncaught error:', e.message || e.error, e);
17
+ });
18
+ window.addEventListener('unhandledrejection', e => {
19
+ console.error('[BOOT] Unhandled promise rejection:', e.reason);
20
+ });
21
+ console.log('[BOOT] JS boot reached');
22
 
23
+ /* -------------------------------------------
24
+ Utils
25
+ -------------------------------------------- */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  function hexToRgbaArray(hex) {
27
+ try {
28
+ hex = String(hex || "").replace("#", "");
29
+ if (hex.length === 6) hex += "FF";
30
+ if (hex.length !== 8) return [1, 1, 1, 1];
31
+ const num = parseInt(hex, 16);
32
+ return [
33
+ ((num >> 24) & 0xff) / 255,
34
+ ((num >> 16) & 0xff) / 255,
35
+ ((num >> 8) & 0xff) / 255,
36
+ (num & 0xff) / 255
37
+ ];
38
+ } catch (e) {
39
+ console.warn("hexToRgbaArray error:", e);
40
+ return [1, 1, 1, 1];
41
+ }
42
  }
43
 
44
  function traverse(entity, callback) {
45
+ callback(entity);
46
+ if (entity.children && entity.children.length) {
47
+ entity.children.forEach((child) => traverse(child, callback));
48
+ }
49
+ }
50
+
51
+ function isMobileUA() {
52
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
53
+ const isAndroid = /Android/i.test(navigator.userAgent);
54
+ return isIOS || isAndroid;
55
  }
56
 
57
+ // Patch global Image -> force CORS
58
+ (function () {
59
+ const OriginalImage = window.Image;
60
+ window.Image = function (...args) {
61
+ const img = new OriginalImage(...args);
62
+ img.crossOrigin = "anonymous";
63
+ return img;
64
+ };
65
+ })();
66
 
67
  /* -------------------------------------------
68
+ Chargement Ammo (WASM + fallback JS) avec timeout
69
  -------------------------------------------- */
70
+ async function loadAmmoWithFallback(pc, baseUrl = "https://playcanvas.github.io/examples/lib/ammo/", timeoutMs = 5000) {
71
+ try {
72
+ pc.WasmModule.setConfig("Ammo", {
73
+ glueUrl: baseUrl + "ammo.wasm.js",
74
+ wasmUrl: baseUrl + "ammo.wasm.wasm",
75
+ fallbackUrl: baseUrl + "ammo.js"
76
+ });
77
+
78
+ const p = pc.WasmModule.getInstance("Ammo", baseUrl + "ammo.wasm.js");
79
+ if (p && typeof p.then === "function") {
80
+ await Promise.race([
81
+ p,
82
+ new Promise((_, rej) => setTimeout(() => rej(new Error("Ammo load timeout (promise)")), timeoutMs))
83
+ ]);
84
+ console.log("[Ammo] WASM ready (promise API).");
85
+ return true;
86
+ }
87
+
88
+ // Callback API (rarement nécessaire)
89
+ await Promise.race([
90
+ new Promise((resolve) => pc.WasmModule.getInstance("Ammo", resolve)),
91
+ new Promise((_, rej) => setTimeout(() => rej(new Error("Ammo load timeout (callback)")), timeoutMs))
92
+ ]);
93
+ console.log("[Ammo] JS fallback ready (callback API).");
94
+ return true;
95
+ } catch (e) {
96
+ console.warn("[Ammo] load failed:", e);
97
+ return false;
98
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  }
100
 
101
  /* -------------------------------------------
102
+ State (module / instance)
103
  -------------------------------------------- */
 
104
  let pc;
105
  export let app = null;
106
+ let playerEntity = null; // capsule dynamique (rigidbody)
107
+ let cameraEntity = null; // caméra enfant
108
+ let modelEntity = null; // GSplat principal (optionnel)
109
+ let envEntity = null; // GLB instancié (environnement render)
110
+
111
  let viewerInitialized = false;
112
  let resizeObserver = null;
113
  let resizeTimeout = null;
114
 
115
+ // Config / paramètres courants
 
 
 
 
116
  let sogUrl, glbUrl, presentoirUrl;
117
+ let color_bg_hex, color_bg;
118
+ let espace_expo_bool;
119
+
120
+ // Camera spawn
121
+ let chosenCameraX, chosenCameraY, chosenCameraZ;
122
 
123
+ // DPR / perf
124
  let maxDevicePixelRatio = 1.75;
125
  let interactDpr = 1.0;
126
  let idleRestoreDelay = 350;
127
  let idleTimer = null;
128
 
129
+ // Physique
130
+ let physicsEnabled = true; // si Ammo indisponible -> false
131
+ let ammoBaseUrl = "https://playcanvas.github.io/examples/lib/ammo/";
132
+ let freeFly = false; // si true => gravity zero (option)
133
  /* -------------------------------------------
134
+ Script FPS minimal (yaw/pitch + ZQSD)
135
+ (nom = 'orbitCamera' pour compat)
136
  -------------------------------------------- */
137
+ function ensureFirstPersonScriptRegistered() {
138
+ if (window.__PLY_FPS_REG__) return;
139
+ window.__PLY_FPS_REG__ = true;
140
+
141
+ var FPS = pc.createScript('orbitCamera');
142
+
143
+ FPS.attributes.add('cameraEntity', { type: 'entity', title: 'Camera (child)' });
144
+ FPS.attributes.add('moveSpeed', { type: 'number', default: 4.0, title: 'Move Speed (m/s)' });
145
+ FPS.attributes.add('lookSpeed', { type: 'number', default: 0.25, title: 'Look Sensitivity' });
146
+ FPS.attributes.add('pitchAngleMin', { type: 'number', default: -89, title: 'Pitch Min (deg)' });
147
+ FPS.attributes.add('pitchAngleMax', { type: 'number', default: 89, title: 'Pitch Max (deg)' });
148
+ FPS.attributes.add('freeFly', { type: 'boolean', default: false, title: 'Free Fly (no gravity)' });
149
+
150
+ FPS.prototype.initialize = function () {
151
+ this.yaw = 0;
152
+ this.pitch = 0;
153
+ this._v = new pc.Vec3();
154
+
155
+ // init yaw/pitch depuis rotations actuelles
156
+ var qCam = (this.cameraEntity || this.entity).getRotation();
157
+ var f = new pc.Vec3(); qCam.transformVector(pc.Vec3.FORWARD, f);
158
+ this.yaw = Math.atan2(f.x, f.z) * pc.math.RAD_TO_DEG;
159
+ var yawQ = new pc.Quat().setFromEulerAngles(0, -this.yaw, 0);
160
+ var noYawQ = new pc.Quat().mul2(yawQ, qCam);
161
+ var fNoYaw = new pc.Vec3(); noYawQ.transformVector(pc.Vec3.FORWARD, fNoYaw);
162
+ this.pitch = pc.math.clamp(Math.atan2(-fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG, this.pitchAngleMin, this.pitchAngleMax);
163
+
164
+ // pointer lock au clic gauche
165
+ if (this.app.mouse) {
166
+ this.app.mouse.on(pc.EVENT_MOUSEDOWN, (e) => {
167
+ if (e.button === pc.MOUSEBUTTON_LEFT && document.activeElement === this.app.graphicsDevice.canvas) {
168
+ const c = this.app.graphicsDevice.canvas;
169
+ if (c.requestPointerLock) c.requestPointerLock();
170
+ }
171
+ }, this);
172
+
173
+ this.app.mouse.on(pc.EVENT_MOUSEMOVE, (e) => {
174
+ if (document.pointerLockElement !== this.app.graphicsDevice.canvas) return;
175
+ this.yaw -= e.dx * this.lookSpeed;
176
+ this.pitch -= e.dy * this.lookSpeed;
177
+ this.pitch = pc.math.clamp(this.pitch, this.pitchAngleMin, this.pitchAngleMax);
178
+ }, this);
179
+ }
180
+ };
181
+
182
+ FPS.prototype.update = function (dt) {
183
+ // yaw/pitch
184
+ this.entity.setLocalEulerAngles(0, this.yaw, 0);
185
+ if (this.cameraEntity) this.cameraEntity.setLocalEulerAngles(this.pitch, 0, 0);
186
+
187
+ // mouvements
188
+ const kb = this.app.keyboard;
189
+ let fwd = 0, str = 0, up = 0;
190
+ if (kb) {
191
+ fwd += (kb.isPressed(pc.KEY_W) || kb.isPressed(pc.KEY_Z) || kb.isPressed(pc.KEY_UP)) ? 1 : 0;
192
+ fwd -= (kb.isPressed(pc.KEY_S) || kb.isPressed(pc.KEY_DOWN)) ? 1 : 0;
193
+ str += (kb.isPressed(pc.KEY_D) || kb.isPressed(pc.KEY_RIGHT)) ? 1 : 0;
194
+ str -= (kb.isPressed(pc.KEY_A) || kb.isPressed(pc.KEY_Q) || kb.isPressed(pc.KEY_LEFT)) ? 1 : 0;
195
+
196
+ // free-fly: E/Space monte, C/Ctrl descend
197
+ if (this.freeFly) {
198
+ up += (kb.isPressed(pc.KEY_E) || kb.isPressed(pc.KEY_SPACE)) ? 1 : 0;
199
+ up -= (kb.isPressed(pc.KEY_C) || kb.isPressed(pc.KEY_CTRL)) ? 1 : 0;
200
+ }
201
+ }
202
+
203
+ const body = this.entity.rigidbody;
204
+ if (!body) return;
205
+
206
+ // directions
207
+ const f = this.entity.forward.clone();
208
+ const r = this.entity.right.clone();
209
+ if (!this.freeFly) { f.y = 0; r.y = 0; }
210
+ if (f.lengthSq()>1e-8) f.normalize();
211
+ if (r.lengthSq()>1e-8) r.normalize();
212
+
213
+ // vélocité cible
214
+ const target = new pc.Vec3()
215
+ .add(f.mulScalar(fwd * this.moveSpeed))
216
+ .add(r.mulScalar(str * this.moveSpeed));
217
+
218
+ if (this.freeFly) target.y = up * this.moveSpeed;
219
+ else target.y = body.linearVelocity.y; // conserve gravité
220
+
221
+ // applique
222
+ this._v.copy(body.linearVelocity);
223
+ this._v.lerp(this._v, target, Math.min(1, dt*10));
224
+ body.linearVelocity = this._v;
225
+ };
226
+ }
227
 
228
+ /* -------------------------------------------
229
+ Initialisation principale
230
+ -------------------------------------------- */
231
  export async function initializeViewer(config, instanceId) {
232
+ if (viewerInitialized) return;
233
+
234
+ const mobile = isMobileUA();
235
+ console.log("[VIEWER] A: initializeViewer begin ", { instanceId });
236
+
237
+ // ---- Lecture config ----
238
+ sogUrl = config.sog_url || config.sogs_json_url || null;
239
+ glbUrl = (config.glb_url !== undefined) ? config.glb_url : null;
240
+ presentoirUrl = (config.presentoir_url !== undefined) ? config.presentoir_url : null;
241
+
242
+ color_bg_hex = config.canvas_background !== undefined ? config.canvas_background : "#FFFFFF";
243
+ espace_expo_bool = config.espace_expo_bool !== undefined ? !!config.espace_expo_bool : false;
244
+ color_bg = hexToRgbaArray(color_bg_hex);
245
+
246
+ // Camera spawn valeurs par défaut
247
+ const camX = config.cameraX !== undefined ? parseFloat(config.cameraX) : 0;
248
+ const camY = config.cameraY !== undefined ? parseFloat(config.cameraY) : 1.6;
249
+ const camZ = config.cameraZ !== undefined ? parseFloat(config.cameraZ) : 4.0;
250
+
251
+ const camXPhone = config.cameraXPhone !== undefined ? parseFloat(config.cameraXPhone) : camX;
252
+ const camYPhone = config.cameraYPhone !== undefined ? parseFloat(config.cameraYPhone) : camY;
253
+ const camZPhone = config.cameraZPhone !== undefined ? parseFloat(config.cameraZPhone) : camZ * 1.5;
254
+
255
+ chosenCameraX = mobile ? camXPhone : camX;
256
+ chosenCameraY = mobile ? camYPhone : camY;
257
+ chosenCameraZ = mobile ? camZPhone : camZ;
258
+
259
+ // DPR / perf
260
+ if (config.maxDevicePixelRatio !== undefined) {
261
+ maxDevicePixelRatio = Math.max(0.75, parseFloat(config.maxDevicePixelRatio) || maxDevicePixelRatio);
262
+ }
263
+ if (config.interactionPixelRatio !== undefined) {
264
+ interactDpr = Math.max(0.75, parseFloat(config.interactionPixelRatio) || interactDpr);
265
+ }
266
+ if (config.idleRestoreDelayMs !== undefined) {
267
+ idleRestoreDelay = Math.max(120, parseInt(config.idleRestoreDelayMs, 10) || idleRestoreDelay);
268
+ }
269
+
270
+ // Physique
271
+ physicsEnabled = config.usePhysics === false ? false : true; // par défaut true
272
+ freeFly = !!config.freeFly;
273
+ if (config.ammoBaseUrl) {
274
+ ammoBaseUrl = String(config.ammoBaseUrl).endsWith("/") ? config.ammoBaseUrl : (config.ammoBaseUrl + "/");
275
+ }
276
+
277
+ // ---- Canvas / DOM ----
278
+ const canvasId = "canvas-" + instanceId;
279
+ const progressDialog = document.getElementById("progress-dialog-" + instanceId);
280
+ const viewerContainer = document.getElementById("viewer-container-" + instanceId);
281
+
282
+ const old = document.getElementById(canvasId);
283
+ if (old) old.remove();
284
+
285
+ const canvas = document.createElement("canvas");
286
+ canvas.id = canvasId;
287
+ canvas.className = "ply-canvas";
288
+ canvas.style.width = "100%";
289
+ canvas.style.height = "100%";
290
+ canvas.setAttribute("tabindex", "0");
291
+ viewerContainer.insertBefore(canvas, progressDialog);
292
+
293
+ // interactions de base
294
+ canvas.style.touchAction = "none";
295
+ canvas.style.webkitTouchCallout = "none";
296
+ canvas.addEventListener("gesturestart", (e) => e.preventDefault());
297
+ canvas.addEventListener("gesturechange", (e) => e.preventDefault());
298
+ canvas.addEventListener("gestureend", (e) => e.preventDefault());
299
+ canvas.addEventListener("dblclick", (e) => e.preventDefault());
300
+ canvas.addEventListener("wheel", (e) => e.preventDefault(), { passive: false });
301
+
302
+ const scrollKeys = new Set(["ArrowUp","ArrowDown","ArrowLeft","ArrowRight","PageUp","PageDown","Home","End"," ","Space","Spacebar"]);
303
+ let isPointerOverCanvas = false;
304
+ const focusCanvas = () => canvas.focus({ preventScroll: true });
305
+ const onPointerEnter = () => { isPointerOverCanvas = true; focusCanvas(); };
306
+ const onPointerLeave = () => { isPointerOverCanvas = false; if (document.activeElement === canvas) canvas.blur(); };
307
+ const onCanvasBlur = () => { isPointerOverCanvas = false; };
308
+
309
+ canvas.addEventListener("pointerenter", onPointerEnter);
310
+ canvas.addEventListener("pointerleave", onPointerLeave);
311
+ canvas.addEventListener("mouseenter", onPointerEnter);
312
+ canvas.addEventListener("mouseleave", onPointerLeave);
313
+ canvas.addEventListener("mousedown", focusCanvas);
314
+ canvas.addEventListener("touchstart", focusCanvas, { passive: true });
315
+ canvas.addEventListener("blur", onCanvasBlur);
316
+
317
+ const onKeyDownCapture = (e) => { if (!isPointerOverCanvas) return; if (scrollKeys.has(e.key) || scrollKeys.has(e.code)) e.preventDefault(); };
318
+ window.addEventListener("keydown", onKeyDownCapture, true);
319
+
320
+ progressDialog.style.display = "block";
321
+
322
+ // ---- Import PlayCanvas ----
323
+ if (!pc) {
324
+ pc = await import("https://esm.run/playcanvas");
325
+ window.pc = pc; // debug
326
+ }
327
+ console.log("[VIEWER] PlayCanvas ESM chargé:", !!pc);
328
+
329
+ // ---- Charge Ammo si activé ----
330
+ if (physicsEnabled) {
331
+ const ammoOk = await loadAmmoWithFallback(pc, ammoBaseUrl, mobile ? 6000 : 4000);
332
+ physicsEnabled = !!ammoOk;
333
+ console.log("[VIEWER] Ammo status:", physicsEnabled ? "OK" : "DISABLED");
334
+ }
335
+
336
+ // ---- Crée l’Application ----
337
+ const device = await pc.createGraphicsDevice(canvas, {
338
+ deviceTypes: ["webgl2", "webgl1"],
339
+ antialias: false
340
+ });
341
+ device.maxPixelRatio = Math.min(window.devicePixelRatio || 1, maxDevicePixelRatio);
342
+
343
+ const opts = new pc.AppOptions();
344
+ opts.graphicsDevice = device;
345
+ opts.mouse = new pc.Mouse(canvas);
346
+ opts.touch = new pc.TouchDevice(canvas);
347
+ opts.keyboard= new pc.Keyboard(canvas);
348
+ // IMPORTANT : inclure Collision et Rigidbody
349
+ opts.componentSystems = [
350
+ pc.RenderComponentSystem,
351
+ pc.CameraComponentSystem,
352
+ pc.LightComponentSystem,
353
+ pc.ScriptComponentSystem,
354
+ pc.GSplatComponentSystem,
355
+ pc.CollisionComponentSystem,
356
+ pc.RigidbodyComponentSystem
357
+ ];
358
+ opts.resourceHandlers = [pc.TextureHandler, pc.ContainerHandler, pc.ScriptHandler, pc.GSplatHandler];
359
+
360
+ app = new pc.Application(canvas, opts);
361
+ app.setCanvasFillMode(pc.FILLMODE_NONE);
362
+ app.setCanvasResolution(pc.RESOLUTION_AUTO);
363
+
364
+ // Gravité selon le mode
365
+ app.scene.gravity = freeFly ? new pc.Vec3(0, 0, 0) : new pc.Vec3(0, -9.81, 0);
366
+
367
+ // Resize observé (debounce)
368
+ resizeObserver = new ResizeObserver((entries) => {
369
+ if (!entries || !entries.length) return;
370
+ if (resizeTimeout) clearTimeout(resizeTimeout);
371
+ resizeTimeout = setTimeout(() => {
372
+ app.resizeCanvas(entries[0].contentRect.width, entries[0].contentRect.height);
373
+ }, 60);
374
+ });
375
+ resizeObserver.observe(viewerContainer);
376
+
377
+ window.addEventListener("resize", () => {
378
+ if (resizeTimeout) clearTimeout(resizeTimeout);
379
+ resizeTimeout = setTimeout(() => {
380
+ app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
381
+ }, 60);
382
+ });
383
+
384
+ app.on("destroy", () => {
385
+ try { resizeObserver.disconnect(); } catch {}
386
+ if (opts.keyboard && opts.keyboard.detach) opts.keyboard.detach();
387
+ window.removeEventListener("keydown", onKeyDownCapture, true);
388
+
389
+ canvas.removeEventListener("pointerenter", onPointerEnter);
390
+ canvas.removeEventListener("pointerleave", onPointerLeave);
391
+ canvas.removeEventListener("mouseenter", onPointerEnter);
392
+ canvas.removeEventListener("mouseleave", onPointerLeave);
393
+ canvas.removeEventListener("mousedown", focusCanvas);
394
+ canvas.removeEventListener("touchstart", focusCanvas);
395
+ canvas.removeEventListener("blur", onCanvasBlur);
396
+ });
397
+
398
+ // ---- Assets (SOG + GLB) ----
399
+ const assets = [];
400
+ let sogAsset = null, glbAsset = null;
401
+
402
+ if (sogUrl) {
403
+ sogAsset = new pc.Asset("gsplat", "gsplat", { url: sogUrl });
404
+ app.assets.add(sogAsset); assets.push(sogAsset);
405
+ }
406
+ if (glbUrl) {
407
+ glbAsset = new pc.Asset("glb", "container", { url: glbUrl });
408
+ app.assets.add(glbAsset); assets.push(glbAsset);
409
+ } else {
410
+ console.warn("[VIEWER] Aucun glb_url fourni — rien à afficher.");
411
+ }
412
+
413
+ // Script FPS (compat nom 'orbitCamera')
414
+ ensureFirstPersonScriptRegistered();
415
+
416
+ // Charge les assets requis
417
+ await new Promise((resolve, reject) => {
418
+ const loader = new pc.AssetListLoader(assets, app.assets);
419
+ loader.load(() => resolve());
420
+ loader.on('error', (e)=>{ console.error('[VIEWER] Asset load error:', e); reject(e); });
421
+ });
422
+
423
+ app.start();
424
+ progressDialog.style.display = "none";
425
+ console.log("[VIEWER] app.start OK — assets chargés");
426
+
427
+ // ---- GSplat (optionnel) ----
428
+ if (sogAsset) {
429
+ modelEntity = new pc.Entity("model");
430
+ modelEntity.addComponent("gsplat", { asset: sogAsset });
431
+ app.root.addChild(modelEntity);
432
+ }
433
+
434
+ // ---- Environnement GLB ----
435
+ envEntity = glbAsset && glbAsset.resource ? glbAsset.resource.instantiateRenderEntity() : null;
436
+ if (envEntity) {
437
+ envEntity.name = "ENV_GLTF";
438
+ app.root.addChild(envEntity);
439
+
440
+ // Mat “fond uni” si demandé
441
+ if (!espace_expo_bool) {
442
+ const matSol = new pc.StandardMaterial();
443
+ matSol.blendType = pc.BLEND_NONE;
444
+ matSol.emissive = new pc.Color(color_bg);
445
+ matSol.emissiveIntensity = 1;
446
+ matSol.useLighting = false;
447
+ matSol.update();
448
+
449
+ traverse(envEntity, (node) => {
450
+ if (node.render && node.render.meshInstances) {
451
+ for (const mi of node.render.meshInstances) mi.material = matSol;
452
+ }
453
+ });
454
+ }
455
+
456
+ // ---- Colliders statiques “box” depuis AABB de chaque meshInstance ----
457
+ // Avantage : pas besoin d'asset collision, super robuste.
458
+ if (physicsEnabled) {
459
+ let rawCount = 0;
460
+ let created = 0;
461
+ traverse(envEntity, (node) => {
462
+ if (node.render && node.render.meshInstances && node.render.meshInstances.length) {
463
+ for (const mi of node.render.meshInstances) {
464
+ rawCount++;
465
+ const aabb = mi.aabb; // monde (pour render instancié, AABB est world-space)
466
+ const center = aabb.center.clone();
467
+ const he = aabb.halfExtents.clone();
468
+
469
+ // crée un enfant collider “box” placé au centre de l’AABB
470
+ const box = new pc.Entity(`COLL_BOX_${created}`);
471
+ box.setPosition(center);
472
+ box.addComponent('collision', {
473
+ type: 'box',
474
+ halfExtents: he
475
+ });
476
+ box.addComponent('rigidbody', {
477
+ type: 'static',
478
+ friction: 0.6,
479
+ restitution: 0.0
480
+ });
481
+ app.root.addChild(box);
482
+ created++;
483
+ }
484
+ }
485
+ });
486
+ console.log(`[VIEWER] Colliders statiques créés: ${created} (raw meshInstances=${rawCount})`);
487
+ } else {
488
+ console.warn("[VIEWER] Physics disabled: aucun collider statique créé.");
489
+ }
490
+ } else {
491
+ console.warn("[VIEWER] GLB resource missing. Rien à collider.");
492
+ }
493
+
494
+ // ---- Caméra + Player capsule ----
495
+ cameraEntity = new pc.Entity("Camera");
496
+ cameraEntity.addComponent("camera", {
497
+ clearColor: new pc.Color(color_bg),
498
+ nearClip: 0.03,
499
+ farClip: 500
500
+ });
501
+
502
+ if (physicsEnabled) {
503
+ playerEntity = new pc.Entity("Player");
504
+ const capsuleRadius = config.capsuleRadius !== undefined ? parseFloat(config.capsuleRadius) : 0.35;
505
+ const capsuleHeight = config.capsuleHeight !== undefined ? parseFloat(config.capsuleHeight) : 1.70;
506
+
507
+ playerEntity.addComponent('collision', {
508
+ type: 'capsule',
509
+ radius: capsuleRadius,
510
+ height: capsuleHeight
511
+ });
512
+ playerEntity.addComponent('rigidbody', {
513
+ type: 'dynamic',
514
+ mass: 75,
515
+ friction: 0.3,
516
+ restitution: 0.0,
517
+ linearDamping: 0.15,
518
+ angularDamping: 0.999
519
+ });
520
+ // Bloque la rotation pour éviter le roulis
521
+ playerEntity.rigidbody.angularFactor = new pc.Vec3(0, 0, 0);
522
+
523
+ // Spawn
524
+ playerEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
525
+ app.root.addChild(playerEntity);
526
+
527
+ // Caméra à hauteur des yeux
528
+ const eyes = config.eyesOffsetY !== undefined
529
+ ? parseFloat(config.eyesOffsetY)
530
+ : Math.max(0.4, capsuleHeight * 0.9 - capsuleRadius); // ~ yeux
531
+ cameraEntity.setLocalPosition(0, eyes, 0);
532
+ playerEntity.addChild(cameraEntity);
533
+
534
+ // Script FPS
535
+ playerEntity.addComponent("script");
536
+ playerEntity.script.create("orbitCamera", {
537
+ attributes: {
538
+ cameraEntity: cameraEntity,
539
+ moveSpeed: (config.moveSpeed !== undefined ? parseFloat(config.moveSpeed) : 4.0),
540
+ lookSpeed: (config.lookSpeed !== undefined ? parseFloat(config.lookSpeed) : 0.25),
541
+ pitchAngleMin: (config.minAngle !== undefined ? parseFloat(config.minAngle) : -89),
542
+ pitchAngleMax: (config.maxAngle !== undefined ? parseFloat(config.maxAngle) : 89),
543
+ freeFly: !!freeFly
544
+ }
545
+ });
546
+ } else {
547
+ // Pas de physique : caméra seule (pas recommandé si tu veux collisions)
548
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
549
+ app.root.addChild(cameraEntity);
550
+ }
551
+
552
+ // Regarder vers le modèle si présent, sinon vers l’origine
553
+ const lookTarget = modelEntity ? modelEntity.getPosition() : new pc.Vec3(0, 1, 0);
554
+ cameraEntity.lookAt(lookTarget);
555
+
556
+ // Taille initiale
557
+ app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
558
+
559
+ // DPR dynamique : réduit pendant interaction
560
+ const setDpr = (val) => {
561
+ const clamped = Math.max(0.5, Math.min(val, maxDevicePixelRatio));
562
+ if (app.graphicsDevice.maxPixelRatio !== clamped) {
563
+ app.graphicsDevice.maxPixelRatio = clamped;
564
+ app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
565
+ }
566
+ };
567
+ const bumpInteraction = () => {
568
+ setDpr(interactDpr);
569
+ if (idleTimer) clearTimeout(idleTimer);
570
+ idleTimer = setTimeout(() => {
571
+ setDpr(Math.min(window.devicePixelRatio || 1, maxDevicePixelRatio));
572
+ }, idleRestoreDelay);
573
+ };
574
+ ["mousedown", "mousemove", "mouseup", "wheel", "touchstart", "touchmove", "keydown"]
575
+ .forEach((ev) => canvas.addEventListener(ev, bumpInteraction, { passive: true }));
576
+
577
+ viewerInitialized = true;
578
+ console.log("[VIEWER] READY — physics=", physicsEnabled ? "ON" : "OFF", "env=", !!envEntity, "sog=", !!modelEntity);
579
  }
580
 
581
  /* -------------------------------------------
582
+ API helper : repositionner le joueur/caméra
583
  -------------------------------------------- */
584
+ export function resetViewerCamera(x, y, z) {
585
+ try {
586
+ if (!app) return;
587
+ const nx = (x !== undefined) ? parseFloat(x) : null;
588
+ const ny = (y !== undefined) ? parseFloat(y) : null;
589
+ const nz = (z !== undefined) ? parseFloat(z) : null;
590
+
591
+ if (playerEntity && playerEntity.rigidbody) {
592
+ const p = playerEntity.getPosition().clone();
593
+ playerEntity.rigidbody.teleport(nx ?? p.x, ny ?? p.y, nz ?? p.z, playerEntity.getRotation());
594
+ playerEntity.rigidbody.linearVelocity = new pc.Vec3(0, 0, 0);
595
+ playerEntity.rigidbody.angularVelocity = new pc.Vec3(0, 0, 0);
596
+ } else if (cameraEntity) {
597
+ const p = cameraEntity.getPosition().clone();
598
+ cameraEntity.setPosition(nx ?? p.x, ny ?? p.y, nz ?? p.z);
599
+ }
600
+ } catch (e) {
601
+ console.error("[viewer_pr_env] resetViewerCamera error:", e);
602
+ }
603
+ }