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

Update viewer_ar_ios.js

Browse files
Files changed (1) hide show
  1. viewer_ar_ios.js +47 -69
viewer_ar_ios.js CHANGED
@@ -1,6 +1,6 @@
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
@@ -10,8 +10,6 @@
10
  // ==========================
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,7 +17,6 @@
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,19 +24,16 @@
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,7 +56,6 @@
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,29 +111,29 @@
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,7 +146,6 @@
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,7 +157,6 @@
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) {
@@ -178,7 +169,7 @@
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,7 +183,7 @@
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;
@@ -201,12 +192,23 @@
201
  anchor.id = "ar-ios-quicklook-button";
202
  anchor.setAttribute("rel", "ar");
203
  anchor.setAttribute("href", buildQuickLookHref(USDZ_URL));
204
- anchor.textContent = "Lancer l’AR";
 
 
 
 
 
 
 
 
 
 
 
205
  document.body.appendChild(anchor);
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,17 +234,14 @@
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,7 +249,6 @@
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,7 +256,6 @@
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,15 +264,11 @@
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,11 +299,10 @@
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,7 +310,7 @@
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,7 +327,6 @@
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,7 +336,6 @@
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,7 +351,7 @@
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;
@@ -377,7 +367,7 @@
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,13 +380,13 @@
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,7 +441,7 @@
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,14 +465,13 @@
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,13 +491,12 @@
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,7 +507,7 @@
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,29 +515,21 @@
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 });
@@ -574,13 +554,13 @@
574
  });
575
  }
576
 
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,7 +568,6 @@
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,7 +584,6 @@
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,7 +600,7 @@
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 () {
@@ -686,7 +664,7 @@
686
  applyRotationY(v);
687
  }, { passive: true });
688
 
689
- // Événements AR (feedback + visibilité du bouton)
690
  app.xr.on("start", function () {
691
  if (startBtn) startBtn.style.display = "none";
692
  message("Session AR démarrée. Visez le sol pour détecter un plan…");
 
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é, déclenche Quick Look direct)
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
 
10
  // ==========================
11
  // Utilitaires généraux
12
  // ==========================
 
 
13
  function getCurrentScript() {
14
  return document.currentScript || (function () {
15
  var scripts = document.getElementsByTagName('script');
 
17
  })();
18
  }
19
 
 
20
  function findConfigUrl() {
21
  var el = getCurrentScript();
22
  if (!el) return null;
 
24
  return url || null;
25
  }
26
 
 
27
  function isIOS() {
28
  return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
29
  }
30
 
 
31
  function timeout(ms) {
32
  return new Promise(function (_res, rej) {
33
  setTimeout(function () { rej(new Error("timeout")); }, ms);
34
  });
35
  }
36
 
 
37
  async function loadConfigJson(url) {
38
  if (!url) return null;
39
  try {
 
56
  umd: ["https://cdn.jsdelivr.net/npm/playcanvas@" + PC_VERSION + "/build/playcanvas.min.js"]
57
  };
58
 
 
59
  async function loadPlayCanvasRobust(opts) {
60
  opts = opts || {};
61
  var esmFirst = (typeof opts.esmFirst === "boolean") ? opts.esmFirst : true;
 
111
  // ==========================
112
  // Overlay / UI (styles intégrés)
113
  // ==========================
 
114
  // - #ar-overlay-root : overlay global (DOM overlay XR + toasts)
115
  // - #ar-start-button : bouton Android/Desktop
116
+ // - #ar-ios-quicklook-button : bouton iOS (rel="ar" + <img> requis pour Quick Look)
117
  // - .ar-rotation-panel : panneau rotation
118
+ // - #ar-rotation-* : slider yaw
119
 
120
  var css = [
121
+ /* Conteneur overlay global */
122
  "#ar-overlay-root{position:fixed;inset:0;z-index:10001;pointer-events:none}",
123
 
124
+ /* Toasts */
125
  ".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}",
126
  ".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)}",
127
 
128
+ /* ----- Bouton Android/Desktop : centré + légèrement plus grand ----- */
129
  "#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}",
130
  "#ar-start-button[disabled]{opacity:.5;cursor:not-allowed}",
131
 
132
+ /* ----- Bouton iOS Quick Look : centré + <img> (exigé par Quick Look) ----- */
133
+ "#ar-ios-quicklook-button{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);z-index:10003;pointer-events:auto;display:inline-block;text-decoration:none}",
134
+ "#ar-ios-quicklook-button img{display:block;height:60px;width:auto;box-shadow:0 10px 28px rgba(0,0,0,.28);border-radius:14px}",
135
 
136
+ /* ----- Panneau rotation ----- */
137
  ".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}",
138
  ".ar-rotation-panel .label{font-size:12px;text-align:center;opacity:.95}",
139
  "#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}",
 
146
  styleTag.textContent = css;
147
  document.head.appendChild(styleTag);
148
 
 
149
  function ensureOverlayRoot() {
150
  var r = document.getElementById("ar-overlay-root");
151
  if (!r) {
 
157
  }
158
  var overlayRoot = ensureOverlayRoot();
159
 
 
160
  function message(txt) {
161
  var el = overlayRoot.querySelector(".pc-ar-msg");
162
  if (!el) {
 
169
  el.textContent = txt;
170
  }
171
 
172
+ // iOS Quick Look URL sans zoom
173
  function buildQuickLookHref(usdzUrl) {
174
  try {
175
  var u = new URL(usdzUrl, window.location.href);
 
183
  }
184
  }
185
 
186
+ // iOS : ancre rel=ar + <img> (c’est ce format qui déclenche Quick Look direct)
187
  function ensureQuickLookButton(USDZ_URL) {
188
  var btn = document.getElementById("ar-ios-quicklook-button");
189
  if (btn) return btn;
 
192
  anchor.id = "ar-ios-quicklook-button";
193
  anchor.setAttribute("rel", "ar");
194
  anchor.setAttribute("href", buildQuickLookHref(USDZ_URL));
195
+
196
+ // Bouton visuel en SVG embarqué, libellé "Lancer l’AR"
197
+ var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="240" height="60">' +
198
+ '<rect rx="14" ry="14" width="240" height="60" fill="black"/>' +
199
+ '<text x="120" y="37" font-size="18" text-anchor="middle" fill="white" font-family="system-ui, -apple-system, Segoe UI, Roboto">Lancer l’AR</text>' +
200
+ '</svg>';
201
+
202
+ var img = document.createElement("img");
203
+ img.alt = "Lancer l’AR";
204
+ img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
205
+
206
+ anchor.appendChild(img);
207
  document.body.appendChild(anchor);
208
  return anchor;
209
  }
210
 
211
+ // Android/Desktop : bouton “Lancer l’AR”
212
  function ensureARStartButton(onClick) {
213
  var btn = document.getElementById("ar-start-button");
214
  if (!btn) {
 
234
  var mountSel = scriptEl && scriptEl.getAttribute('data-mount');
235
  var desiredHeight = (scriptEl && scriptEl.getAttribute('data-height')) || '70vh';
236
 
 
237
  var mountEl = null;
238
  if (mountSel) mountEl = document.querySelector(mountSel);
239
  if (!mountEl) {
 
240
  mountEl = document.createElement('div');
241
  mountEl.id = 'ar-mount-fallback';
242
  document.body.insertBefore(mountEl, document.body.firstChild);
243
  }
244
 
 
245
  var mountStyle = mountEl.style;
246
  if (!mountStyle.position) mountStyle.position = 'relative';
247
  mountStyle.width = mountStyle.width || '100%';
 
249
  mountStyle.touchAction = mountStyle.touchAction || 'manipulation';
250
  mountStyle.webkitTapHighlightColor = 'transparent';
251
 
 
252
  var canvas = mountEl.querySelector('#application-canvas');
253
  if (!canvas) {
254
  canvas = document.createElement('canvas');
 
256
  mountEl.appendChild(canvas);
257
  }
258
 
 
259
  var cs = canvas.style;
260
  cs.position = 'absolute';
261
  cs.left = '0';
 
264
  cs.height = '100%';
265
  cs.display = 'block';
266
 
267
+ try { mountEl.scrollIntoView({ behavior: 'instant', block: 'start' }); } catch (_) {}
 
 
 
268
 
269
  return canvas;
270
  }
271
 
 
272
  function ensureRotationPanel() {
273
  var p = overlayRoot.querySelector(".ar-rotation-panel");
274
  if (p) return p;
 
299
  cfg.usdz_url :
300
  null;
301
 
302
+ // iOS → Quick Look via ancre rel=ar + <img> (ouverture directe de l’USDZ)
303
  if (isIOS()) {
304
  if (USDZ_URL) {
305
  ensureQuickLookButton(USDZ_URL);
 
306
  message("Modèle chargé. Appuyez sur « Lancer l’AR ».");
307
  } else {
308
  message("iOS détecté, mais aucun 'usdz_url' dans config.json.");
 
310
  return;
311
  }
312
 
313
+ // Android/Desktop → PlayCanvas/WebXR (bouton centré)
314
  try {
315
  await loadPlayCanvasRobust({ esmFirst: true, loadTimeoutMs: 15000 });
316
  } catch (e) {
 
327
  function initARApp(GLB_URL) {
328
  var pc = window.pc;
329
 
 
330
  var canvas = ensureCanvas();
331
  var rotationPanel = ensureRotationPanel();
332
  var rotTrack = rotationPanel.querySelector("#ar-rotation-track");
 
336
 
337
  window.focus();
338
 
 
339
  var app = new pc.Application(canvas, {
340
  mouse: new pc.Mouse(canvas),
341
  touch: new pc.TouchDevice(canvas),
 
351
  app.on("destroy", function () { window.removeEventListener("resize", onResize); });
352
  app.start();
353
 
354
+ // Rendu PBR “safe”
355
  app.scene.gammaCorrection = pc.GAMMA_SRGB;
356
  app.scene.toneMapping = pc.TONEMAP_ACES;
357
  app.scene.exposure = 1;
 
367
  light.setLocalEulerAngles(45, 30, 0);
368
  app.root.addChild(light);
369
 
370
+ // Réticule
371
  var reticleMat = new pc.StandardMaterial();
372
  reticleMat.diffuse = new pc.Color(0.2, 0.8, 1.0);
373
  reticleMat.opacity = 0.85;
 
380
  reticle.enabled = false;
381
  app.root.addChild(reticle);
382
 
383
+ // Modèle
384
  var modelRoot = new pc.Entity("ModelRoot");
385
  modelRoot.enabled = false;
386
  app.root.addChild(modelRoot);
387
  var modelLoaded = false, placedOnce = false;
388
 
389
+ // Ombre blob
390
  var blob = null;
391
  var BLOB_SIZE = 0.4;
392
  var BLOB_OFFSET_Y = 0.005;
 
441
  return e;
442
  }
443
 
444
+ // Rotation via slider
445
  var baseEulerX = 0, baseEulerZ = 0;
446
  var rotationYDeg = 0;
447
  function clamp360(d) { return Math.max(0, Math.min(360, d)); }
 
465
  if (rotLikePlane) blob.setRotation(rotLikePlane);
466
  }
467
 
468
+ // Chargement GLB
469
  app.assets.loadFromUrl(GLB_URL, "container", function (err, asset) {
470
  if (err) { console.error(err); message("Échec du chargement du modèle GLB."); return; }
471
  var instance = asset.resource.instantiateRenderEntity({ castShadows: true, receiveShadows: false });
472
  modelRoot.addChild(instance);
473
  modelRoot.setLocalScale(1, 1, 1);
474
 
 
475
  var renders = instance.findComponents('render');
476
  for (var ri = 0; ri < renders.length; ri++) {
477
  var r = renders[ri];
 
491
  baseEulerX = initE.x; baseEulerZ = initE.z;
492
 
493
  modelLoaded = true;
 
494
  message("Modèle chargé. Appuyez sur « Lancer l’AR ».");
495
  });
496
 
497
  if (!app.xr.supported) { message("WebXR n’est pas supporté sur cet appareil."); return; }
498
 
499
+ // Slider (pointer capture)
500
  var uiInteracting = false;
501
  var draggingTrack = false;
502
  var activePointerId = null;
 
507
  var y = (e.clientY != null) ? e.clientY : ((e.touches && e.touches[0] && e.touches[0].clientY) || 0);
508
  var ratio = (y - rect.top) / rect.height;
509
  var t = Math.max(0, Math.min(1, ratio));
510
+ return (1 - t) * 360;
511
  }
512
 
513
  function onPointerDownCapture(e) {
 
515
  uiInteracting = true;
516
  draggingTrack = true;
517
  activePointerId = (e.pointerId != null) ? e.pointerId : 1;
518
+ if (rotTrack.setPointerCapture) { try { rotTrack.setPointerCapture(activePointerId); } catch (er) {} }
 
 
519
  applyRotationY(degFromPointer(e));
520
+ e.preventDefault(); e.stopPropagation();
 
521
  }
522
  function onPointerMoveCapture(e) {
523
  if (!draggingTrack || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
524
  applyRotationY(degFromPointer(e));
525
+ e.preventDefault(); e.stopPropagation();
 
526
  }
527
  function endDrag(e) {
528
  if (!draggingTrack || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
529
+ draggingTrack = false; uiInteracting = false;
530
+ if (rotTrack.releasePointerCapture) { try { rotTrack.releasePointerCapture(activePointerId); } catch (er) {} }
 
 
 
531
  activePointerId = null;
532
+ e.preventDefault(); e.stopPropagation();
 
533
  }
534
 
535
  document.addEventListener("pointerdown", onPointerDownCapture, { capture: true, passive: false });
 
554
  });
555
  }
556
 
557
+ // Bouton start Android/Desktop
558
  var startBtn = ensureARStartButton(activateAR);
559
 
560
+ // Pas de démarrage par tap/clic sur le canvas
561
  app.keyboard.on("keydown", function (evt) { if (evt.key === pc.KEY_ESCAPE && app.xr.active) app.xr.end(); });
562
 
563
+ // Hit-test (plans quasi-horizontaux)
564
  var TMP_IN = new pc.Vec3(0, 1, 0), TMP_OUT = new pc.Vec3();
565
  function isHorizontalUpFacing(rot, minDot) {
566
  minDot = (typeof minDot === "number") ? minDot : 0.75;
 
568
  return TMP_OUT.y >= minDot;
569
  }
570
 
 
571
  app.xr.hitTest.on("available", function () {
572
  app.xr.hitTest.start({
573
  entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
 
584
  modelRoot.enabled = true;
585
  modelRoot.setPosition(pos);
586
 
 
587
  blob = createBlobShadowAt(pos, rot);
588
 
589
  var e = new pc.Vec3();
 
600
  });
601
  });
602
 
603
+ // Drag XR continu
604
  var isDragging = false;
605
  app.xr.input.on("add", function (inputSource) {
606
  inputSource.on("selectstart", function () {
 
664
  applyRotationY(v);
665
  }, { passive: true });
666
 
667
+ // Événements AR (feedback + visibilité bouton)
668
  app.xr.on("start", function () {
669
  if (startBtn) startBtn.style.display = "none";
670
  message("Session AR démarrée. Visez le sol pour détecter un plan…");