MikaFil commited on
Commit
a27e97a
·
verified ·
1 Parent(s): 90780a9

Update viewer_ar_ios.js

Browse files
Files changed (1) hide show
  1. viewer_ar_ios.js +75 -31
viewer_ar_ios.js CHANGED
@@ -1,7 +1,7 @@
1
  /* viewer_ar_ios.js — AR PlayCanvas + GLB/USDZ via config.json
2
  - Lit config.json (data-config) => { "glb_url": "...", "usdz_url": "..." }
3
- - iOS : AR Quick Look (USDZ) avec #allowsContentScaling=0 (pas de zoom) + bouton "Lancer l’AR"
4
- - Android/Desktop : WebXR AR (plans horizontaux uniquement) + bouton "Lancer l’AR" (pas de tap écran) + curseur de rotation (yaw) + blob shadow
5
  - Éclairage PBR par défaut (sans WebXR light estimation)
6
  - Monté dans un conteneur choisi (data-mount="#ar-mount") pour Squarespace
7
  */
@@ -11,6 +11,7 @@
11
  // Utilitaires généraux
12
  // ==========================
13
 
 
14
  function getCurrentScript() {
15
  return document.currentScript || (function () {
16
  var scripts = document.getElementsByTagName('script');
@@ -18,6 +19,7 @@
18
  })();
19
  }
20
 
 
21
  function findConfigUrl() {
22
  var el = getCurrentScript();
23
  if (!el) return null;
@@ -25,16 +27,19 @@
25
  return url || null;
26
  }
27
 
 
28
  function isIOS() {
29
  return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
30
  }
31
 
 
32
  function timeout(ms) {
33
  return new Promise(function (_res, rej) {
34
  setTimeout(function () { rej(new Error("timeout")); }, ms);
35
  });
36
  }
37
 
 
38
  async function loadConfigJson(url) {
39
  if (!url) return null;
40
  try {
@@ -57,6 +62,7 @@
57
  umd: ["https://cdn.jsdelivr.net/npm/playcanvas@" + PC_VERSION + "/build/playcanvas.min.js"]
58
  };
59
 
 
60
  async function loadPlayCanvasRobust(opts) {
61
  opts = opts || {};
62
  var esmFirst = (typeof opts.esmFirst === "boolean") ? opts.esmFirst : true;
@@ -112,21 +118,29 @@
112
  // ==========================
113
  // Overlay / UI (styles intégrés)
114
  // ==========================
 
 
 
 
 
 
 
115
  var css = [
 
116
  "#ar-overlay-root{position:fixed;inset:0;z-index:10001;pointer-events:none}",
117
 
 
118
  ".pc-ar-msg{position:fixed;left:50%;transform:translateX(-50%);bottom:16px;z-index:10002;padding:10px 14px;background:rgba(0,0,0,.65);color:#fff;border-radius:12px;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;font-size:14px;line-height:1.3;text-align:center;max-width:min(90vw,640px);box-shadow:0 6px 20px rgba(0,0,0,.25);backdrop-filter:blur(4px);pointer-events:none}",
119
  ".pc-ar-msg.pc-ar-msg--centerBig{top:50%;bottom:auto;transform:translate(-50%,-50%);font-size:20px;padding:16px 22px;max-width:min(90vw,760px)}",
120
 
121
- /* ----- Bouton start commun (Android/Desktop) ----- */
122
- "#ar-start-button{position:fixed;left:50%;top:50%transform:translateX(-50%, -50%);z-index:10003;pointer-events:auto;border:none;border-radius:12px;padding:18px 28px;background:#000;color:#fff;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;font-size:20px;line-height:1;box-shadow:0 6px 20px rgba(0,0,0,.25);cursor:pointer}",
123
-
124
  "#ar-start-button[disabled]{opacity:.5;cursor:not-allowed}",
125
 
126
- /* ----- Bouton iOS Quick Look (ancre rel=ar, stylée comme un bouton) ----- */
127
- "#ar-ios-quicklook-button{position:fixed;left:50%;transform:translateX(-50%);bottom:72px;z-index:10003;display:inline-block;pointer-events:auto;background:#000;color:#fff;border-radius:12px;padding:14px 20px;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;font-size:16px;line-height:1;text-decoration:none;box-shadow:0 6px 20px rgba(0,0,0,.25)}",
128
 
129
- /* ----- Panneau rotation ----- */
130
  ".ar-rotation-panel{position:absolute;right:12px;top:50%;transform:translateY(-50%);background:rgba(0,0,0,0.55);color:#fff;padding:12px 10px;border-radius:16px;width:56px;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;pointer-events:auto;display:flex;flex-direction:column;align-items:center;gap:8px;box-shadow:0 6px 18px rgba(0,0,0,.25);backdrop-filter:blur(6px);touch-action:none}",
131
  ".ar-rotation-panel .label{font-size:12px;text-align:center;opacity:.95}",
132
  "#ar-rotation-track{position:relative;width:48px;height:200px;display:flex;align-items:center;justify-content:center;touch-action:none;overflow:visible;pointer-events:auto}",
@@ -139,7 +153,7 @@
139
  styleTag.textContent = css;
140
  document.head.appendChild(styleTag);
141
 
142
- // Overlay root
143
  function ensureOverlayRoot() {
144
  var r = document.getElementById("ar-overlay-root");
145
  if (!r) {
@@ -151,7 +165,7 @@
151
  }
152
  var overlayRoot = ensureOverlayRoot();
153
 
154
- // Messages
155
  function message(txt) {
156
  var el = overlayRoot.querySelector(".pc-ar-msg");
157
  if (!el) {
@@ -159,13 +173,12 @@
159
  el.className = "pc-ar-msg";
160
  overlayRoot.appendChild(el);
161
  }
162
- // On centre/agrandit si c’est l’invite de démarrage
163
  var isStartMsg = /Appuyez sur .Lancer l.?AR./i.test(txt);
164
  el.classList.toggle("pc-ar-msg--centerBig", isStartMsg);
165
  el.textContent = txt;
166
  }
167
 
168
- // Quick Look URL
169
  function buildQuickLookHref(usdzUrl) {
170
  try {
171
  var u = new URL(usdzUrl, window.location.href);
@@ -179,10 +192,11 @@
179
  }
180
  }
181
 
182
- // iOS: bouton rel=ar “Lancer l’AR”
183
  function ensureQuickLookButton(USDZ_URL) {
184
  var btn = document.getElementById("ar-ios-quicklook-button");
185
  if (btn) return btn;
 
186
  var anchor = document.createElement("a");
187
  anchor.id = "ar-ios-quicklook-button";
188
  anchor.setAttribute("rel", "ar");
@@ -192,7 +206,7 @@
192
  return anchor;
193
  }
194
 
195
- // Android/Desktop: bouton “Lancer l’AR”
196
  function ensureARStartButton(onClick) {
197
  var btn = document.getElementById("ar-start-button");
198
  if (!btn) {
@@ -218,14 +232,17 @@
218
  var mountSel = scriptEl && scriptEl.getAttribute('data-mount');
219
  var desiredHeight = (scriptEl && scriptEl.getAttribute('data-height')) || '70vh';
220
 
 
221
  var mountEl = null;
222
  if (mountSel) mountEl = document.querySelector(mountSel);
223
  if (!mountEl) {
 
224
  mountEl = document.createElement('div');
225
  mountEl.id = 'ar-mount-fallback';
226
  document.body.insertBefore(mountEl, document.body.firstChild);
227
  }
228
 
 
229
  var mountStyle = mountEl.style;
230
  if (!mountStyle.position) mountStyle.position = 'relative';
231
  mountStyle.width = mountStyle.width || '100%';
@@ -233,6 +250,7 @@
233
  mountStyle.touchAction = mountStyle.touchAction || 'manipulation';
234
  mountStyle.webkitTapHighlightColor = 'transparent';
235
 
 
236
  var canvas = mountEl.querySelector('#application-canvas');
237
  if (!canvas) {
238
  canvas = document.createElement('canvas');
@@ -240,6 +258,7 @@
240
  mountEl.appendChild(canvas);
241
  }
242
 
 
243
  var cs = canvas.style;
244
  cs.position = 'absolute';
245
  cs.left = '0';
@@ -248,11 +267,15 @@
248
  cs.height = '100%';
249
  cs.display = 'block';
250
 
251
- try { mountEl.scrollIntoView({ behavior: 'instant', block: 'start' }); } catch (_) {}
 
 
 
252
 
253
  return canvas;
254
  }
255
 
 
256
  function ensureRotationPanel() {
257
  var p = overlayRoot.querySelector(".ar-rotation-panel");
258
  if (p) return p;
@@ -283,10 +306,11 @@
283
  cfg.usdz_url :
284
  null;
285
 
286
- // iOS → Quick Look via bouton
287
  if (isIOS()) {
288
  if (USDZ_URL) {
289
  ensureQuickLookButton(USDZ_URL);
 
290
  message("Modèle chargé. Appuyez sur « Lancer l’AR ».");
291
  } else {
292
  message("iOS détecté, mais aucun 'usdz_url' dans config.json.");
@@ -294,7 +318,7 @@
294
  return;
295
  }
296
 
297
- // Android/Desktop → PlayCanvas/WebXR (avec bouton de démarrage)
298
  try {
299
  await loadPlayCanvasRobust({ esmFirst: true, loadTimeoutMs: 15000 });
300
  } catch (e) {
@@ -311,6 +335,7 @@
311
  function initARApp(GLB_URL) {
312
  var pc = window.pc;
313
 
 
314
  var canvas = ensureCanvas();
315
  var rotationPanel = ensureRotationPanel();
316
  var rotTrack = rotationPanel.querySelector("#ar-rotation-track");
@@ -320,6 +345,7 @@
320
 
321
  window.focus();
322
 
 
323
  var app = new pc.Application(canvas, {
324
  mouse: new pc.Mouse(canvas),
325
  touch: new pc.TouchDevice(canvas),
@@ -335,11 +361,13 @@
335
  app.on("destroy", function () { window.removeEventListener("resize", onResize); });
336
  app.start();
337
 
 
338
  app.scene.gammaCorrection = pc.GAMMA_SRGB;
339
  app.scene.toneMapping = pc.TONEMAP_ACES;
340
  app.scene.exposure = 1;
341
  app.scene.ambientLight = new pc.Color(1, 1, 1);
342
 
 
343
  var camera = new pc.Entity("Camera");
344
  camera.addComponent("camera", { clearColor: new pc.Color(0, 0, 0, 0), farClip: 10000 });
345
  app.root.addChild(camera);
@@ -349,6 +377,7 @@
349
  light.setLocalEulerAngles(45, 30, 0);
350
  app.root.addChild(light);
351
 
 
352
  var reticleMat = new pc.StandardMaterial();
353
  reticleMat.diffuse = new pc.Color(0.2, 0.8, 1.0);
354
  reticleMat.opacity = 0.85;
@@ -361,11 +390,13 @@
361
  reticle.enabled = false;
362
  app.root.addChild(reticle);
363
 
 
364
  var modelRoot = new pc.Entity("ModelRoot");
365
  modelRoot.enabled = false;
366
  app.root.addChild(modelRoot);
367
  var modelLoaded = false, placedOnce = false;
368
 
 
369
  var blob = null;
370
  var BLOB_SIZE = 0.4;
371
  var BLOB_OFFSET_Y = 0.005;
@@ -420,6 +451,7 @@
420
  return e;
421
  }
422
 
 
423
  var baseEulerX = 0, baseEulerZ = 0;
424
  var rotationYDeg = 0;
425
  function clamp360(d) { return Math.max(0, Math.min(360, d)); }
@@ -443,12 +475,14 @@
443
  if (rotLikePlane) blob.setRotation(rotLikePlane);
444
  }
445
 
 
446
  app.assets.loadFromUrl(GLB_URL, "container", function (err, asset) {
447
  if (err) { console.error(err); message("Échec du chargement du modèle GLB."); return; }
448
  var instance = asset.resource.instantiateRenderEntity({ castShadows: true, receiveShadows: false });
449
  modelRoot.addChild(instance);
450
  modelRoot.setLocalScale(1, 1, 1);
451
 
 
452
  var renders = instance.findComponents('render');
453
  for (var ri = 0; ri < renders.length; ri++) {
454
  var r = renders[ri];
@@ -468,12 +502,13 @@
468
  baseEulerX = initE.x; baseEulerZ = initE.z;
469
 
470
  modelLoaded = true;
 
471
  message("Modèle chargé. Appuyez sur « Lancer l’AR ».");
472
  });
473
 
474
  if (!app.xr.supported) { message("WebXR n’est pas supporté sur cet appareil."); return; }
475
 
476
- // ----- Slider (pointer capture) -----
477
  var uiInteracting = false;
478
  var draggingTrack = false;
479
  var activePointerId = null;
@@ -484,7 +519,7 @@
484
  var y = (e.clientY != null) ? e.clientY : ((e.touches && e.touches[0] && e.touches[0].clientY) || 0);
485
  var ratio = (y - rect.top) / rect.height;
486
  var t = Math.max(0, Math.min(1, ratio));
487
- return (1 - t) * 360;
488
  }
489
 
490
  function onPointerDownCapture(e) {
@@ -492,21 +527,29 @@
492
  uiInteracting = true;
493
  draggingTrack = true;
494
  activePointerId = (e.pointerId != null) ? e.pointerId : 1;
495
- if (rotTrack.setPointerCapture) { try { rotTrack.setPointerCapture(activePointerId); } catch (er) {} }
 
 
496
  applyRotationY(degFromPointer(e));
497
- e.preventDefault(); e.stopPropagation();
 
498
  }
499
  function onPointerMoveCapture(e) {
500
  if (!draggingTrack || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
501
  applyRotationY(degFromPointer(e));
502
- e.preventDefault(); e.stopPropagation();
 
503
  }
504
  function endDrag(e) {
505
  if (!draggingTrack || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
506
- draggingTrack = false; uiInteracting = false;
507
- if (rotTrack.releasePointerCapture) { try { rotTrack.releasePointerCapture(activePointerId); } catch (er) {} }
 
 
 
508
  activePointerId = null;
509
- e.preventDefault(); e.stopPropagation();
 
510
  }
511
 
512
  document.addEventListener("pointerdown", onPointerDownCapture, { capture: true, passive: false });
@@ -514,7 +557,7 @@
514
  document.addEventListener("pointerup", endDrag, { capture: true, passive: false });
515
  document.addEventListener("pointercancel", endDrag, { capture: true, passive: false });
516
 
517
- // ----- Démarrage AR UNIQUEMENT via bouton -----
518
  function activateAR() {
519
  if (!app.xr.isAvailable(pc.XRTYPE_AR)) { message("AR immersive indisponible sur cet appareil."); return; }
520
  if (!app.xr.domOverlay) app.xr.domOverlay = {};
@@ -534,11 +577,10 @@
534
  // Crée le bouton et relie le clic au démarrage AR
535
  var startBtn = ensureARStartButton(activateAR);
536
 
537
- // IMPORTANT : on supprime tout démarrage par clic/tap sur le canvas
538
- // (AUCUN écouteur mousedown/touchend ne lance activateAR ici)
539
  app.keyboard.on("keydown", function (evt) { if (evt.key === pc.KEY_ESCAPE && app.xr.active) app.xr.end(); });
540
 
541
- // ----- Hit-test -----
542
  var TMP_IN = new pc.Vec3(0, 1, 0), TMP_OUT = new pc.Vec3();
543
  function isHorizontalUpFacing(rot, minDot) {
544
  minDot = (typeof minDot === "number") ? minDot : 0.75;
@@ -546,6 +588,7 @@
546
  return TMP_OUT.y >= minDot;
547
  }
548
 
 
549
  app.xr.hitTest.on("available", function () {
550
  app.xr.hitTest.start({
551
  entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
@@ -562,6 +605,7 @@
562
  modelRoot.enabled = true;
563
  modelRoot.setPosition(pos);
564
 
 
565
  blob = createBlobShadowAt(pos, rot);
566
 
567
  var e = new pc.Vec3();
@@ -578,7 +622,7 @@
578
  });
579
  });
580
 
581
- // Drag XR
582
  var isDragging = false;
583
  app.xr.input.on("add", function (inputSource) {
584
  inputSource.on("selectstart", function () {
@@ -605,7 +649,7 @@
605
  inputSource.on("selectend", function () { isDragging = false; });
606
  });
607
 
608
- // Desktop rotation à la souris (clic droit ou Shift+clic gauche)
609
  var rotateMode = false, lastMouseX = 0;
610
  var ROTATE_SENSITIVITY = 0.25;
611
  app.mouse.on("mousedown", function (e) {
 
1
  /* viewer_ar_ios.js — AR PlayCanvas + GLB/USDZ via config.json
2
  - Lit config.json (data-config) => { "glb_url": "...", "usdz_url": "..." }
3
+ - iOS : AR Quick Look (USDZ) avec #allowsContentScaling=0 (pas de zoom) + bouton "Lancer l’AR" (centré)
4
+ - Android/Desktop : WebXR AR (plans horizontaux uniquement) + bouton "Lancer l’AR" (centré, pas de tap écran) + curseur de rotation (yaw) + ombre blob
5
  - Éclairage PBR par défaut (sans WebXR light estimation)
6
  - Monté dans un conteneur choisi (data-mount="#ar-mount") pour Squarespace
7
  */
 
11
  // Utilitaires généraux
12
  // ==========================
13
 
14
+ // Récupère de façon robuste le <script> courant (compatible pages CMS)
15
  function getCurrentScript() {
16
  return document.currentScript || (function () {
17
  var scripts = document.getElementsByTagName('script');
 
19
  })();
20
  }
21
 
22
+ // URL du fichier config.json passée via data-config sur le <script>
23
  function findConfigUrl() {
24
  var el = getCurrentScript();
25
  if (!el) return null;
 
27
  return url || null;
28
  }
29
 
30
+ // Détection iOS (pour Quick Look)
31
  function isIOS() {
32
  return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
33
  }
34
 
35
+ // Petit helper timeout pour sécuriser des chargements
36
  function timeout(ms) {
37
  return new Promise(function (_res, rej) {
38
  setTimeout(function () { rej(new Error("timeout")); }, ms);
39
  });
40
  }
41
 
42
+ // Charge config.json (chemins GLB/USDZ)
43
  async function loadConfigJson(url) {
44
  if (!url) return null;
45
  try {
 
62
  umd: ["https://cdn.jsdelivr.net/npm/playcanvas@" + PC_VERSION + "/build/playcanvas.min.js"]
63
  };
64
 
65
+ // Charge PlayCanvas de façon robuste (ESM puis fallback UMD)
66
  async function loadPlayCanvasRobust(opts) {
67
  opts = opts || {};
68
  var esmFirst = (typeof opts.esmFirst === "boolean") ? opts.esmFirst : true;
 
118
  // ==========================
119
  // Overlay / UI (styles intégrés)
120
  // ==========================
121
+ // NOTE: ids/classes explicites :
122
+ // - #ar-overlay-root : overlay global (DOM overlay XR + toasts)
123
+ // - #ar-start-button : bouton Android/Desktop
124
+ // - #ar-ios-quicklook-button : bouton iOS (rel="ar")
125
+ // - .ar-rotation-panel : panneau rotation
126
+ // - #ar-rotation-track/#ar-rotation-knob/#ar-rotation-range/#ar-rotation-value : slider yaw
127
+
128
  var css = [
129
+ /* Conteneur overlay global (DOM Overlay WebXR + messages) */
130
  "#ar-overlay-root{position:fixed;inset:0;z-index:10001;pointer-events:none}",
131
 
132
+ /* Toast (bas centre) utilisé pour la plupart des messages */
133
  ".pc-ar-msg{position:fixed;left:50%;transform:translateX(-50%);bottom:16px;z-index:10002;padding:10px 14px;background:rgba(0,0,0,.65);color:#fff;border-radius:12px;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;font-size:14px;line-height:1.3;text-align:center;max-width:min(90vw,640px);box-shadow:0 6px 20px rgba(0,0,0,.25);backdrop-filter:blur(4px);pointer-events:none}",
134
  ".pc-ar-msg.pc-ar-msg--centerBig{top:50%;bottom:auto;transform:translate(-50%,-50%);font-size:20px;padding:16px 22px;max-width:min(90vw,760px)}",
135
 
136
+ /* ----- Bouton start Android/Desktop : centré et légèrement plus grand ----- */
137
+ "#ar-start-button{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);z-index:10003;pointer-events:auto;border:none;border-radius:16px;padding:16px 24px;font-size:18px;line-height:1;background:#000;color:#fff;box-shadow:0 10px 28px rgba(0,0,0,.28);cursor:pointer;min-width:200px;text-align:center}",
 
138
  "#ar-start-button[disabled]{opacity:.5;cursor:not-allowed}",
139
 
140
+ /* ----- Bouton iOS Quick Look (ancre rel=ar) : centré et légèrement plus grand ----- */
141
+ "#ar-ios-quicklook-button{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);z-index:10003;display:inline-block;pointer-events:auto;background:#000;color:#fff;border-radius:16px;padding:16px 24px;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;font-size:18px;line-height:1;text-decoration:none;box-shadow:0 10px 28px rgba(0,0,0,.28);min-width:200px;text-align:center}",
142
 
143
+ /* ----- Panneau de rotation ----- */
144
  ".ar-rotation-panel{position:absolute;right:12px;top:50%;transform:translateY(-50%);background:rgba(0,0,0,0.55);color:#fff;padding:12px 10px;border-radius:16px;width:56px;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;pointer-events:auto;display:flex;flex-direction:column;align-items:center;gap:8px;box-shadow:0 6px 18px rgba(0,0,0,.25);backdrop-filter:blur(6px);touch-action:none}",
145
  ".ar-rotation-panel .label{font-size:12px;text-align:center;opacity:.95}",
146
  "#ar-rotation-track{position:relative;width:48px;height:200px;display:flex;align-items:center;justify-content:center;touch-action:none;overflow:visible;pointer-events:auto}",
 
153
  styleTag.textContent = css;
154
  document.head.appendChild(styleTag);
155
 
156
+ // Crée/récupère le conteneur d’overlay global
157
  function ensureOverlayRoot() {
158
  var r = document.getElementById("ar-overlay-root");
159
  if (!r) {
 
165
  }
166
  var overlayRoot = ensureOverlayRoot();
167
 
168
+ // Système de message (centre + agrandi si invite de départ)
169
  function message(txt) {
170
  var el = overlayRoot.querySelector(".pc-ar-msg");
171
  if (!el) {
 
173
  el.className = "pc-ar-msg";
174
  overlayRoot.appendChild(el);
175
  }
 
176
  var isStartMsg = /Appuyez sur .Lancer l.?AR./i.test(txt);
177
  el.classList.toggle("pc-ar-msg--centerBig", isStartMsg);
178
  el.textContent = txt;
179
  }
180
 
181
+ // Construit l’URL Quick Look empêchant le pinch-to-scale
182
  function buildQuickLookHref(usdzUrl) {
183
  try {
184
  var u = new URL(usdzUrl, window.location.href);
 
192
  }
193
  }
194
 
195
+ // Crée le bouton iOS Quick Look (ancre rel=ar) centré
196
  function ensureQuickLookButton(USDZ_URL) {
197
  var btn = document.getElementById("ar-ios-quicklook-button");
198
  if (btn) return btn;
199
+
200
  var anchor = document.createElement("a");
201
  anchor.id = "ar-ios-quicklook-button";
202
  anchor.setAttribute("rel", "ar");
 
206
  return anchor;
207
  }
208
 
209
+ // Android/Desktop : bouton “Lancer l’AR” — centré
210
  function ensureARStartButton(onClick) {
211
  var btn = document.getElementById("ar-start-button");
212
  if (!btn) {
 
232
  var mountSel = scriptEl && scriptEl.getAttribute('data-mount');
233
  var desiredHeight = (scriptEl && scriptEl.getAttribute('data-height')) || '70vh';
234
 
235
+ // Trouve (ou fabrique) le conteneur d’accueil du canvas
236
  var mountEl = null;
237
  if (mountSel) mountEl = document.querySelector(mountSel);
238
  if (!mountEl) {
239
+ // Fallback : placé en haut de <body> pour éviter d’être “sous le footer”
240
  mountEl = document.createElement('div');
241
  mountEl.id = 'ar-mount-fallback';
242
  document.body.insertBefore(mountEl, document.body.firstChild);
243
  }
244
 
245
+ // Style de base du conteneur
246
  var mountStyle = mountEl.style;
247
  if (!mountStyle.position) mountStyle.position = 'relative';
248
  mountStyle.width = mountStyle.width || '100%';
 
250
  mountStyle.touchAction = mountStyle.touchAction || 'manipulation';
251
  mountStyle.webkitTapHighlightColor = 'transparent';
252
 
253
+ // Crée/réutilise le canvas
254
  var canvas = mountEl.querySelector('#application-canvas');
255
  if (!canvas) {
256
  canvas = document.createElement('canvas');
 
258
  mountEl.appendChild(canvas);
259
  }
260
 
261
+ // Le canvas remplit le conteneur
262
  var cs = canvas.style;
263
  cs.position = 'absolute';
264
  cs.left = '0';
 
267
  cs.height = '100%';
268
  cs.display = 'block';
269
 
270
+ // Optionnel : faire défiler vers la zone AR
271
+ try {
272
+ mountEl.scrollIntoView({ behavior: 'instant', block: 'start' });
273
+ } catch (_) {}
274
 
275
  return canvas;
276
  }
277
 
278
+ // Panneau “Rotation” vertical (slider personnalisé)
279
  function ensureRotationPanel() {
280
  var p = overlayRoot.querySelector(".ar-rotation-panel");
281
  if (p) return p;
 
306
  cfg.usdz_url :
307
  null;
308
 
309
+ // iOS → Quick Look (bouton centré)
310
  if (isIOS()) {
311
  if (USDZ_URL) {
312
  ensureQuickLookButton(USDZ_URL);
313
+ // Message centré + agrandi pour être bien visible
314
  message("Modèle chargé. Appuyez sur « Lancer l’AR ».");
315
  } else {
316
  message("iOS détecté, mais aucun 'usdz_url' dans config.json.");
 
318
  return;
319
  }
320
 
321
+ // Android/Desktop → PlayCanvas/WebXR (avec bouton centré)
322
  try {
323
  await loadPlayCanvasRobust({ esmFirst: true, loadTimeoutMs: 15000 });
324
  } catch (e) {
 
335
  function initARApp(GLB_URL) {
336
  var pc = window.pc;
337
 
338
+ // Canvas + panneau rotation
339
  var canvas = ensureCanvas();
340
  var rotationPanel = ensureRotationPanel();
341
  var rotTrack = rotationPanel.querySelector("#ar-rotation-track");
 
345
 
346
  window.focus();
347
 
348
+ // Application PlayCanvas
349
  var app = new pc.Application(canvas, {
350
  mouse: new pc.Mouse(canvas),
351
  touch: new pc.TouchDevice(canvas),
 
361
  app.on("destroy", function () { window.removeEventListener("resize", onResize); });
362
  app.start();
363
 
364
+ // Réglages de rendu PBR “safe”
365
  app.scene.gammaCorrection = pc.GAMMA_SRGB;
366
  app.scene.toneMapping = pc.TONEMAP_ACES;
367
  app.scene.exposure = 1;
368
  app.scene.ambientLight = new pc.Color(1, 1, 1);
369
 
370
+ // Caméra + lumière
371
  var camera = new pc.Entity("Camera");
372
  camera.addComponent("camera", { clearColor: new pc.Color(0, 0, 0, 0), farClip: 10000 });
373
  app.root.addChild(camera);
 
377
  light.setLocalEulerAngles(45, 30, 0);
378
  app.root.addChild(light);
379
 
380
+ // Réticule (visualise la cible du hit-test)
381
  var reticleMat = new pc.StandardMaterial();
382
  reticleMat.diffuse = new pc.Color(0.2, 0.8, 1.0);
383
  reticleMat.opacity = 0.85;
 
390
  reticle.enabled = false;
391
  app.root.addChild(reticle);
392
 
393
+ // Conteneur du modèle chargé
394
  var modelRoot = new pc.Entity("ModelRoot");
395
  modelRoot.enabled = false;
396
  app.root.addChild(modelRoot);
397
  var modelLoaded = false, placedOnce = false;
398
 
399
+ // Ombre de contact (blob “peinte” sur un plan)
400
  var blob = null;
401
  var BLOB_SIZE = 0.4;
402
  var BLOB_OFFSET_Y = 0.005;
 
451
  return e;
452
  }
453
 
454
+ // Rotation via slider (0..360)
455
  var baseEulerX = 0, baseEulerZ = 0;
456
  var rotationYDeg = 0;
457
  function clamp360(d) { return Math.max(0, Math.min(360, d)); }
 
475
  if (rotLikePlane) blob.setRotation(rotLikePlane);
476
  }
477
 
478
+ // Chargement du modèle GLB (depuis config.json)
479
  app.assets.loadFromUrl(GLB_URL, "container", function (err, asset) {
480
  if (err) { console.error(err); message("Échec du chargement du modèle GLB."); return; }
481
  var instance = asset.resource.instantiateRenderEntity({ castShadows: true, receiveShadows: false });
482
  modelRoot.addChild(instance);
483
  modelRoot.setLocalScale(1, 1, 1);
484
 
485
+ // Ajustements simples matériaux (évite rendu trop sombre sans IBL)
486
  var renders = instance.findComponents('render');
487
  for (var ri = 0; ri < renders.length; ri++) {
488
  var r = renders[ri];
 
502
  baseEulerX = initE.x; baseEulerZ = initE.z;
503
 
504
  modelLoaded = true;
505
+ // >>> Message de démarrage
506
  message("Modèle chargé. Appuyez sur « Lancer l’AR ».");
507
  });
508
 
509
  if (!app.xr.supported) { message("WebXR n’est pas supporté sur cet appareil."); return; }
510
 
511
+ // Gestion fiable du slider (capture des pointer events)
512
  var uiInteracting = false;
513
  var draggingTrack = false;
514
  var activePointerId = null;
 
519
  var y = (e.clientY != null) ? e.clientY : ((e.touches && e.touches[0] && e.touches[0].clientY) || 0);
520
  var ratio = (y - rect.top) / rect.height;
521
  var t = Math.max(0, Math.min(1, ratio));
522
+ return (1 - t) * 360; // 360 en haut, 0 en bas
523
  }
524
 
525
  function onPointerDownCapture(e) {
 
527
  uiInteracting = true;
528
  draggingTrack = true;
529
  activePointerId = (e.pointerId != null) ? e.pointerId : 1;
530
+ if (rotTrack.setPointerCapture) {
531
+ try { rotTrack.setPointerCapture(activePointerId); } catch (er) {}
532
+ }
533
  applyRotationY(degFromPointer(e));
534
+ e.preventDefault();
535
+ e.stopPropagation();
536
  }
537
  function onPointerMoveCapture(e) {
538
  if (!draggingTrack || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
539
  applyRotationY(degFromPointer(e));
540
+ e.preventDefault();
541
+ e.stopPropagation();
542
  }
543
  function endDrag(e) {
544
  if (!draggingTrack || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
545
+ draggingTrack = false;
546
+ uiInteracting = false;
547
+ if (rotTrack.releasePointerCapture) {
548
+ try { rotTrack.releasePointerCapture(activePointerId); } catch (er) {}
549
+ }
550
  activePointerId = null;
551
+ e.preventDefault();
552
+ e.stopPropagation();
553
  }
554
 
555
  document.addEventListener("pointerdown", onPointerDownCapture, { capture: true, passive: false });
 
557
  document.addEventListener("pointerup", endDrag, { capture: true, passive: false });
558
  document.addEventListener("pointercancel", endDrag, { capture: true, passive: false });
559
 
560
+ // Démarrage AR (UNIQUEMENT via bouton)
561
  function activateAR() {
562
  if (!app.xr.isAvailable(pc.XRTYPE_AR)) { message("AR immersive indisponible sur cet appareil."); return; }
563
  if (!app.xr.domOverlay) app.xr.domOverlay = {};
 
577
  // Crée le bouton et relie le clic au démarrage AR
578
  var startBtn = ensureARStartButton(activateAR);
579
 
580
+ // IMPORTANT : aucun démarrage par clic/tap sur le canvas
 
581
  app.keyboard.on("keydown", function (evt) { if (evt.key === pc.KEY_ESCAPE && app.xr.active) app.xr.end(); });
582
 
583
+ // Hit-test : détecte uniquement des surfaces quasi-horizontales
584
  var TMP_IN = new pc.Vec3(0, 1, 0), TMP_OUT = new pc.Vec3();
585
  function isHorizontalUpFacing(rot, minDot) {
586
  minDot = (typeof minDot === "number") ? minDot : 0.75;
 
588
  return TMP_OUT.y >= minDot;
589
  }
590
 
591
+ // Hit Test global (alimentation du réticule + premier placement)
592
  app.xr.hitTest.on("available", function () {
593
  app.xr.hitTest.start({
594
  entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
 
605
  modelRoot.enabled = true;
606
  modelRoot.setPosition(pos);
607
 
608
+ // Ombre de contact au placement initial
609
  blob = createBlobShadowAt(pos, rot);
610
 
611
  var e = new pc.Vec3();
 
622
  });
623
  });
624
 
625
+ // Déplacement XR continu (drag) — ignoré si UI active
626
  var isDragging = false;
627
  app.xr.input.on("add", function (inputSource) {
628
  inputSource.on("selectstart", function () {
 
649
  inputSource.on("selectend", function () { isDragging = false; });
650
  });
651
 
652
+ // Desktop : rotation à la souris (clic droit ou Shift+clic gauche)
653
  var rotateMode = false, lastMouseX = 0;
654
  var ROTATE_SENSITIVITY = 0.25;
655
  app.mouse.on("mousedown", function (e) {