MikaFil commited on
Commit
2d08221
·
verified ·
1 Parent(s): e8fe9df

Update viewer_ar_ios.js

Browse files
Files changed (1) hide show
  1. viewer_ar_ios.js +94 -104
viewer_ar_ios.js CHANGED
@@ -1,14 +1,15 @@
1
- /* viewer_ar_ios.js — AR PlayCanvas + GLB/USDZ via config.json (+ "sol" + mode mur : Z = normale, Y = haut)
2
  - config.json : { "glb_url": "...", "usdz_url": "...", "sol": true|false }
3
- - sol = true → plans horizontaux (sol) + rotation autour de l’axe local Y (haut)
4
- - sol = false → plans verticaux (mur) :
5
- * Z local = normale du mur (perpendiculaire au plan)
6
- * Y local = direction plafond (Up monde) projetée dans le plan
7
- * rotation utilisateur = autour de Z local (normale)
8
- * PENDANT une translation : orientation verrouillée (pas de twist parasite)
9
- * À la fin d’une translation : réalignement base sur le NOUVEAU mur (Z=normale, Y→plafond)
10
- - iOS : AR Quick Look (USDZ)
11
- - Android/Desktop : WebXR AR + bouton "Lancer l’AR" + slider de rotation
 
12
  */
13
 
14
  (function () {
@@ -298,15 +299,9 @@
298
  ensureQuickLookButton(USDZ_URL);
299
 
300
  if (PLACE_ON_FLOOR) {
301
- message(
302
- "Modèle chargé. Appuyez sur « Lancer l’AR ». Dans la vue AR (Quick Look), " +
303
- "placez l’objet sur le sol et ajustez sa position/orientation avec les gestes iOS."
304
- );
305
  } else {
306
- message(
307
- "Modèle chargé. Appuyez sur « Lancer l’AR ». Sur iOS (Quick Look), l’objet ne peut pas être " +
308
- "aligné automatiquement au mur : placez-le et orientez-le manuellement (Y vers le plafond)."
309
- );
310
  }
311
  } else {
312
  message("iOS détecté, mais aucun 'usdz_url' dans config.json.");
@@ -390,25 +385,14 @@
390
  app.root.addChild(modelRoot);
391
  var modelLoaded = false, placedOnce = false;
392
 
393
- // --- Helpers vecteurs/quaternions stables ---
394
  var UP = new pc.Vec3(0, 1, 0);
 
395
  function projOnPlane(v, n) {
396
  var d = v.dot(n);
397
  return new pc.Vec3(v.x - d*n.x, v.y - d*n.y, v.z - d*n.z);
398
  }
399
- function buildWallBasisFromNormalZ(Nz) {
400
- // Z local = normale du mur
401
- var Z = Nz.clone().normalize();
402
- // Y local = Up projété dans le plan (toujours "vers le plafond" le long du mur)
403
- var Y = projOnPlane(UP, Z);
404
- if (Y.lengthSq() < 1e-8) Y = projOnPlane(new pc.Vec3(0,1,0), Z);
405
- Y.normalize();
406
- // X = Y × Z (garantit base orthonormée et droite, avec X×Y = Z)
407
- var X = new pc.Vec3(); X.cross(Y, Z).normalize();
408
- // Re-orthonormalise Y = Z × X pour éliminer la dérive
409
- Y.cross(Z, X).normalize();
410
- return { X: X, Y: Y, Z: Z };
411
- }
412
  function quatFromBasis(X, Y, Z) {
413
  // colonnes = X,Y,Z
414
  var m00 = X.x, m01 = Y.x, m02 = Z.x;
@@ -445,19 +429,43 @@
445
  q.normalize();
446
  return q;
447
  }
448
- function computeWallBaseFromRot_ZNormal(rot) {
449
- // WebXR/PlayCanvas : la normale du plan est l’axe Z de la pose
450
- var Nz = new pc.Vec3();
451
- rot.transformVector(new pc.Vec3(0, 0, 1), Nz); // Z monde de la pose
452
- Nz.normalize();
453
- var basis = buildWallBasisFromNormalZ(Nz); // Z=normale, Y=Up projeté
454
- return quatFromBasis(basis.X, basis.Y, basis.Z);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
455
  }
456
 
457
  // Mode mur / sol
458
  var wallMode = !PLACE_ON_FLOOR;
459
- var wallBaseRot = new pc.Quat(); // base (mur) : Z=normale, Y=up-projeté
460
- var wallAngleDeg = 0; // rotation utilisateur autour de Z local (normale)
461
 
462
  // Ombre blob (sol uniquement)
463
  var blob = null;
@@ -514,9 +522,7 @@
514
  return e;
515
  }
516
 
517
- // Rotation via slider (sol: Euler Y, mur: twist autour de Z local)
518
- var baseEulerX = 0, baseEulerZ = 0;
519
- var rotationYDeg = 0;
520
  function clamp360(d) { return Math.max(0, Math.min(360, d)); }
521
  function updateKnobFromY(yDeg) {
522
  var t = 1 - (yDeg / 360);
@@ -524,24 +530,16 @@
524
  rotInput.value = String(Math.round(yDeg));
525
  rotVal.textContent = String(Math.round(yDeg)) + "°";
526
  }
527
- function getCurrentAngle() { return wallMode ? wallAngleDeg : rotationYDeg; }
528
 
529
- function applyRotationY(deg) {
530
  var y = clamp360(deg);
 
531
  if (!modelRoot.enabled) { updateKnobFromY(y); return; }
532
-
533
- if (!wallMode) {
534
- rotationYDeg = y;
535
- modelRoot.setEulerAngles(baseEulerX, y, baseEulerZ);
536
- } else {
537
- // mur : rotation autour de Z local
538
- wallAngleDeg = y;
539
- var qLocal = new pc.Quat();
540
- qLocal.setFromAxisAngle(new pc.Vec3(0, 0, 1), y); // rotation locale autour de Z
541
- var qFinal = new pc.Quat();
542
- qFinal.mul2(wallBaseRot, qLocal); // base (Z=normale, Y=up projeté) puis twist utilisateur
543
- modelRoot.setRotation(qFinal);
544
- }
545
  updateKnobFromY(y);
546
  }
547
 
@@ -558,6 +556,7 @@
558
  modelRoot.addChild(instance);
559
  modelRoot.setLocalScale(1, 1, 1);
560
 
 
561
  var renders = instance.findComponents('render');
562
  for (var ri = 0; ri < renders.length; ri++) {
563
  var r = renders[ri];
@@ -573,30 +572,29 @@
573
  }
574
  }
575
 
576
- var initE = modelRoot.getEulerAngles();
577
- baseEulerX = initE.x; baseEulerZ = initE.z;
578
-
579
  modelLoaded = true;
580
  message("Modèle chargé. Appuyez sur « Lancer l’AR ».");
581
  });
582
 
583
  if (!app.xr.supported) { message("WebXR n’est pas supporté sur cet appareil."); return; }
584
 
585
- // ----- Détection orientation des plans (utilise Z comme normale) -----
586
- var TMP_Z = new pc.Vec3(0, 0, 1), TMP_OUT = new pc.Vec3();
587
 
 
588
  function isHorizontalUpFacing(rot, minDot) {
589
- // sol : normale (Z) ≈ +Y
590
  minDot = (typeof minDot === "number") ? minDot : 0.75;
591
- rot.transformVector(TMP_Z, TMP_OUT);
592
  return TMP_OUT.y >= minDot;
593
  }
 
 
594
  function isVerticalFacing(rot, maxAbsY) {
595
- // mur : normale (Z) quasi horizontale → |Z.y| petit
596
  maxAbsY = (typeof maxAbsY === "number") ? maxAbsY : 0.35;
597
- rot.transformVector(TMP_Z, TMP_OUT);
598
  return Math.abs(TMP_OUT.y) <= maxAbsY;
599
  }
 
600
  function planeMatchesMode(rot) {
601
  return wallMode ? isVerticalFacing(rot) : isHorizontalUpFacing(rot);
602
  }
@@ -626,12 +624,12 @@
626
  draggingTrack = true;
627
  activePointerId = (e.pointerId != null) ? e.pointerId : 1;
628
  if (rotTrack.setPointerCapture) { try { rotTrack.setPointerCapture(activePointerId); } catch (er) {} }
629
- applyRotationY(degFromPointer(e));
630
  e.preventDefault(); e.stopPropagation();
631
  }
632
  function onPointerMoveCapture(e) {
633
  if (!draggingTrack || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
634
- applyRotationY(degFromPointer(e));
635
  e.preventDefault(); e.stopPropagation();
636
  }
637
  function endDrag(e) {
@@ -686,23 +684,23 @@
686
 
687
  if (!wallMode) {
688
  // --- MODE SOL ---
 
 
 
 
 
689
  blob = createBlobShadowAt(pos, rot);
690
- var e = new pc.Vec3();
691
- rot.getEulerAngles(e);
692
- var y0 = ((e.y % 360) + 360) % 360;
693
- applyRotationY(y0);
694
  } else {
695
  // --- MODE MUR (première pose) ---
696
- wallBaseRot.copy(computeWallBaseFromRot_ZNormal(rot)); // Z=normale, Y=up-projeté
697
- modelRoot.setRotation(wallBaseRot);
698
- applyRotationY(0); // angle initial = 0 (autour de Z)
699
  }
700
 
701
  placedOnce = true;
702
  rotInput.disabled = false;
703
  message(
704
  wallMode
705
- ? "Objet placé contre le mur (Z=normale, Y→plafond). Glissez pour déplacer, tournez avec le slider →"
706
  : "Objet placé. Glissez pour déplacer, tournez-le avec le slider →"
707
  );
708
  }
@@ -712,16 +710,15 @@
712
  });
713
 
714
  // Drag XR continu (déplacements suivants, sans rotation parasite)
715
- var isDragging = false;
716
- var dragLockedRot = new pc.Quat();
717
- var lastHitRot = new pc.Quat();
718
 
719
  app.xr.input.on("add", function (inputSource) {
720
  inputSource.on("selectstart", function () {
721
  if (uiInteracting) return;
722
  if (!placedOnce || !modelLoaded) return;
723
 
724
- // Verrouille l'orientation courante pendant la translation
725
  dragLockedRot.copy(modelRoot.getRotation());
726
 
727
  inputSource.hitTestStart({
@@ -734,26 +731,22 @@
734
  if (!isDragging) return;
735
  if (!planeMatchesMode(rot)) return;
736
 
737
- lastHitRot.copy(rot); // garde trace du dernier mur rencontré
738
- modelRoot.setPosition(pos); // translate uniquement
 
739
 
740
- if (!wallMode) {
741
- updateBlobPositionUnder(pos, rot);
742
- } else {
743
- modelRoot.setRotation(dragLockedRot); // aucune rotation pendant le drag
744
- }
745
  });
746
 
747
- // Fin du drag : snap orientation sur le NOUVEAU mur, en conservant l'angle utilisateur
748
  transientSource.once("remove", function () {
749
  isDragging = false;
750
  if (wallMode) {
751
- wallBaseRot.copy(computeWallBaseFromRot_ZNormal(lastHitRot));
752
- var qLocal = new pc.Quat();
753
- qLocal.setFromAxisAngle(new pc.Vec3(0, 0, 1), wallAngleDeg); // rotation autour de Z local
754
- var qFinal = new pc.Quat();
755
- qFinal.mul2(wallBaseRot, qLocal);
756
- modelRoot.setRotation(qFinal);
757
  }
758
  });
759
  }
@@ -765,9 +758,7 @@
765
  });
766
  });
767
 
768
- // Desktop : rotation à la souris (clic droit ou Shift+clic gauche)
769
- var rotateMode = false, lastMouseX = 0;
770
- var ROTATE_SENSITIVITY = 0.25;
771
  app.mouse.on("mousedown", function (e) {
772
  if (!app.xr.active || !placedOnce || uiInteracting) return;
773
  if (e.button === 0 && !e.shiftKey) {
@@ -784,17 +775,16 @@
784
  if (reticle.enabled) {
785
  var p = reticle.getPosition();
786
  modelRoot.setPosition(p);
 
787
  if (!wallMode) updateBlobPositionUnder(p, null);
788
- else modelRoot.setRotation(dragLockedRot);
789
  }
790
  } else if (rotateMode && modelRoot.enabled) {
791
  var dx = e.x - lastMouseX;
792
  lastMouseX = e.x;
793
- applyRotationY(getCurrentAngle() + dx * ROTATE_SENSITIVITY);
794
  }
795
  });
796
  app.mouse.on("mouseup", function () {
797
- // (optionnel) faire un snap sur desktop si tu déclenches un hit-test ici.
798
  isDragging = false;
799
  rotateMode = false;
800
  });
@@ -804,14 +794,14 @@
804
  rotInput.disabled = true;
805
  rotInput.addEventListener("input", function (e) {
806
  var v = parseFloat(e.target.value || "0");
807
- applyRotationY(v);
808
  }, { passive: true });
809
 
810
  // Événements AR
811
  app.xr.on("start", function () {
812
  if (startBtn) startBtn.style.display = "none";
813
  message(wallMode
814
- ? "Session AR démarrée. Visez un mur (Z=normale, Y du modèle vers le plafond)…"
815
  : "Session AR démarrée. Visez le sol pour détecter un plan…");
816
  reticle.enabled = true;
817
  });
 
1
+ /* viewer_ar_ios.js — AR PlayCanvas + GLB/USDZ via config.json (+ "sol")
2
  - config.json : { "glb_url": "...", "usdz_url": "...", "sol": true|false }
3
+ - sol = true → plans horizontaux (sol)
4
+ * Y local = Up monde
5
+ * rotation utilisateur = autour de Y local
6
+ - sol = false plans verticaux (mur)
7
+ * Y local = normale du mur
8
+ * Z local = Up monde projeté dans le plan du mur
9
+ * rotation utilisateur = autour de Y local (normale)
10
+ - Translation : aucune rotation pendant le drag ; snap sur nouveau plan en fin de drag
11
+ - iOS : AR Quick Look (manuel)
12
+ - Android/Desktop : WebXR + PlayCanvas
13
  */
14
 
15
  (function () {
 
299
  ensureQuickLookButton(USDZ_URL);
300
 
301
  if (PLACE_ON_FLOOR) {
302
+ message("Modèle chargé. Appuyez sur « Lancer l’AR ». Dans Quick Look, placez l’objet sur le sol et ajustez-le avec les gestes iOS.");
 
 
 
303
  } else {
304
+ message("Modèle chargé. Appuyez sur « Lancer l’AR ». Sur iOS (Quick Look), l’alignement automatique au mur n’est pas disponible ; placez et orientez l’objet manuellement (Y vers le plafond).");
 
 
 
305
  }
306
  } else {
307
  message("iOS détecté, mais aucun 'usdz_url' dans config.json.");
 
385
  app.root.addChild(modelRoot);
386
  var modelLoaded = false, placedOnce = false;
387
 
388
+ // --- Helpers vecteurs/quaternions ---
389
  var UP = new pc.Vec3(0, 1, 0);
390
+
391
  function projOnPlane(v, n) {
392
  var d = v.dot(n);
393
  return new pc.Vec3(v.x - d*n.x, v.y - d*n.y, v.z - d*n.z);
394
  }
395
+
 
 
 
 
 
 
 
 
 
 
 
 
396
  function quatFromBasis(X, Y, Z) {
397
  // colonnes = X,Y,Z
398
  var m00 = X.x, m01 = Y.x, m02 = Z.x;
 
429
  q.normalize();
430
  return q;
431
  }
432
+
433
+ // --- Bases pour chaque mode ---
434
+ function computeFloorBase() {
435
+ // Y local = UP (0,1,0). On fabrique une base quelconque stable.
436
+ var Y = UP.clone(); // vertical
437
+ var Z = new pc.Vec3(0, 0, 1); // avant “monde”
438
+ // Orthonormalise Z dans le plan horizontal
439
+ Z = projOnPlane(Z, Y); if (Z.lengthSq() < 1e-8) Z = new pc.Vec3(1,0,0);
440
+ Z.normalize();
441
+ var X = new pc.Vec3(); X.cross(Y, Z).normalize();
442
+ Z.cross(X, Y).normalize();
443
+ return quatFromBasis(X, Y, Z);
444
+ }
445
+
446
+ function computeWallBaseFromHit(rot) {
447
+ // Hypothèse PlayCanvas/WebXR : la normale du plan est l’axe Y de la pose.
448
+ // N = rot * (0,1,0)
449
+ var N = new pc.Vec3();
450
+ rot.transformVector(new pc.Vec3(0,1,0), N);
451
+ N.normalize(); // Y local = N (normale du mur)
452
+
453
+ // Z local = Up projeté dans le plan (toujours “vers le plafond” dans le plan vertical)
454
+ var Z = projOnPlane(UP, N);
455
+ if (Z.lengthSq() < 1e-8) Z = new pc.Vec3(0,0,1);
456
+ Z.normalize();
457
+
458
+ // X = Y × Z ; re-orthonormalise
459
+ var X = new pc.Vec3(); X.cross(N, Z).normalize();
460
+ Z.cross(X, N).normalize();
461
+
462
+ return quatFromBasis(X, N /*Y*/, Z);
463
  }
464
 
465
  // Mode mur / sol
466
  var wallMode = !PLACE_ON_FLOOR;
467
+ var baseRot = new pc.Quat(); // base : définit les axes locaux comme voulu (voir ci-dessus)
468
+ var userYawDeg = 0; // rotation utilisateur autour de Y local (toujours)
469
 
470
  // Ombre blob (sol uniquement)
471
  var blob = null;
 
522
  return e;
523
  }
524
 
525
+ // Rotation via slider (toujours autour de Y local)
 
 
526
  function clamp360(d) { return Math.max(0, Math.min(360, d)); }
527
  function updateKnobFromY(yDeg) {
528
  var t = 1 - (yDeg / 360);
 
530
  rotInput.value = String(Math.round(yDeg));
531
  rotVal.textContent = String(Math.round(yDeg)) + "°";
532
  }
 
533
 
534
+ function applyYawAroundLocalY(deg) {
535
  var y = clamp360(deg);
536
+ userYawDeg = y;
537
  if (!modelRoot.enabled) { updateKnobFromY(y); return; }
538
+ var qLocal = new pc.Quat();
539
+ qLocal.setFromAxisAngle(pc.Vec3.UP, y); // UP ici = axe Y LOCAL quand on post-multiplie
540
+ var qFinal = new pc.Quat();
541
+ qFinal.mul2(baseRot, qLocal);
542
+ modelRoot.setRotation(qFinal);
 
 
 
 
 
 
 
 
543
  updateKnobFromY(y);
544
  }
545
 
 
556
  modelRoot.addChild(instance);
557
  modelRoot.setLocalScale(1, 1, 1);
558
 
559
+ // matériaux safe
560
  var renders = instance.findComponents('render');
561
  for (var ri = 0; ri < renders.length; ri++) {
562
  var r = renders[ri];
 
572
  }
573
  }
574
 
 
 
 
575
  modelLoaded = true;
576
  message("Modèle chargé. Appuyez sur « Lancer l’AR ».");
577
  });
578
 
579
  if (!app.xr.supported) { message("WebXR n’est pas supporté sur cet appareil."); return; }
580
 
581
+ // ----- Détection orientation des plans (pose Y = normale du plan) -----
582
+ var TMP_Y = new pc.Vec3(0, 1, 0), TMP_OUT = new pc.Vec3();
583
 
584
+ // Sol : normale (Y) ≈ +Up
585
  function isHorizontalUpFacing(rot, minDot) {
 
586
  minDot = (typeof minDot === "number") ? minDot : 0.75;
587
+ rot.transformVector(TMP_Y, TMP_OUT);
588
  return TMP_OUT.y >= minDot;
589
  }
590
+
591
+ // Mur : normale (Y) horizontale → |Y.y| petit
592
  function isVerticalFacing(rot, maxAbsY) {
 
593
  maxAbsY = (typeof maxAbsY === "number") ? maxAbsY : 0.35;
594
+ rot.transformVector(TMP_Y, TMP_OUT);
595
  return Math.abs(TMP_OUT.y) <= maxAbsY;
596
  }
597
+
598
  function planeMatchesMode(rot) {
599
  return wallMode ? isVerticalFacing(rot) : isHorizontalUpFacing(rot);
600
  }
 
624
  draggingTrack = true;
625
  activePointerId = (e.pointerId != null) ? e.pointerId : 1;
626
  if (rotTrack.setPointerCapture) { try { rotTrack.setPointerCapture(activePointerId); } catch (er) {} }
627
+ applyYawAroundLocalY(degFromPointer(e));
628
  e.preventDefault(); e.stopPropagation();
629
  }
630
  function onPointerMoveCapture(e) {
631
  if (!draggingTrack || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
632
+ applyYawAroundLocalY(degFromPointer(e));
633
  e.preventDefault(); e.stopPropagation();
634
  }
635
  function endDrag(e) {
 
684
 
685
  if (!wallMode) {
686
  // --- MODE SOL ---
687
+ baseRot.copy(computeFloorBase());
688
+ // angle initial : récupérer un yaw “monde” pour démarrer aligné (optionnel)
689
+ var e = new pc.Vec3(); rot.getEulerAngles(e);
690
+ applyYawAroundLocalY(((e.y % 360) + 360) % 360);
691
+ // Ombre au sol
692
  blob = createBlobShadowAt(pos, rot);
 
 
 
 
693
  } else {
694
  // --- MODE MUR (première pose) ---
695
+ baseRot.copy(computeWallBaseFromHit(rot)); // Y=normale, Z=Up projeté
696
+ applyYawAroundLocalY(0); // yaw utilisateur = 0 au départ
 
697
  }
698
 
699
  placedOnce = true;
700
  rotInput.disabled = false;
701
  message(
702
  wallMode
703
+ ? "Objet placé contre le mur (Y=normale, Z→plafond). Glissez pour déplacer, tournez avec le slider →"
704
  : "Objet placé. Glissez pour déplacer, tournez-le avec le slider →"
705
  );
706
  }
 
710
  });
711
 
712
  // Drag XR continu (déplacements suivants, sans rotation parasite)
713
+ var rotateMode = false, lastMouseX = 0;
714
+ var ROTATE_SENSITIVITY = 0.25;
 
715
 
716
  app.xr.input.on("add", function (inputSource) {
717
  inputSource.on("selectstart", function () {
718
  if (uiInteracting) return;
719
  if (!placedOnce || !modelLoaded) return;
720
 
721
+ // Verrouille l'orientation pendant la translation
722
  dragLockedRot.copy(modelRoot.getRotation());
723
 
724
  inputSource.hitTestStart({
 
731
  if (!isDragging) return;
732
  if (!planeMatchesMode(rot)) return;
733
 
734
+ lastHitRot.copy(rot); // garde trace du dernier plan
735
+ modelRoot.setPosition(pos); // translate uniquement
736
+ modelRoot.setRotation(dragLockedRot); // aucune rotation pendant le drag
737
 
738
+ if (!wallMode) updateBlobPositionUnder(pos, rot);
 
 
 
 
739
  });
740
 
741
+ // Fin du drag : snap orientation sur NOUVEAU plan + ré-applique yaw local Y
742
  transientSource.once("remove", function () {
743
  isDragging = false;
744
  if (wallMode) {
745
+ baseRot.copy(computeWallBaseFromHit(lastHitRot)); // recalcule Y/Z
746
+ applyYawAroundLocalY(userYawDeg); // conserve l'angle utilisateur autour de Y local
747
+ } else {
748
+ // sol : garder baseRot (Y=Up monde), ne rien changer d'orientation
749
+ modelRoot.setRotation(dragLockedRot);
 
750
  }
751
  });
752
  }
 
758
  });
759
  });
760
 
761
+ // Desktop : rotation à la souris (Shift+drag gauche ou clic droit)
 
 
762
  app.mouse.on("mousedown", function (e) {
763
  if (!app.xr.active || !placedOnce || uiInteracting) return;
764
  if (e.button === 0 && !e.shiftKey) {
 
775
  if (reticle.enabled) {
776
  var p = reticle.getPosition();
777
  modelRoot.setPosition(p);
778
+ modelRoot.setRotation(dragLockedRot);
779
  if (!wallMode) updateBlobPositionUnder(p, null);
 
780
  }
781
  } else if (rotateMode && modelRoot.enabled) {
782
  var dx = e.x - lastMouseX;
783
  lastMouseX = e.x;
784
+ applyYawAroundLocalY(userYawDeg + dx * ROTATE_SENSITIVITY);
785
  }
786
  });
787
  app.mouse.on("mouseup", function () {
 
788
  isDragging = false;
789
  rotateMode = false;
790
  });
 
794
  rotInput.disabled = true;
795
  rotInput.addEventListener("input", function (e) {
796
  var v = parseFloat(e.target.value || "0");
797
+ applyYawAroundLocalY(v);
798
  }, { passive: true });
799
 
800
  // Événements AR
801
  app.xr.on("start", function () {
802
  if (startBtn) startBtn.style.display = "none";
803
  message(wallMode
804
+ ? "Session AR démarrée. Visez un mur (Y=normale du mur, Z du modèle vers le plafond)…"
805
  : "Session AR démarrée. Visez le sol pour détecter un plan…");
806
  reticle.enabled = true;
807
  });