MikaFil commited on
Commit
5f7f32a
·
verified ·
1 Parent(s): 97e1c09

Update deplacement_dans_env/viewer_pr_env.js

Browse files
Files changed (1) hide show
  1. deplacement_dans_env/viewer_pr_env.js +691 -210
deplacement_dans_env/viewer_pr_env.js CHANGED
@@ -1,8 +1,11 @@
1
- // viewer.js
2
  // ==============================
 
 
 
3
 
4
  /* -------------------------------------------
5
- Utils
6
  -------------------------------------------- */
7
 
8
  // (Conservé pour compat, même si .sog n'en a pas besoin)
@@ -60,34 +63,654 @@ function traverse(entity, 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
  /* -------------------------------------------
@@ -127,19 +750,12 @@ export async function initializeViewer(config, instanceId) {
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");
@@ -213,67 +829,38 @@ export async function initializeViewer(config, instanceId) {
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
 
@@ -282,8 +869,9 @@ export async function initializeViewer(config, instanceId) {
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, {
@@ -292,8 +880,6 @@ export async function initializeViewer(config, instanceId) {
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();
@@ -334,11 +920,8 @@ export async function initializeViewer(config, instanceId) {
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
 
@@ -357,18 +940,19 @@ export async function initializeViewer(config, instanceId) {
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");
@@ -383,29 +967,31 @@ export async function initializeViewer(config, instanceId) {
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,
@@ -413,16 +999,22 @@ export async function initializeViewer(config, instanceId) {
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
 
@@ -454,72 +1046,8 @@ export async function initializeViewer(config, instanceId) {
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
  /* -------------------------------------------
@@ -534,55 +1062,8 @@ export function resetViewerCamera() {
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
  }
 
1
+ // viewer_pr_env.js
2
  // ==============================
3
+ // Version intégrant le contrôleur caméra en local (pas de <script src=...>)
4
+ // + Fix "sortie du monde" : clamp dans la worldAabb et pas de culling de boîtes quasi-globales
5
+ // + Logs détaillés
6
 
7
  /* -------------------------------------------
8
+ Utils communs
9
  -------------------------------------------- */
10
 
11
  // (Conservé pour compat, même si .sog n'en a pas besoin)
 
63
  }
64
  }
65
 
66
+ // Helpers math pour le script caméra
67
+ function vAdd(a,b){ return new pc.Vec3(a.x+b.x,a.y+b.y,a.z+b.z); }
68
+ function vSub(a,b){ return new pc.Vec3(a.x-b.x,a.y-b.y,a.z-b.z); }
69
+ function vScale(v,s){ return new pc.Vec3(v.x*s,v.y*s,v.z*s); }
70
+ function vLen(v){ return Math.sqrt(v.x*v.x+v.y*v.y+v.z*v.z); }
71
+ function vDot(a,b){ return a.x*b.x+a.y*b.y+a.z*b.z; }
72
+ function clamp(v,a,b){ return Math.max(a, Math.min(b,v)); }
73
+
74
  /* -------------------------------------------
75
+ Scripts caméra intégrés (ex-ctrl_camera_pr_env.js)
76
+ — collisions capsule vs AABBs + cage monde
77
  -------------------------------------------- */
78
+ function registerFreeCamScripts() {
79
+ if (window.__PLY_FREECAM_REG__) return;
80
+ window.__PLY_FREECAM_REG__ = true;
81
+
82
+ // ctrl_camera_pr_env.js — corrigé
83
+
84
+ var FreeCamera = pc.createScript('orbitCamera'); // garder le nom public pour compat viewer
85
+
86
+ // ======================== Attributs ===========================
87
+ // Look
88
+ FreeCamera.attributes.add('inertiaFactor', { type: 'number', default: 0.10, title: 'Inertia (rotation)' });
89
+ FreeCamera.attributes.add('pitchAngleMin', { type: 'number', default: -89, title: 'Pitch Min (deg)' });
90
+ FreeCamera.attributes.add('pitchAngleMax', { type: 'number', default: 89, title: 'Pitch Max (deg)' });
91
+
92
+ // Sol mini (filet de sécurité)
93
+ FreeCamera.attributes.add('minY', { type: 'number', default: -10, title: 'Minimum camera Y' });
94
+
95
+ // Vitesse (m/s)
96
+ FreeCamera.attributes.add('moveSpeed', { type: 'number', default: 2.0, title: 'Move Speed' });
97
+ FreeCamera.attributes.add('strafeSpeed', { type: 'number', default: 2.0, title: 'Strafe Speed' });
98
+ FreeCamera.attributes.add('dollySpeed', { type: 'number', default: 2.0, title: 'Mouse/Pinch Dolly Speed' });
99
+
100
+ // Capsule caméra
101
+ FreeCamera.attributes.add('capsuleRadius', { type: 'number', default: 0.30, title: 'Capsule Radius (m)' });
102
+ FreeCamera.attributes.add('capsuleHeight', { type: 'number', default: 1.60, title: 'Capsule Height (m) — yeux à ~0.9m au-dessus du centre' });
103
+ FreeCamera.attributes.add('collisionEps', { type: 'number', default: 0.0005, title: 'Collision Epsilon' });
104
+
105
+ // Mouvement "swept"
106
+ FreeCamera.attributes.add('maxStepDistance', { type: 'number', default: 0.10, title: 'Max step distance (swept move)' }); // réduit (anti-tunnel)
107
+ FreeCamera.attributes.add('maxResolveIters', { type: 'number', default: 8, title: 'Max resolve iterations per step' });
108
+
109
+ // Escaliers
110
+ FreeCamera.attributes.add('stepHeight', { type: 'number', default: 0.35, title: 'Max step-up height (m)' });
111
+ FreeCamera.attributes.add('stepAhead', { type: 'number', default: 0.20, title: 'Probe distance ahead for step (m)' });
112
+ FreeCamera.attributes.add('snapDownMax', { type: 'number', default: 0.60, title: 'Max snap-down (m)' });
113
+ FreeCamera.attributes.add('enableGroundSnap', { type: 'boolean', default: true, title: 'Enable ground snap' });
114
+
115
+ // AABBs (construction "indoor-safe")
116
+ FreeCamera.attributes.add('inflateBias', { type: 'number', default: 0.0, title: 'Extra inflate AABB (m)' });
117
+ FreeCamera.attributes.add('mergeGap', { type: 'number', default: 0.0, title: 'Merge AABBs gap (0: chevauchement réel seulement)' });
118
+ FreeCamera.attributes.add('globalCullFrac',{ type: 'number', default: 0.0, title: 'Cull near-global AABBs (désactivé)' }); // désactivé
119
+
120
+ // BBox globale optionnelle (filet additionnel)
121
+ FreeCamera.attributes.add('Xmin', { type: 'number', default: -Infinity, title: 'BBox Xmin' });
122
+ FreeCamera.attributes.add('Xmax', { type: 'number', default: Infinity, title: 'BBox Xmax' });
123
+ FreeCamera.attributes.add('Ymin', { type: 'number', default: -Infinity, title: 'BBox Ymin' });
124
+ FreeCamera.attributes.add('Ymax', { type: 'number', default: Infinity, title: 'BBox Ymax' });
125
+ FreeCamera.attributes.add('Zmin', { type: 'number', default: -Infinity, title: 'BBox Zmin' });
126
+ FreeCamera.attributes.add('Zmax', { type: 'number', default: Infinity, title: 'BBox Zmax' });
127
+
128
+ // Compat (pour le viewer)
129
+ FreeCamera.attributes.add('focusEntity', { type: 'entity', title: 'Collision Root (ENV GLB)' });
130
+ FreeCamera.attributes.add('frameOnStart', { type: 'boolean', default: false, title: 'Compat: Frame on Start (unused)' });
131
+ FreeCamera.attributes.add('yawAngleMin', { type: 'number', default: -360, title: 'Compat: Yaw Min (unused)' });
132
+ FreeCamera.attributes.add('yawAngleMax', { type: 'number', default: 360, title: 'Compat: Yaw Max (unused)' });
133
+ FreeCamera.attributes.add('distanceMin', { type: 'number', default: 0.1, title: 'Compat: Distance Min (unused)' });
134
+
135
+ // ======================== Capsule util ===========================
136
+ function capsuleSegment(p, height, radius) {
137
+ var seg = Math.max(0, height - 2*radius);
138
+ var half = seg * 0.5;
139
+ return { a: new pc.Vec3(p.x, p.y - half, p.z), b: new pc.Vec3(p.x, p.y + half, p.z), len: seg };
140
+ }
141
+
142
+ function capsuleVsAabbPenetration(center, height, radius, aabb, outPush) {
143
+ var seg = capsuleSegment(center, height, radius);
144
+ var pts = [seg.a, seg.b, new pc.Vec3((seg.a.x+seg.b.x)/2,(seg.a.y+seg.b.y)/2,(seg.a.z+seg.b.z)/2)];
145
+
146
+ var amin = aabb.getMin();
147
+ var amax = aabb.getMax();
148
+ var eps = 1e-9;
149
+
150
+ var bestPen = 0;
151
+ var bestPush = null;
152
+
153
+ for (var i=0;i<pts.length;i++){
154
+ var p = pts[i];
155
+ var cx = clamp(p.x, amin.x, amax.x);
156
+ var cy = clamp(p.y, amin.y, amax.y);
157
+ var cz = clamp(p.z, amin.z, amax.z);
158
+
159
+ var dx = p.x - cx, dy = p.y - cy, dz = p.z - cz;
160
+ var d2 = dx*dx + dy*dy + dz*dz;
161
+ var d = Math.sqrt(Math.max(d2, eps));
162
+ var pen = radius - d;
163
+
164
+ if (pen > bestPen) {
165
+ if (d > 1e-6) {
166
+ bestPush = new pc.Vec3(dx/d * pen, dy/d * pen, dz/d * pen);
167
+ } else {
168
+ var ex = Math.min(Math.abs(p.x - amin.x), Math.abs(amax.x - p.x));
169
+ var ey = Math.min(Math.abs(p.y - amin.y), Math.abs(amax.y - p.y));
170
+ var ez = Math.min(Math.abs(p.z - amin.z), Math.abs(amax.z - p.z));
171
+ if (ex <= ey && ex <= ez) bestPush = new pc.Vec3((Math.abs(p.x - amin.x) < Math.abs(amax.x - p.x) ? -1:1)*pen,0,0);
172
+ else if (ey <= ex && ey <= ez) bestPush = new pc.Vec3(0,(Math.abs(p.y - amin.y) < Math.abs(amax.y - p.y) ? -1:1)*pen,0);
173
+ else bestPush = new pc.Vec3(0,0,(Math.abs(p.z - amin.z) < Math.abs(amax.z - p.z) ? -1:1)*pen);
174
+ }
175
+ bestPen = pen;
176
+ }
177
+ }
178
 
179
+ if (bestPen > 0 && outPush) outPush.copy(bestPush);
180
+ return bestPen;
 
 
 
181
  }
182
 
183
+ // ======================== Getters pitch/yaw ===========================
184
+ Object.defineProperty(FreeCamera.prototype, 'pitch', {
185
+ get: function () { return this._targetPitch; },
186
+ set: function (v) { this._targetPitch = pc.math.clamp(v, this.pitchAngleMin, this.pitchAngleMax); }
187
+ });
188
+ Object.defineProperty(FreeCamera.prototype, 'yaw', {
189
+ get: function () { return this._targetYaw; },
190
+ set: function (v) { this._targetYaw = v; }
 
 
 
 
 
 
191
  });
192
 
193
+ // ======================== Init ===========================
194
+ FreeCamera.prototype.initialize = function () {
195
+ var q = this.entity.getRotation();
196
+ var f = new pc.Vec3(); q.transformVector(pc.Vec3.FORWARD, f);
197
+
198
+ this._yaw = Math.atan2(f.x, f.z) * pc.math.RAD_TO_DEG;
199
+ var yawQuat = new pc.Quat().setFromEulerAngles(0, -this._yaw, 0);
200
+ var noYawQ = new pc.Quat().mul2(yawQuat, q);
201
+ var fNoYaw = new pc.Vec3(); noYawQ.transformVector(pc.Vec3.FORWARD, fNoYaw);
202
+ this._pitch = pc.math.clamp(Math.atan2(-fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG, this.pitchAngleMin, this.pitchAngleMax);
203
+
204
+ this._targetYaw = this._yaw;
205
+ this._targetPitch = this._pitch;
206
+ this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
207
+
208
+ this.app.systems.script.app.freeCamState = this.app.systems.script.app.freeCamState || {};
209
+ this.state = this.app.systems.script.app.freeCamState;
210
+
211
+ // Colliders + cage monde
212
+ this._buildAabbsFromFocus();
213
+
214
+ var p0 = this.entity.getPosition().clone();
215
+ var p1 = this._resolveCapsuleCollisions(p0, this.maxResolveIters);
216
+ if (vLen(vSub(p1,p0)) > this.capsuleRadius * 0.25) {
217
+ var dir = vSub(p1,p0); var L = vLen(dir)||1;
218
+ p1 = vAdd(p0, vScale(dir, (this.capsuleRadius*0.25)/L));
219
+ }
220
+ this._clampBBoxMinY(p1);
221
+ p1 = this._clampInsideWorld(p1); // <<< clamp dans la cage
222
+ this.entity.setPosition(p1);
223
+
224
+ var self = this;
225
+ this._onResize = function(){ self._checkAspectRatio(); };
226
+ window.addEventListener('resize', this._onResize, false);
227
+ this._checkAspectRatio();
228
+
229
+ console.log('[FREE-CAM:init] pos=', p1);
230
+ };
231
+
232
+ FreeCamera.prototype.update = function (dt) {
233
+ var t = this.inertiaFactor === 0 ? 1 : Math.min(dt / this.inertiaFactor, 1);
234
+ this._yaw = pc.math.lerp(this._yaw, this._targetYaw, t);
235
+ this._pitch = pc.math.lerp(this._pitch, this._targetPitch, t);
236
+ this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
237
+
238
+ var pos = this.entity.getPosition().clone();
239
+ pos = this._resolveCapsuleCollisions(pos, this.maxResolveIters);
240
+ this._clampBBoxMinY(pos);
241
+ pos = this._clampInsideWorld(pos); // <<< clamp dans la cage à chaque frame
242
+ this.entity.setPosition(pos);
243
+ };
244
+
245
+ FreeCamera.prototype._checkAspectRatio = function () {
246
+ var gd = this.app.graphicsDevice;
247
+ if (!gd) return;
248
+ this.entity.camera.horizontalFov = (gd.height > gd.width);
249
+ };
250
+
251
+ // ======================== Build colliders ===========================
252
+ FreeCamera.prototype._buildAabbsFromFocus = function () {
253
+ this._colliders = [];
254
+ this._worldAabb = null;
255
+ this._useCollision = false;
256
+ this._containMin = null;
257
+ this._containMax = null;
258
+
259
+ var root = this.focusEntity;
260
+ if (!root) {
261
+ console.warn('[FREE-CAM] focusEntity manquant — pas de collisions.');
262
+ return;
263
+ }
264
+
265
+ // Collecte des AABBs de mesh
266
+ var boxes = [];
267
+ var stack = [root];
268
+ while (stack.length) {
269
+ var e = stack.pop();
270
+ var rc = e.render;
271
+ if (rc && rc.meshInstances && rc.meshInstances.length) {
272
+ for (var i=0;i<rc.meshInstances.length;i++){
273
+ var bb = rc.meshInstances[i].aabb;
274
+ boxes.push(new pc.BoundingBox(bb.center.clone(), bb.halfExtents.clone()));
275
+ }
276
+ }
277
+ var ch = e.children;
278
+ if (ch && ch.length) for (var j=0;j<ch.length;j++) stack.push(ch[j]);
279
+ }
280
+ if (boxes.length === 0) {
281
+ console.warn('[FREE-CAM] Aucun meshInstance sous focusEntity — pas de collisions.');
282
+ return;
283
+ }
284
+
285
+ // Monde
286
+ var world = boxes[0].clone();
287
+ for (var k=1;k<boxes.length;k++) world.add(boxes[k]);
288
+
289
+ // Désactiver tout culling des quasi-globales : on garde tout
290
+ var filtered = boxes;
291
+
292
+ // Merge strict
293
+ var merged = this._mergeAabbs(filtered, Math.max(0, this.mergeGap||0));
294
+
295
+ // Petit gonflage optionnel
296
+ var inflate = Math.max(0, this.inflateBias||0);
297
+ for (var n=0;n<merged.length;n++){
298
+ merged[n].halfExtents.add(new pc.Vec3(inflate, inflate, inflate));
299
+ }
300
+
301
+ // Enregistrer
302
+ if (merged.length>0) {
303
+ for (var t=0;t<merged.length;t++) this._colliders.push({ aabb: merged[t] });
304
+ this._worldAabb = world;
305
+ this._useCollision = true;
306
+
307
+ // Cage monde : world min/max moins le rayon pour éviter le clipping
308
+ var R = Math.max(0, this.capsuleRadius || 0.3);
309
+ var wmin = world.getMin().clone().add(new pc.Vec3(R, R, R));
310
+ var wmax = world.getMax().clone().sub(new pc.Vec3(R, R, R));
311
+ this._containMin = wmin;
312
+ this._containMax = wmax;
313
+ }
314
+
315
+ console.log('[FREE-CAM] colliders=', this._colliders.length, '(raw=', boxes.length, ')');
316
+ if (this._worldAabb) {
317
+ console.log('[FREE-CAM] worldAabb min=', this._worldAabb.getMin(), 'max=', this._worldAabb.getMax());
318
+ console.log('[FREE-CAM] contain box min=', this._containMin, 'max=', this._containMax);
319
+ }
320
+ };
321
+
322
+ FreeCamera.prototype._mergeAabbs = function (boxes, gap) {
323
+ if (!boxes || boxes.length<=1) return boxes.slice();
324
+ var out = boxes.slice();
325
+ var tol = Math.max(0, gap||0);
326
+ var changed = true;
327
+
328
+ function overlap(a,b,t){
329
+ var amin=a.getMin(), amax=a.getMax();
330
+ var bmin=b.getMin(), bmax=b.getMax();
331
+ return !(
332
+ amax.x < bmin.x - t || amin.x > bmax.x + t ||
333
+ amax.y < bmin.y - t || amin.y > bmax.y + t ||
334
+ amax.z < bmin.z - t || amin.z > bmax.z + t
335
+ );
336
+ }
337
+
338
+ while (changed){
339
+ changed=false;
340
+ var next=[], used=new Array(out.length).fill(false);
341
+ for (var i=0;i<out.length;i++){
342
+ if (used[i]) continue;
343
+ var acc = out[i];
344
+ for (var j=i+1;j<out.length;j++){
345
+ if (used[j]) continue;
346
+ if (overlap(acc,out[j],tol)){
347
+ var aMin=acc.getMin(), aMax=acc.getMax();
348
+ var bMin=out[j].getMin(), bMax=out[j].getMax();
349
+ var nMin=new pc.Vec3(Math.min(aMin.x,bMin.x), Math.min(aMin.y,bMin.y), Math.min(aMin.z,bMin.z));
350
+ var nMax=new pc.Vec3(Math.max(aMax.x,bMax.x), Math.max(aMax.y,bMax.y), Math.max(aMax.z,bMax.z));
351
+ var c=nMin.clone().add(nMax).mulScalar(0.5);
352
+ var h=new pc.Vec3(Math.abs(nMax.x-c.x), Math.abs(nMax.y-c.y), Math.abs(nMax.z-c.z));
353
+ acc = new pc.BoundingBox(c,h);
354
+ used[j]=true; changed=true;
355
+ }
356
+ }
357
+ used[i]=true; next.push(acc);
358
+ }
359
+ out=next;
360
+ }
361
+ return out;
362
+ };
363
+
364
+ // ======================== BBox globale + minY ===========================
365
+ FreeCamera.prototype._bboxEnabled = function () {
366
+ return (this.Xmin < this.Xmax) && (this.Ymin < this.Ymax) && (this.Zmin < this.Zmax);
367
+ };
368
+ FreeCamera.prototype._clampBBoxMinY = function (p) {
369
+ if (p.y < this.minY) p.y = this.minY;
370
+ if (!this._bboxEnabled()) return;
371
+ p.x = clamp(p.x, this.Xmin, this.Xmax);
372
+ p.y = clamp(p.y, Math.max(this.Ymin, this.minY), this.Ymax);
373
+ p.z = clamp(p.z, this.Zmin, this.Zmax);
374
+ };
375
+
376
+ // >>> Cage monde : clamp interne <<<
377
+ FreeCamera.prototype._clampInsideWorld = function(p){
378
+ if (!this._containMin || !this._containMax) return p;
379
+ p.x = Math.max(this._containMin.x, Math.min(this._containMax.x, p.x));
380
+ p.y = Math.max(Math.max(this._containMin.y, this.minY), Math.min(this._containMax.y, p.y));
381
+ p.z = Math.max(this._containMin.z, Math.min(this._containMax.z, p.z));
382
+ return p;
383
+ };
384
+ FreeCamera.prototype._isOutsideWorld = function(p){
385
+ if (!this._containMin || !this._containMax) return false;
386
+ return (p.x < this._containMin.x || p.x > this._containMax.x ||
387
+ p.y < Math.max(this._containMin.y, this.minY) || p.y > this._containMax.y ||
388
+ p.z < this._containMin.z || p.z > this._containMax.z);
389
+ };
390
+
391
+ // ======================== Capsule vs AABBs : resolve ===========================
392
+ FreeCamera.prototype._resolveCapsuleCollisions = function (pos, maxIters) {
393
+ if (!this._useCollision) return pos.clone();
394
+
395
+ var p = pos.clone();
396
+ var R = Math.max(0, this.capsuleRadius);
397
+ var H = Math.max(2*R, this.capsuleHeight);
398
+ var eps = Math.max(1e-7, this.collisionEps);
399
+
400
+ if (!this._colliders || this._colliders.length===0) return p;
401
+
402
+ var iters = Math.max(1, maxIters||1);
403
+ for (var iter=0; iter<iters; iter++){
404
+ var moved = false;
405
+
406
+ for (var i=0;i<this._colliders.length;i++){
407
+ var aabb = this._colliders[i].aabb;
408
+ var push = new pc.Vec3();
409
+ var pen = capsuleVsAabbPenetration(p, H, R, aabb, push);
410
+ if (pen > eps) {
411
+ p.add(push);
412
+ moved = true;
413
+ }
414
+ }
415
+ if (!moved) break;
416
+ }
417
+ return p;
418
+ };
419
+
420
+ // ======================== Mvt principal : swept + step + snap ===========================
421
+ FreeCamera.prototype._moveSwept = function (from, delta) {
422
+ if (!this._useCollision) return vAdd(from, delta);
423
+
424
+ var maxStep = Math.max(0.01, this.maxStepDistance||0.1);
425
+ var dist = vLen(delta);
426
+ if (dist <= maxStep) return this._moveStep(from, delta);
427
+
428
+ var steps = Math.ceil(dist / maxStep);
429
+ var step = vScale(delta, 1/steps);
430
+ var cur = from.clone();
431
+ for (var i=0;i<steps;i++) cur = this._moveStep(cur, step);
432
+ return cur;
433
+ };
434
+
435
+ FreeCamera.prototype._moveStep = function (from, delta) {
436
+ var target = vAdd(from, delta);
437
+ var after = this._resolveCapsuleCollisions(target, this.maxResolveIters);
438
+ var collided = (vLen(vSub(after, target)) > 0);
439
+
440
+ if (!collided) {
441
+ if (this.enableGroundSnap) after = this._snapDown(after);
442
+ after = this._clampInsideWorld(after); // <<< clamp
443
+ return after;
444
+ }
445
+
446
+ var n = this._estimateNormal(after);
447
+ if (n) {
448
+ var desire = delta.clone();
449
+ var slide = vSub(desire, vScale(n, vDot(desire, n)));
450
+ var slideTarget = vAdd(from, slide);
451
+ var slideAfter = this._resolveCapsuleCollisions(slideTarget, this.maxResolveIters);
452
+
453
+ // Step-up si slide insuffisant
454
+ if (vLen(vSub(slideAfter, slideTarget)) > 0) {
455
+ var stepped = this._tryStepUp(from, desire);
456
+ if (stepped) {
457
+ if (this.enableGroundSnap) stepped = this._snapDown(stepped);
458
+ stepped = this._clampInsideWorld(stepped); // <<< clamp
459
+ return stepped;
460
+ }
461
+ }
462
+
463
+ if (this.enableGroundSnap) slideAfter = this._snapDown(slideAfter);
464
+ slideAfter = this._clampInsideWorld(slideAfter); // <<< clamp
465
+ return slideAfter;
466
+ }
467
+
468
+ if (this.enableGroundSnap) after = this._snapDown(after);
469
+ after = this._clampInsideWorld(after); // <<< clamp
470
+ return after;
471
+ };
472
+
473
+ // Normale approx via micro-probes
474
+ FreeCamera.prototype._estimateNormal = function (p) {
475
+ if (!this._colliders) return null;
476
+ var probe = 0.02;
477
+ var base = this._resolveCapsuleCollisions(p, this.maxResolveIters);
478
+ var nx = this._resolveCapsuleCollisions(new pc.Vec3(p.x+probe,p.y,p.z), this.maxResolveIters);
479
+ var px = this._resolveCapsuleCollisions(new pc.Vec3(p.x-probe,p.y,p.z), this.maxResolveIters);
480
+ var ny = this._resolveCapsuleCollisions(new pc.Vec3(p.x,p.y+probe,p.z), this.maxResolveIters);
481
+ var py = this._resolveCapsuleCollisions(new pc.Vec3(p.x,p.y-probe,p.z), this.maxResolveIters);
482
+ var nz = this._resolveCapsuleCollisions(new pc.Vec3(p.x,p.y,p.z+probe), this.maxResolveIters);
483
+ var pz = this._resolveCapsuleCollisions(new pc.Vec3(p.x,p.y,p.z-probe), this.maxResolveIters);
484
+
485
+ function d(a,b){ return vLen(vSub(a,b)); }
486
+ var dx = d(nx,base) - d(px,base);
487
+ var dy = d(ny,base) - d(py,base);
488
+ var dz = d(nz,base) - d(pz,base);
489
+ var n = new pc.Vec3(dx,dy,dz);
490
+ var L = vLen(n);
491
+ if (L < 1e-5) return null;
492
+ return vScale(n, 1/L);
493
+ };
494
+
495
+ // Step-up : monter une marche (jusqu'à stepHeight)
496
+ FreeCamera.prototype._tryStepUp = function (from, wishDelta) {
497
+ var R = Math.max(0, this.capsuleRadius);
498
+ var H = Math.max(2*R, this.capsuleHeight);
499
+ var maxH = Math.max(0, this.stepHeight || 0.35);
500
+ var ahead = Math.max(0.05, this.stepAhead || 0.20);
501
+
502
+ var horiz = new pc.Vec3(wishDelta.x, 0, wishDelta.z);
503
+ var hLen = vLen(horiz);
504
+ if (hLen < 1e-6) return null;
505
+ horiz = vScale(horiz, 1/hLen);
506
+
507
+ var probe = vAdd(from, vScale(horiz, Math.min(hLen, ahead)));
508
+ var trials = 3;
509
+ for (var i=1;i<=trials;i++){
510
+ var up = maxH * (i/trials);
511
+ var raised = new pc.Vec3(probe.x, probe.y + up, probe.z);
512
+ raised = this._resolveCapsuleCollisions(raised, this.maxResolveIters);
513
+ var stepped = this._resolveCapsuleCollisions(vAdd(raised, wishDelta), this.maxResolveIters);
514
+
515
+ if (vLen(vSub(stepped, raised)) > 0.02) return stepped;
516
+ }
517
+ return null;
518
+ };
519
+
520
+ // Snap-down : redéposer sur le sol si on flotte un peu (descente d'escalier)
521
+ FreeCamera.prototype._snapDown = function (p) {
522
+ var R = Math.max(0, this.capsuleRadius);
523
+ var H = Math.max(2*R, this.capsuleHeight);
524
+ var maxDown = Math.max(0, this.snapDownMax || 0.6);
525
+
526
+ var steps = 4;
527
+ var step = maxDown / steps;
528
+ var cur = p.clone();
529
+ for (var i=0;i<steps;i++){
530
+ var down = new pc.Vec3(cur.x, cur.y - step, cur.z);
531
+ var resolved = this._resolveCapsuleCollisions(down, this.maxResolveIters);
532
+ if (vLen(vSub(resolved, down)) > 0) {
533
+ return this._resolveCapsuleCollisions(new pc.Vec3(resolved.x, resolved.y + 0.01, resolved.z), this.maxResolveIters);
534
+ }
535
+ cur.copy(down);
536
+ }
537
+ return p;
538
+ };
539
+
540
+ // ===================== INPUT SOURIS =====================
541
+ var FreeCameraInputMouse = pc.createScript('orbitCameraInputMouse');
542
+ FreeCameraInputMouse.attributes.add('lookSensitivity', { type: 'number', default: 0.28, title: 'Look Sensitivity' });
543
+ FreeCameraInputMouse.attributes.add('wheelSensitivity',{ type: 'number', default: 0.8, title: 'Wheel Sensitivity (anti-tunnel)' });
544
+
545
+ FreeCameraInputMouse.prototype.initialize = function () {
546
+ this.freeCam = this.entity.script.orbitCamera;
547
+ this.last = new pc.Vec2();
548
+ this.isLooking = false;
549
+
550
+ if (this.app.mouse) {
551
+ this.app.mouse.on(pc.EVENT_MOUSEDOWN, this.onMouseDown, this);
552
+ this.app.mouse.on(pc.EVENT_MOUSEUP, this.onMouseUp, this);
553
+ this.app.mouse.on(pc.EVENT_MOUSEMOVE, this.onMouseMove, this);
554
+ this.app.mouse.on(pc.EVENT_MOUSEWHEEL, this.onMouseWheel, this);
555
+ this.app.mouse.disableContextMenu();
556
+ }
557
+ var self = this;
558
+ this._onOut = function(){ self.isLooking = false; };
559
+ window.addEventListener('mouseout', this._onOut, false);
560
+
561
+ this.on('destroy', () => {
562
+ if (this.app.mouse) {
563
+ this.app.mouse.off(pc.EVENT_MOUSEDOWN, this.onMouseDown, this);
564
+ this.app.mouse.off(pc.EVENT_MOUSEUP, this.onMouseUp, this);
565
+ this.app.mouse.off(pc.EVENT_MOUSEMOVE, this.onMouseMove, this);
566
+ this.app.mouse.off(pc.EVENT_MOUSEWHEEL, this.onMouseWheel, this);
567
+ }
568
+ window.removeEventListener('mouseout', this._onOut, false);
569
+ });
570
+ };
571
+
572
+ FreeCameraInputMouse.prototype.onMouseDown = function (e) {
573
+ this.isLooking = true;
574
+ this.last.set(e.x, e.y);
575
+ };
576
+
577
+ FreeCameraInputMouse.prototype.onMouseUp = function () {
578
+ this.isLooking = false;
579
+ };
580
+
581
+ FreeCameraInputMouse.prototype.onMouseMove = function (e) {
582
+ if (!this.isLooking || !this.freeCam) return;
583
+ var sens = this.lookSensitivity;
584
+ this.freeCam.yaw = this.freeCam.yaw - e.dx * sens;
585
+ this.freeCam.pitch = this.freeCam.pitch - e.dy * sens;
586
+ this.last.set(e.x, e.y);
587
+ };
588
+
589
+ FreeCameraInputMouse.prototype.onMouseWheel = function (e) {
590
+ if (!this.freeCam) return;
591
+ var cam = this.entity;
592
+ var move = -e.wheelDelta * this.wheelSensitivity * this.freeCam.dollySpeed * 0.05;
593
+ var fwd = cam.forward.clone(); if (fwd.lengthSq()>1e-8) fwd.normalize();
594
+ var from = cam.getPosition().clone();
595
+ var to = from.clone().add(fwd.mulScalar(move));
596
+ var next = this.freeCam._moveSwept(from, to.sub(from));
597
+ this.freeCam._clampBBoxMinY(next);
598
+ next = this.freeCam._clampInsideWorld(next);
599
+ cam.setPosition(next);
600
+ e.event.preventDefault();
601
+ };
602
+
603
+ // ===================== INPUT TOUCH =====================
604
+ var FreeCameraInputTouch = pc.createScript('orbitCameraInputTouch');
605
+ FreeCameraInputTouch.attributes.add('lookSensitivity', { type: 'number', default: 0.5, title: 'Look Sensitivity' });
606
+ FreeCameraInputTouch.attributes.add('pinchDollyFactor', { type: 'number', default: 0.02, title: 'Pinch Dolly Factor' });
607
+
608
+ FreeCameraInputTouch.prototype.initialize = function () {
609
+ this.freeCam = this.entity.script.orbitCamera;
610
+ this.last = new pc.Vec2();
611
+ this.isLooking = false;
612
+ this.lastPinch = 0;
613
+
614
+ if (this.app.touch) {
615
+ this.app.touch.on(pc.EVENT_TOUCHSTART, this.onTouchStartEndCancel, this);
616
+ this.app.touch.on(pc.EVENT_TOUCHEND, this.onTouchStartEndCancel, this);
617
+ this.app.touch.on(pc.EVENT_TOUCHCANCEL, this.onTouchStartEndCancel, this);
618
+ this.app.touch.on(pc.EVENT_TOUCHMOVE, this.onTouchMove, this);
619
+ }
620
+
621
+ this.on('destroy', () => {
622
+ if (this.app.touch) {
623
+ this.app.touch.off(pc.EVENT_TOUCHSTART, this.onTouchStartEndCancel, this);
624
+ this.app.touch.off(pc.EVENT_TOUCHEND, this.onTouchStartEndCancel, this);
625
+ this.app.touch.off(pc.EVENT_TOUCHCANCEL, this.onTouchStartEndCancel, this);
626
+ this.app.touch.off(pc.EVENT_TOUCHMOVE, this.onTouchMove, this);
627
+ }
628
+ });
629
+ };
630
+
631
+ FreeCameraInputTouch.prototype.onTouchStartEndCancel = function (e) {
632
+ var t = e.touches;
633
+ if (t.length === 1) {
634
+ this.isLooking = (e.event.type === 'touchstart');
635
+ this.last.set(t[0].x, t[0].y);
636
+ } else if (t.length === 2) {
637
+ var dx = t[0].x - t[1].x, dy = t[0].y - t[1].y;
638
+ this.lastPinch = Math.sqrt(dx*dx + dy*dy);
639
+ } else {
640
+ this.isLooking = false;
641
+ }
642
+ };
643
+
644
+ FreeCameraInputTouch.prototype.onTouchMove = function (e) {
645
+ var t = e.touches;
646
+ if (!this.freeCam) return;
647
+
648
+ if (t.length === 1 && this.isLooking) {
649
+ var s = this.lookSensitivity;
650
+ var dx = t[0].x - this.last.x, dy = t[0].y - this.last.y;
651
+ this.freeCam.yaw = this.freeCam.yaw - dx * s;
652
+ this.freeCam.pitch = this.freeCam.pitch - dy * s;
653
+ this.last.set(t[0].x, t[0].y);
654
+ } else if (t.length === 2) {
655
+ var dx = t[0].x - t[1].x, dy = t[0].y - t[1].y;
656
+ var dist = Math.sqrt(dx*dx + dy*dy);
657
+ var delta = dist - this.lastPinch; this.lastPinch = dist;
658
+
659
+ var cam = this.entity;
660
+ var fwd = cam.forward.clone(); if (fwd.lengthSq()>1e-8) fwd.normalize();
661
+ var from = cam.getPosition().clone();
662
+ var to = from.clone().add(fwd.mulScalar(delta * this.pinchDollyFactor * this.freeCam.dollySpeed));
663
+ var next = this.freeCam._moveSwept(from, to.sub(from));
664
+ this.freeCam._clampBBoxMinY(next);
665
+ next = this.freeCam._clampInsideWorld(next);
666
+ cam.setPosition(next);
667
+ }
668
+ };
669
+
670
+ // ===================== INPUT CLAVIER =====================
671
+ var FreeCameraInputKeyboard = pc.createScript('orbitCameraInputKeyboard');
672
+ FreeCameraInputKeyboard.attributes.add('acceleration', { type: 'number', default: 1.0, title: 'Accel (unused, future)' });
673
+
674
+ FreeCameraInputKeyboard.prototype.initialize = function () {
675
+ this.freeCam = this.entity.script.orbitCamera;
676
+ this.kb = this.app.keyboard || null;
677
+ };
678
+
679
+ FreeCameraInputKeyboard.prototype.update = function (dt) {
680
+ if (!this.freeCam || !this.kb) return;
681
+
682
+ var fwd = (this.kb.isPressed(pc.KEY_UP) || this.kb.isPressed(pc.KEY_Z) || this.kb.isPressed(pc.KEY_W)) ? 1 :
683
+ (this.kb.isPressed(pc.KEY_DOWN) || this.kb.isPressed(pc.KEY_S)) ? -1 : 0;
684
+
685
+ var strf = (this.kb.isPressed(pc.KEY_RIGHT) || this.kb.isPressed(pc.KEY_D)) ? 1 :
686
+ (this.kb.isPressed(pc.KEY_LEFT) || this.kb.isPressed(pc.KEY_Q) || this.kb.isPressed(pc.KEY_A)) ? -1 : 0;
687
+
688
+ if (fwd !== 0 || strf !== 0) {
689
+ var cam = this.entity;
690
+ var from = cam.getPosition().clone();
691
+
692
+ var forward = cam.forward.clone(); if (forward.lengthSq()>1e-8) forward.normalize();
693
+ var right = cam.right.clone(); if (right.lengthSq()>1e-8) right.normalize();
694
+
695
+ var delta = new pc.Vec3()
696
+ .add(forward.mulScalar(fwd * this.freeCam.moveSpeed * dt))
697
+ .add(right .mulScalar(strf * this.freeCam.strafeSpeed * dt));
698
+
699
+ var next = this.freeCam._moveSwept(from, delta);
700
+ this.freeCam._clampBBoxMinY(next);
701
+ next = this.freeCam._clampInsideWorld(next);
702
+ cam.setPosition(next);
703
+ }
704
+
705
+ var shift = this.kb.isPressed(pc.KEY_SHIFT);
706
+ if (shift) {
707
+ var yawDir = (this.kb.isPressed(pc.KEY_LEFT) ? 1 : 0) - (this.kb.isPressed(pc.KEY_RIGHT) ? 1 : 0);
708
+ var pitchDir = (this.kb.isPressed(pc.KEY_UP) ? 1 : 0) - (this.kb.isPressed(pc.KEY_DOWN) ? 1 : 0);
709
+ var yawSpeed = 120, pitchSpeed = 90;
710
+ if (yawDir !== 0) this.freeCam.yaw = this.freeCam.yaw + yawDir * yawSpeed * dt;
711
+ if (pitchDir !== 0) this.freeCam.pitch = this.freeCam.pitch + pitchDir * pitchSpeed * dt;
712
+ }
713
+ };
714
  }
715
 
716
  /* -------------------------------------------
 
750
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
751
  const isMobile = isIOS || /Android/i.test(navigator.userAgent);
752
 
753
+ console.log(`[VIEWER] A: initializeViewer begin`, { instanceId });
754
+
755
  // --- Configuration ---
 
756
  sogUrl = config.sog_url || config.sogs_json_url;
757
+ glbUrl = config.glb_url ?? "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/ressources/espace_expo/sol_blanc_2.glb";
758
+ presentoirUrl = config.presentoir_url ?? "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/ressources/espace_expo/sol_blanc_2.glb";
 
 
 
 
 
 
 
 
759
 
760
  minZoom = parseFloat(config.minZoom || "1");
761
  maxZoom = parseFloat(config.maxZoom || "20");
 
829
  canvas.addEventListener("dblclick", (e) => e.preventDefault());
830
  canvas.addEventListener(
831
  "touchstart",
832
+ (e) => { if (e.touches.length > 1) e.preventDefault(); },
 
 
833
  { passive: false }
834
  );
835
  canvas.addEventListener(
836
  "wheel",
837
+ (e) => { e.preventDefault(); },
 
 
838
  { passive: false }
839
  );
840
 
841
  // Bloque le scroll page uniquement quand le pointeur est sur le canvas
842
  const scrollKeys = new Set([
843
+ "ArrowUp","ArrowDown","ArrowLeft","ArrowRight",
844
+ "PageUp","PageDown","Home","End"," ","Space","Spacebar"
 
 
 
 
 
 
 
 
 
845
  ]);
846
  let isPointerOverCanvas = false;
847
  const focusCanvas = () => canvas.focus({ preventScroll: true });
848
 
849
+ const onPointerEnter = () => { isPointerOverCanvas = true; focusCanvas(); };
850
+ const onPointerLeave = () => { isPointerOverCanvas = false; if (document.activeElement === canvas) canvas.blur(); };
851
+ const onCanvasBlur = () => { isPointerOverCanvas = false; };
 
 
 
 
 
 
 
 
852
 
853
  canvas.addEventListener("pointerenter", onPointerEnter);
854
  canvas.addEventListener("pointerleave", onPointerLeave);
855
  canvas.addEventListener("mouseenter", onPointerEnter);
856
  canvas.addEventListener("mouseleave", onPointerLeave);
857
  canvas.addEventListener("mousedown", focusCanvas);
858
+ canvas.addEventListener("touchstart", () => { focusCanvas(); }, { passive: false });
 
 
 
 
 
 
859
  canvas.addEventListener("blur", onCanvasBlur);
860
 
861
  const onKeyDownCapture = (e) => {
862
  if (!isPointerOverCanvas) return;
863
+ if (scrollKeys.has(e.key) || scrollKeys.has(e.code)) e.preventDefault();
 
 
864
  };
865
  window.addEventListener("keydown", onKeyDownCapture, true);
866
 
 
869
  // --- Charge PlayCanvas lib ESM (une par module/instance) ---
870
  if (!pc) {
871
  pc = await import("https://esm.run/playcanvas");
872
+ window.pc = pc; // debug
873
  }
874
+ console.log('[VIEWER] PlayCanvas ESM chargé:', !!pc);
875
 
876
  // --- Crée l'Application ---
877
  const device = await pc.createGraphicsDevice(canvas, {
 
880
  twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
881
  antialias: false
882
  });
 
 
883
  device.maxPixelRatio = Math.min(window.devicePixelRatio || 1, maxDevicePixelRatio);
884
 
885
  const opts = new pc.AppOptions();
 
920
  }, 60);
921
  });
922
 
 
923
  app.on("destroy", () => {
924
+ try { resizeObserver.disconnect(); } catch {}
 
 
925
  if (opts.keyboard && opts.keyboard.detach) opts.keyboard.detach();
926
  window.removeEventListener("keydown", onKeyDownCapture, true);
927
 
 
940
  app.assets.add(sogAsset);
941
  app.assets.add(glbAsset);
942
 
943
+ // >>> Scripts caméra en local (aucune requête réseau) <<<
944
+ registerFreeCamScripts();
945
 
946
  // ---------- CHARGEMENT SOG + GLB AVANT CREATION CAMERA ----------
947
  await new Promise((resolve, reject) => {
948
  const loader = new pc.AssetListLoader([sogAsset, glbAsset], app.assets);
949
  loader.load(() => resolve());
950
+ loader.on('error', (e)=>{ console.error('[VIEWER] Asset load error:', e); reject(e); });
951
  });
952
 
953
  app.start(); // démarre l'update loop dès que possible
954
  progressDialog.style.display = "none";
955
+ console.log("[VIEWER] app.start OK — assets chargés");
956
 
957
  // --- Modèle principal (GSplat via .sog) ---
958
  modelEntity = new pc.Entity("model");
 
967
  if (envEntity) {
968
  envEntity.name = "ENV_GLTF";
969
  app.root.addChild(envEntity);
970
+ // Log du nombre de meshInstances pour vérifier la construction des colliders
971
+ let meshCount = 0;
972
+ traverse(envEntity, (node) => {
973
+ if (node.render && node.render.meshInstances) meshCount += node.render.meshInstances.length;
974
+ });
975
+ console.log("[VIEWER] env ready:", true, "meshInstances=", meshCount);
976
  } else {
977
+ console.warn("[VIEWER] GLB resource missing: collisions fallback sur GSplat (moins précis).");
978
  }
979
 
980
  // --- Caméra + scripts d’input (free-cam nommée 'orbitCamera' pour compat) ---
981
  cameraEntity = new pc.Entity("camera");
982
  cameraEntity.addComponent("camera", {
983
  clearColor: new pc.Color(color_bg),
984
+ nearClip: 0.02,
985
+ farClip: 250
986
  });
987
  cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
988
  cameraEntity.lookAt(modelEntity.getPosition());
989
  cameraEntity.addComponent("script");
990
 
 
991
  cameraEntity.script.create("orbitCamera", {
992
  attributes: {
993
  focusEntity: envEntity || modelEntity,
994
+ inertiaFactor: 0.18,
 
995
  distanceMax: maxZoom,
996
  distanceMin: minZoom,
997
  pitchAngleMax: maxAngle,
 
999
  yawAngleMax: maxAzimuth,
1000
  yawAngleMin: minAzimuth,
1001
  minY: minY,
1002
+ frameOnStart: false,
1003
+ capsuleRadius: 0.30,
1004
+ capsuleHeight: 1.60,
1005
+ maxStepDistance: 0.10,
1006
+ maxResolveIters: 8,
1007
+ globalCullFrac: 0.0 // désactivé
1008
  }
1009
  });
1010
+ cameraEntity.script.create("orbitCameraInputMouse", {
1011
+ attributes: { lookSensitivity: 0.28, wheelSensitivity: 0.8 }
1012
+ });
1013
+ cameraEntity.script.create("orbitCameraInputTouch", {
1014
+ attributes: { lookSensitivity: 0.5, pinchDollyFactor: 0.02 }
1015
+ });
1016
  cameraEntity.script.create("orbitCameraInputKeyboard", {
1017
+ attributes: { acceleration: 1.0 }
 
 
 
1018
  });
1019
  app.root.addChild(cameraEntity);
1020
 
 
1046
  canvas.addEventListener(ev, bumpInteraction, { passive: true });
1047
  });
1048
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1049
  viewerInitialized = true;
1050
+ console.log("[VIEWER] READY — physics=OFF (AABB), env=", !!envEntity, "sog=", !!modelEntity);
1051
  }
1052
 
1053
  /* -------------------------------------------
 
1062
 
1063
  const modelPos = modelEntity.getPosition();
1064
 
1065
+ // Ici c'est une "free-cam", on ne remet pas une orbite : juste réaligner le regard
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1066
  cameraEntity.lookAt(modelPos);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1067
  } catch (e) {
1068
  console.error("[viewer.js] resetViewerCamera error:", e);
1069
  }