MikaFil commited on
Commit
01823c0
·
verified ·
1 Parent(s): 47527db

Update viewer_ar_ios.js

Browse files
Files changed (1) hide show
  1. viewer_ar_ios.js +132 -50
viewer_ar_ios.js CHANGED
@@ -1,8 +1,13 @@
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) + rotation autour de l’axe local Y (haut)
4
- - sol = false → plans verticaux (mur) + rotation autour de l’axe local Y aligné à la normale du mur
5
- - iOS : AR Quick Look (USDZ) — l’utilisateur place et oriente l’objet lui-même
 
 
 
 
 
6
  - Android/Desktop : WebXR AR + bouton "Lancer l’AR" + slider de rotation
7
  */
8
 
@@ -300,8 +305,8 @@
300
  } else {
301
  message(
302
  "Modèle chargé. Appuyez sur « Lancer l’AR ». Sur iOS (Quick Look), l’objet ne peut pas être " +
303
- "automatiquement aligné au mur comme sur Android : placez-le et orientez-le manuellement " +
304
- "contre le mur (pointe vers vous, base sur le mur)."
305
  );
306
  }
307
  } else {
@@ -386,10 +391,75 @@
386
  app.root.addChild(modelRoot);
387
  var modelLoaded = false, placedOnce = false;
388
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  // Mode mur / sol
390
  var wallMode = !PLACE_ON_FLOOR;
391
- var wallBaseRot = new pc.Quat(); // orientation de base alignée au mur
392
- var wallAngleDeg = 0;
393
 
394
  // Ombre blob (sol uniquement)
395
  var blob = null;
@@ -446,29 +516,21 @@
446
  return e;
447
  }
448
 
449
- // Rotation via slider
450
  var baseEulerX = 0, baseEulerZ = 0;
451
  var rotationYDeg = 0;
452
-
453
  function clamp360(d) { return Math.max(0, Math.min(360, d)); }
454
-
455
  function updateKnobFromY(yDeg) {
456
  var t = 1 - (yDeg / 360);
457
  rotKnob.style.top = String(t * 100) + "%";
458
  rotInput.value = String(Math.round(yDeg));
459
  rotVal.textContent = String(Math.round(yDeg)) + "°";
460
  }
461
-
462
- function getCurrentAngle() {
463
- return wallMode ? wallAngleDeg : rotationYDeg;
464
- }
465
 
466
  function applyRotationY(deg) {
467
  var y = clamp360(deg);
468
- if (!modelRoot.enabled) {
469
- updateKnobFromY(y);
470
- return;
471
- }
472
 
473
  if (!wallMode) {
474
  rotationYDeg = y;
@@ -476,12 +538,11 @@
476
  } else {
477
  wallAngleDeg = y;
478
  var qLocal = new pc.Quat();
479
- qLocal.setFromAxisAngle(pc.Vec3.UP, y); // rotation locale autour de Y
480
  var qFinal = new pc.Quat();
481
- qFinal.mul2(wallBaseRot, qLocal); // base (mur) puis rotation locale Y
482
  modelRoot.setRotation(qFinal);
483
  }
484
-
485
  updateKnobFromY(y);
486
  }
487
 
@@ -517,7 +578,7 @@
517
  baseEulerX = initE.x; baseEulerZ = initE.z;
518
 
519
  modelLoaded = true;
520
- message("Modèle chargé. Appuyez sur « L’ARancer l’AR ».");
521
  });
522
 
523
  if (!app.xr.supported) { message("WebXR n’est pas supporté sur cet appareil."); return; }
@@ -525,25 +586,26 @@
525
  // ----- Détection orientation des plans -----
526
  var TMP_IN = new pc.Vec3(0, 1, 0), TMP_OUT = new pc.Vec3();
527
 
528
- // Sol : normal ≈ +Y
529
  function isHorizontalUpFacing(rot, minDot) {
530
  minDot = (typeof minDot === "number") ? minDot : 0.75;
531
  rot.transformVector(TMP_IN, TMP_OUT);
532
  return TMP_OUT.y >= minDot;
533
  }
534
-
535
- // Mur : normal horizontale → axe Y du hit-test presque horizontal
536
  function isVerticalFacing(rot, maxAbsY) {
537
  maxAbsY = (typeof maxAbsY === "number") ? maxAbsY : 0.35;
538
  rot.transformVector(TMP_IN, TMP_OUT);
539
  return Math.abs(TMP_OUT.y) <= maxAbsY;
540
  }
541
-
542
  function planeMatchesMode(rot) {
543
  return wallMode ? isVerticalFacing(rot) : isHorizontalUpFacing(rot);
544
  }
545
 
546
- // Slider (pointer capture)
 
 
 
 
 
547
  var uiInteracting = false;
548
  var draggingTrack = false;
549
  var activePointerId = null;
@@ -602,10 +664,9 @@
602
  }
603
 
604
  var startBtn = ensureARStartButton(activateAR);
605
-
606
  app.keyboard.on("keydown", function (evt) { if (evt.key === pc.KEY_ESCAPE && app.xr.active) app.xr.end(); });
607
 
608
- // Hit-test principal (pour le reticle + premier placement)
609
  app.xr.hitTest.on("available", function () {
610
  app.xr.hitTest.start({
611
  entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
@@ -631,16 +692,16 @@
631
  applyRotationY(y0);
632
  } else {
633
  // --- MODE MUR (première pose) ---
634
- modelRoot.setRotation(rot); // Y local = normale du mur
635
- wallBaseRot.copy(rot);
636
- applyRotationY(0); // angle initial = 0
637
  }
638
 
639
  placedOnce = true;
640
  rotInput.disabled = false;
641
  message(
642
  wallMode
643
- ? "Objet placé contre le mur. Glissez pour déplacer sur un autre mur, tournez-le avec le slider →"
644
  : "Objet placé. Glissez pour déplacer, tournez-le avec le slider →"
645
  );
646
  }
@@ -649,13 +710,15 @@
649
  });
650
  });
651
 
652
- // Drag XR continu (déplacement après placement)
653
- var isDragging = false;
654
  app.xr.input.on("add", function (inputSource) {
655
  inputSource.on("selectstart", function () {
656
  if (uiInteracting) return;
657
  if (!placedOnce || !modelLoaded) return;
658
 
 
 
 
659
  inputSource.hitTestStart({
660
  entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
661
  callback: function (err, transientSource) {
@@ -666,15 +729,27 @@
666
  if (!isDragging) return;
667
  if (!planeMatchesMode(rot)) return;
668
 
 
 
 
 
669
  modelRoot.setPosition(pos);
670
 
671
  if (!wallMode) {
672
- // sol : on ne change que la position, l'orientation reste pilotée par le slider
673
  updateBlobPositionUnder(pos, rot);
674
  } else {
675
- // mur : à CHAQUE déplacement on ré-aligne la base sur le plan courant,
676
- // puis on ré-applique l'angle du slider autour de Y
677
- wallBaseRot.copy(rot); // Y local = normale du mur courant
 
 
 
 
 
 
 
 
 
678
  var qLocal = new pc.Quat();
679
  qLocal.setFromAxisAngle(pc.Vec3.UP, wallAngleDeg);
680
  var qFinal = new pc.Quat();
@@ -682,12 +757,13 @@
682
  modelRoot.setRotation(qFinal);
683
  }
684
  });
685
-
686
- transientSource.once("remove", function () { isDragging = false; });
687
  }
688
  });
689
  });
690
- inputSource.on("selectend", function () { isDragging = false; });
 
 
 
691
  });
692
 
693
  // Desktop : rotation à la souris (clic droit ou Shift+clic gauche)
@@ -697,6 +773,7 @@
697
  if (!app.xr.active || !placedOnce || uiInteracting) return;
698
  if (e.button === 0 && !e.shiftKey) {
699
  isDragging = true;
 
700
  } else if (e.button === 2 || (e.button === 0 && e.shiftKey)) {
701
  rotateMode = true;
702
  lastMouseX = e.x;
@@ -709,6 +786,7 @@
709
  var p = reticle.getPosition();
710
  modelRoot.setPosition(p);
711
  if (!wallMode) updateBlobPositionUnder(p, null);
 
712
  }
713
  } else if (rotateMode && modelRoot.enabled) {
714
  var dx = e.x - lastMouseX;
@@ -716,7 +794,14 @@
716
  applyRotationY(getCurrentAngle() + dx * ROTATE_SENSITIVITY);
717
  }
718
  });
719
- app.mouse.on("mouseup", function () { isDragging = false; rotateMode = false; });
 
 
 
 
 
 
 
720
  window.addEventListener("contextmenu", function (e) { e.preventDefault(); });
721
 
722
  // Slider (accessibilité clavier)
@@ -726,14 +811,12 @@
726
  applyRotationY(v);
727
  }, { passive: true });
728
 
729
- // Événements AR (feedback + visibilité bouton)
730
  app.xr.on("start", function () {
731
  if (startBtn) startBtn.style.display = "none";
732
- message(
733
- wallMode
734
- ? "Session AR démarrée. Visez un mur pour détecter un plan vertical…"
735
- : "Session AR démarrée. Visez le sol pour détecter un plan…"
736
- );
737
  reticle.enabled = true;
738
  });
739
  app.xr.on("end", function () {
@@ -741,7 +824,6 @@
741
  message("Session AR terminée.");
742
  reticle.enabled = false;
743
  isDragging = false;
744
- rotateMode = false;
745
  rotInput.disabled = true;
746
  });
747
  app.xr.on("available:" + pc.XRTYPE_AR, function (a) {
 
1
+ /* viewer_ar_ios.js — AR PlayCanvas + GLB/USDZ via config.json (+ "sol" + mode mur stable)
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
+ * Y local = normale du mur
6
+ * Z local = direction plafond (Up monde) projetée dans le plan du mur
7
+ * rotation utilisateur = autour de Y local (normale)
8
+ * PENDANT une translation : orientation verrouillée (pas de twist parasite)
9
+ * À la fin d’une translation : on recalcule la base par rapport AU NOUVEAU MUR
10
+ - iOS : AR Quick Look (USDZ)
11
  - Android/Desktop : WebXR AR + bouton "Lancer l’AR" + slider de rotation
12
  */
13
 
 
305
  } else {
306
  message(
307
  "Modèle chargé. Appuyez sur « Lancer l’AR ». Sur iOS (Quick Look), l’objet ne peut pas être " +
308
+ "automatiquement aligné au mur : placez-le et orientez-le manuellement " +
309
+ "contre le mur (Z vers le plafond)."
310
  );
311
  }
312
  } else {
 
391
  app.root.addChild(modelRoot);
392
  var modelLoaded = false, placedOnce = false;
393
 
394
+ // --- Helpers vecteurs/quaternions stables ---
395
+ var UP = new pc.Vec3(0, 1, 0);
396
+ function projOnPlane(v, n) {
397
+ var d = v.dot(n);
398
+ return new pc.Vec3(v.x - d*n.x, v.y - d*n.y, v.z - d*n.z);
399
+ }
400
+ function normalizeOr(v, fallback) {
401
+ var len = v.length();
402
+ if (len > 1e-6) { v.scale(1/len); return v; }
403
+ return fallback.clone ? fallback.clone() : new pc.Vec3(fallback.x, fallback.y, fallback.z);
404
+ }
405
+ function buildWallBasisFromNormal(N) {
406
+ var Y = N.clone().normalize(); // Y = normale du mur
407
+ var Z = projOnPlane(UP, Y); // Z = Up projeté (vers plafond)
408
+ if (Z.lengthSq() < 1e-8) Z = projOnPlane(new pc.Vec3(0,0,1), Y);
409
+ Z.normalize();
410
+ var X = new pc.Vec3(); X.cross(Z, Y).normalize(); // X = Z × Y
411
+ Z.cross(Y, X).normalize(); // Re-orthonormalise
412
+ return { X: X, Y: Y, Z: Z };
413
+ }
414
+ function quatFromBasis(X, Y, Z) {
415
+ // colonnes = X,Y,Z
416
+ var m00 = X.x, m01 = Y.x, m02 = Z.x;
417
+ var m10 = X.y, m11 = Y.y, m12 = Z.y;
418
+ var m20 = X.z, m21 = Y.z, m22 = Z.z;
419
+
420
+ var t = m00 + m11 + m22;
421
+ var q = new pc.Quat();
422
+ if (t > 0) {
423
+ var s = Math.sqrt(t + 1.0) * 2;
424
+ q.w = 0.25 * s;
425
+ q.x = (m21 - m12) / s;
426
+ q.y = (m02 - m20) / s;
427
+ q.z = (m10 - m01) / s;
428
+ } else if (m00 > m11 && m00 > m22) {
429
+ var s = Math.sqrt(1.0 + m00 - m11 - m22) * 2;
430
+ q.w = (m21 - m12) / s;
431
+ q.x = 0.25 * s;
432
+ q.y = (m01 + m10) / s;
433
+ q.z = (m02 + m20) / s;
434
+ } else if (m11 > m22) {
435
+ var s2 = Math.sqrt(1.0 + m11 - m00 - m22) * 2;
436
+ q.w = (m02 - m20) / s2;
437
+ q.x = (m01 + m10) / s2;
438
+ q.y = 0.25 * s2;
439
+ q.z = (m12 + m21) / s2;
440
+ } else {
441
+ var s3 = Math.sqrt(1.0 + m22 - m00 - m11) * 2;
442
+ q.w = (m10 - m01) / s3;
443
+ q.x = (m02 + m20) / s3;
444
+ q.y = (m12 + m21) / s3;
445
+ q.z = 0.25 * s3;
446
+ }
447
+ q.normalize();
448
+ return q;
449
+ }
450
+ function computeWallBaseFromRot(rot) {
451
+ // Y monde = rot * (0,1,0) = normale du mur
452
+ var N = new pc.Vec3();
453
+ rot.transformVector(new pc.Vec3(0, 1, 0), N);
454
+ N.normalize();
455
+ var basis = buildWallBasisFromNormal(N);
456
+ return quatFromBasis(basis.X, basis.Y, basis.Z); // Y=normale, Z=plafond
457
+ }
458
+
459
  // Mode mur / sol
460
  var wallMode = !PLACE_ON_FLOOR;
461
+ var wallBaseRot = new pc.Quat(); // base (mur) : Y=normale, Z=plafond
462
+ var wallAngleDeg = 0; // yaw utilisateur autour de Y local
463
 
464
  // Ombre blob (sol uniquement)
465
  var blob = null;
 
516
  return e;
517
  }
518
 
519
+ // Rotation via slider (sol: Euler Y, mur: twist autour de Y local)
520
  var baseEulerX = 0, baseEulerZ = 0;
521
  var rotationYDeg = 0;
 
522
  function clamp360(d) { return Math.max(0, Math.min(360, d)); }
 
523
  function updateKnobFromY(yDeg) {
524
  var t = 1 - (yDeg / 360);
525
  rotKnob.style.top = String(t * 100) + "%";
526
  rotInput.value = String(Math.round(yDeg));
527
  rotVal.textContent = String(Math.round(yDeg)) + "°";
528
  }
529
+ function getCurrentAngle() { return wallMode ? wallAngleDeg : rotationYDeg; }
 
 
 
530
 
531
  function applyRotationY(deg) {
532
  var y = clamp360(deg);
533
+ if (!modelRoot.enabled) { updateKnobFromY(y); return; }
 
 
 
534
 
535
  if (!wallMode) {
536
  rotationYDeg = y;
 
538
  } else {
539
  wallAngleDeg = y;
540
  var qLocal = new pc.Quat();
541
+ qLocal.setFromAxisAngle(pc.Vec3.UP, y); // rotation autour de Y local (avant base)
542
  var qFinal = new pc.Quat();
543
+ qFinal.mul2(wallBaseRot, qLocal); // base mur (Y=normale, Z=up projeté) puis yaw utilisateur
544
  modelRoot.setRotation(qFinal);
545
  }
 
546
  updateKnobFromY(y);
547
  }
548
 
 
578
  baseEulerX = initE.x; baseEulerZ = initE.z;
579
 
580
  modelLoaded = true;
581
+ message("Modèle chargé. Appuyez sur « Lancer l’AR ».");
582
  });
583
 
584
  if (!app.xr.supported) { message("WebXR n’est pas supporté sur cet appareil."); return; }
 
586
  // ----- Détection orientation des plans -----
587
  var TMP_IN = new pc.Vec3(0, 1, 0), TMP_OUT = new pc.Vec3();
588
 
 
589
  function isHorizontalUpFacing(rot, minDot) {
590
  minDot = (typeof minDot === "number") ? minDot : 0.75;
591
  rot.transformVector(TMP_IN, TMP_OUT);
592
  return TMP_OUT.y >= minDot;
593
  }
 
 
594
  function isVerticalFacing(rot, maxAbsY) {
595
  maxAbsY = (typeof maxAbsY === "number") ? maxAbsY : 0.35;
596
  rot.transformVector(TMP_IN, TMP_OUT);
597
  return Math.abs(TMP_OUT.y) <= maxAbsY;
598
  }
 
599
  function planeMatchesMode(rot) {
600
  return wallMode ? isVerticalFacing(rot) : isHorizontalUpFacing(rot);
601
  }
602
 
603
+ // --- Translation sans rotation parasite ---
604
+ var isDragging = false;
605
+ var dragLockedRot = new pc.Quat(); // orientation figée pendant le drag
606
+ var lastHitRot = new pc.Quat(); // dernière rotation de hit (pour snap en fin de drag)
607
+
608
+ // Slider (pointer capture UI)
609
  var uiInteracting = false;
610
  var draggingTrack = false;
611
  var activePointerId = null;
 
664
  }
665
 
666
  var startBtn = ensureARStartButton(activateAR);
 
667
  app.keyboard.on("keydown", function (evt) { if (evt.key === pc.KEY_ESCAPE && app.xr.active) app.xr.end(); });
668
 
669
+ // Hit-test principal (reticle + PREMIER placement)
670
  app.xr.hitTest.on("available", function () {
671
  app.xr.hitTest.start({
672
  entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
 
692
  applyRotationY(y0);
693
  } else {
694
  // --- MODE MUR (première pose) ---
695
+ wallBaseRot.copy(computeWallBaseFromRot(rot)); // Y=normale, Z=plafond
696
+ modelRoot.setRotation(wallBaseRot);
697
+ applyRotationY(0); // yaw initial = 0
698
  }
699
 
700
  placedOnce = true;
701
  rotInput.disabled = false;
702
  message(
703
  wallMode
704
+ ? "Objet placé contre le mur (Y=normale, Z→plafond). Glissez pour déplacer, tournez avec le slider →"
705
  : "Objet placé. Glissez pour déplacer, tournez-le avec le slider →"
706
  );
707
  }
 
710
  });
711
  });
712
 
713
+ // Drag XR continu (déplacements suivants, sans rotation parasite)
 
714
  app.xr.input.on("add", function (inputSource) {
715
  inputSource.on("selectstart", function () {
716
  if (uiInteracting) return;
717
  if (!placedOnce || !modelLoaded) return;
718
 
719
+ // Verrouille l'orientation courante pendant la translation
720
+ dragLockedRot.copy(modelRoot.getRotation());
721
+
722
  inputSource.hitTestStart({
723
  entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
724
  callback: function (err, transientSource) {
 
729
  if (!isDragging) return;
730
  if (!planeMatchesMode(rot)) return;
731
 
732
+ // Mémorise la dernière orientation de plan rencontrée
733
+ lastHitRot.copy(rot);
734
+
735
+ // Déplace uniquement
736
  modelRoot.setPosition(pos);
737
 
738
  if (!wallMode) {
 
739
  updateBlobPositionUnder(pos, rot);
740
  } else {
741
+ // En mode mur : PAS de rotation pendant le drag
742
+ modelRoot.setRotation(dragLockedRot);
743
+ }
744
+ });
745
+
746
+ // Fin du drag : snap propre au NOUVEAU mur (si on est en mode mur)
747
+ transientSource.once("remove", function () {
748
+ isDragging = false;
749
+ if (wallMode) {
750
+ // Recalcule la base avec le dernier mur touché
751
+ wallBaseRot.copy(computeWallBaseFromRot(lastHitRot));
752
+ // Ré-applique le yaw utilisateur autour de Y local (normale du NOUVEAU mur)
753
  var qLocal = new pc.Quat();
754
  qLocal.setFromAxisAngle(pc.Vec3.UP, wallAngleDeg);
755
  var qFinal = new pc.Quat();
 
757
  modelRoot.setRotation(qFinal);
758
  }
759
  });
 
 
760
  }
761
  });
762
  });
763
+
764
+ inputSource.on("selectend", function () {
765
+ isDragging = false;
766
+ });
767
  });
768
 
769
  // Desktop : rotation à la souris (clic droit ou Shift+clic gauche)
 
773
  if (!app.xr.active || !placedOnce || uiInteracting) return;
774
  if (e.button === 0 && !e.shiftKey) {
775
  isDragging = true;
776
+ dragLockedRot.copy(modelRoot.getRotation());
777
  } else if (e.button === 2 || (e.button === 0 && e.shiftKey)) {
778
  rotateMode = true;
779
  lastMouseX = e.x;
 
786
  var p = reticle.getPosition();
787
  modelRoot.setPosition(p);
788
  if (!wallMode) updateBlobPositionUnder(p, null);
789
+ else modelRoot.setRotation(dragLockedRot);
790
  }
791
  } else if (rotateMode && modelRoot.enabled) {
792
  var dx = e.x - lastMouseX;
 
794
  applyRotationY(getCurrentAngle() + dx * ROTATE_SENSITIVITY);
795
  }
796
  });
797
+ app.mouse.on("mouseup", function () {
798
+ if (isDragging) {
799
+ // En desktop on n'a pas lastHitRot : on garde l'orientation verrouillée
800
+ // (optionnel : on pourrait lancer un hit-test unique ici pour "snap", selon l'intégration)
801
+ }
802
+ isDragging = false;
803
+ rotateMode = false;
804
+ });
805
  window.addEventListener("contextmenu", function (e) { e.preventDefault(); });
806
 
807
  // Slider (accessibilité clavier)
 
811
  applyRotationY(v);
812
  }, { passive: true });
813
 
814
+ // Événements AR
815
  app.xr.on("start", function () {
816
  if (startBtn) startBtn.style.display = "none";
817
+ message(wallMode
818
+ ? "Session AR démarrée. Visez un mur (Y=normale, Z du modèle sera vers le plafond)…"
819
+ : "Session AR démarrée. Visez le sol pour détecter un plan…");
 
 
820
  reticle.enabled = true;
821
  });
822
  app.xr.on("end", function () {
 
824
  message("Session AR terminée.");
825
  reticle.enabled = false;
826
  isDragging = false;
 
827
  rotInput.disabled = true;
828
  });
829
  app.xr.on("available:" + pc.XRTYPE_AR, function (a) {