MikaFil commited on
Commit
6200bd0
·
verified ·
1 Parent(s): af14196

Update viewer_ar_ios.js

Browse files
Files changed (1) hide show
  1. viewer_ar_ios.js +64 -104
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)
4
- - Android/Desktop : WebXR AR (plans horizontaux uniquement) + 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,7 +11,6 @@
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 +18,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 +25,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 +57,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,43 +112,33 @@
118
  // ==========================
119
  // Overlay / UI (styles intégrés)
120
  // ==========================
121
- // NOTE: on renomme les ids/classes pour être plus explicites :
122
- // - #ar-overlay-root (ancien #xr-overlay-root)
123
- // - .ar-rotation-panel (ancien .ar-ui)
124
- // - #ar-rotation-track (ancien #ar-rotY-wrap)
125
- // - #ar-rotation-knob (ancien #ar-rotY-knob)
126
- // - #ar-rotation-range (ancien #ar-rotY)
127
- // - #ar-rotation-value (ancien #ar-rotY-val)
128
- // - #ar-ios-quicklook-button (ancien #ios-quicklook-btn)
129
-
130
  var css = [
131
- /* Conteneur overlay global (DOM Overlay WebXR + messages) */
132
  "#ar-overlay-root{position:fixed;inset:0;z-index:10001;pointer-events:none}",
133
 
134
- /* Toast (bas centre) utilisé pour la plupart des messages */
135
  ".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}",
136
-
137
- /* Variante centrée + agrandie (pour “Modèle chargé. Touchez l’écran…”) */
138
  ".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)}",
139
 
140
- /* ----- Panneau de rotation (ne pas modifier le texte 'Rotation') ----- */
 
 
 
 
 
 
 
141
  ".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}",
142
  ".ar-rotation-panel .label{font-size:12px;text-align:center;opacity:.95}",
143
  "#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}",
144
  ".rotY-rail{position:absolute;left:50%;transform:translateX(-50%);width:4px;height:100%;background:rgba(255,255,255,.35);border-radius:2px;pointer-events:none}",
145
  "#ar-rotation-knob{position:absolute;left:50%;width:22px;height:22px;border-radius:50%;background:#fff;box-shadow:0 2px 8px rgba(0,0,0,.35);transform:translate(-50%,-50%);top:50%;will-change:top;touch-action:none;pointer-events:none}",
146
  "input#ar-rotation-range.rotY{position:absolute;opacity:0;pointer-events:none;width:0;height:0}",
147
- "#ar-rotation-value{font-size:12px;opacity:.95}",
148
-
149
- /* ----- Bouton iOS Quick Look : centré et agrandi ----- */
150
- "#ar-ios-quicklook-button{position:fixed;left:50%;transform:translateX(-50%);bottom:72px;z-index:10003;display:inline-block;pointer-events:auto}",
151
- "#ar-ios-quicklook-button img{display:block;height:56px;width:auto}" // agrandi
152
  ].join("\n");
153
  var styleTag = document.createElement("style");
154
  styleTag.textContent = css;
155
  document.head.appendChild(styleTag);
156
 
157
- // Crée/récupère le conteneur d’overlay global
158
  function ensureOverlayRoot() {
159
  var r = document.getElementById("ar-overlay-root");
160
  if (!r) {
@@ -166,9 +150,7 @@
166
  }
167
  var overlayRoot = ensureOverlayRoot();
168
 
169
- // Système de message :
170
- // - message() = toast par défaut (en bas)
171
- // - si le texte correspond au message de démarrage, on centre + agrandit
172
  function message(txt) {
173
  var el = overlayRoot.querySelector(".pc-ar-msg");
174
  if (!el) {
@@ -176,13 +158,13 @@
176
  el.className = "pc-ar-msg";
177
  overlayRoot.appendChild(el);
178
  }
179
- // Style spécial si c’est le message de démarrage à agrandir/centrer
180
- var isStartMsg = /Mod[eè]le charg[é|e]\. Touchez l[’'`]écran pour d[ée]marrer l’AR\./i.test(txt);
181
  el.classList.toggle("pc-ar-msg--centerBig", isStartMsg);
182
  el.textContent = txt;
183
  }
184
 
185
- // Construit l’URL Quick Look empêchant le pinch-to-scale
186
  function buildQuickLookHref(usdzUrl) {
187
  try {
188
  var u = new URL(usdzUrl, window.location.href);
@@ -196,27 +178,37 @@
196
  }
197
  }
198
 
199
- // Crée le bouton iOS Quick Look (ancre rel=ar) centré/agrandi
200
  function ensureQuickLookButton(USDZ_URL) {
201
  var btn = document.getElementById("ar-ios-quicklook-button");
202
  if (btn) return btn;
203
-
204
  var anchor = document.createElement("a");
205
  anchor.id = "ar-ios-quicklook-button";
206
  anchor.setAttribute("rel", "ar");
207
  anchor.setAttribute("href", buildQuickLookHref(USDZ_URL));
208
-
209
- var img = document.createElement("img");
210
- img.alt = "Voir en AR";
211
- img.src =
212
- "data:image/svg+xml;charset=utf-8," +
213
- encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="200" height="56"><rect rx="12" ry="12" width="200" height="56" fill="black"/><text x="100" y="35" font-size="18" text-anchor="middle" fill="white" font-family="system-ui, -apple-system, Segoe UI, Roboto">Voir en AR</text></svg>');
214
-
215
- anchor.appendChild(img);
216
  document.body.appendChild(anchor);
217
  return anchor;
218
  }
219
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  // ==========================
221
  // Montage du canvas (Squarespace-friendly)
222
  // ==========================
@@ -225,17 +217,14 @@
225
  var mountSel = scriptEl && scriptEl.getAttribute('data-mount');
226
  var desiredHeight = (scriptEl && scriptEl.getAttribute('data-height')) || '70vh';
227
 
228
- // Trouve (ou fabrique) le conteneur d’accueil du canvas
229
  var mountEl = null;
230
  if (mountSel) mountEl = document.querySelector(mountSel);
231
  if (!mountEl) {
232
- // Fallback : placé en haut de <body> pour éviter d’être “sous le footer”
233
  mountEl = document.createElement('div');
234
  mountEl.id = 'ar-mount-fallback';
235
  document.body.insertBefore(mountEl, document.body.firstChild);
236
  }
237
 
238
- // Style de base du conteneur
239
  var mountStyle = mountEl.style;
240
  if (!mountStyle.position) mountStyle.position = 'relative';
241
  mountStyle.width = mountStyle.width || '100%';
@@ -243,7 +232,6 @@
243
  mountStyle.touchAction = mountStyle.touchAction || 'manipulation';
244
  mountStyle.webkitTapHighlightColor = 'transparent';
245
 
246
- // Crée/réutilise le canvas
247
  var canvas = mountEl.querySelector('#application-canvas');
248
  if (!canvas) {
249
  canvas = document.createElement('canvas');
@@ -251,7 +239,6 @@
251
  mountEl.appendChild(canvas);
252
  }
253
 
254
- // Le canvas remplit le conteneur
255
  var cs = canvas.style;
256
  cs.position = 'absolute';
257
  cs.left = '0';
@@ -260,15 +247,11 @@
260
  cs.height = '100%';
261
  cs.display = 'block';
262
 
263
- // Optionnel : faire défiler vers la zone AR
264
- try {
265
- mountEl.scrollIntoView({ behavior: 'instant', block: 'start' });
266
- } catch (_) {}
267
 
268
  return canvas;
269
  }
270
 
271
- // Panneau “Rotation” vertical (slider personnalisé)
272
  function ensureRotationPanel() {
273
  var p = overlayRoot.querySelector(".ar-rotation-panel");
274
  if (p) return p;
@@ -276,7 +259,7 @@
276
  p.className = "ar-rotation-panel";
277
  p.innerHTML =
278
  '<div class="label">Rotation</div>' +
279
- '<div id="ar-rotation-track" class="rotY-wrap">' + // (on garde la classe existante pour le rail)
280
  ' <div class="rotY-rail"></div>' +
281
  ' <div id="ar-rotation-knob" class="rotY-knob"></div>' +
282
  ' <input id="ar-rotation-range" class="rotY" type="range" min="0" max="360" step="1" value="0"/>' +
@@ -299,19 +282,18 @@
299
  cfg.usdz_url :
300
  null;
301
 
302
- // iOS → Quick Look
303
  if (isIOS()) {
304
  if (USDZ_URL) {
305
  ensureQuickLookButton(USDZ_URL);
306
- // Message centré + agrandi pour être bien visible au-dessus du bouton
307
- message("Modèle chargé. Touchez l’écran pour démarrer l’AR.");
308
  } else {
309
  message("iOS détecté, mais aucun 'usdz_url' dans config.json.");
310
  }
311
  return;
312
  }
313
 
314
- // Android/Desktop → PlayCanvas/WebXR
315
  try {
316
  await loadPlayCanvasRobust({ esmFirst: true, loadTimeoutMs: 15000 });
317
  } catch (e) {
@@ -328,7 +310,6 @@
328
  function initARApp(GLB_URL) {
329
  var pc = window.pc;
330
 
331
- // Canvas + panneau rotation
332
  var canvas = ensureCanvas();
333
  var rotationPanel = ensureRotationPanel();
334
  var rotTrack = rotationPanel.querySelector("#ar-rotation-track");
@@ -338,7 +319,6 @@
338
 
339
  window.focus();
340
 
341
- // Application PlayCanvas
342
  var app = new pc.Application(canvas, {
343
  mouse: new pc.Mouse(canvas),
344
  touch: new pc.TouchDevice(canvas),
@@ -354,13 +334,11 @@
354
  app.on("destroy", function () { window.removeEventListener("resize", onResize); });
355
  app.start();
356
 
357
- // Réglages de rendu PBR “safe”
358
  app.scene.gammaCorrection = pc.GAMMA_SRGB;
359
  app.scene.toneMapping = pc.TONEMAP_ACES;
360
  app.scene.exposure = 1;
361
  app.scene.ambientLight = new pc.Color(1, 1, 1);
362
 
363
- // Caméra + lumière
364
  var camera = new pc.Entity("Camera");
365
  camera.addComponent("camera", { clearColor: new pc.Color(0, 0, 0, 0), farClip: 10000 });
366
  app.root.addChild(camera);
@@ -370,7 +348,6 @@
370
  light.setLocalEulerAngles(45, 30, 0);
371
  app.root.addChild(light);
372
 
373
- // Réticule (visualise la cible du hit-test)
374
  var reticleMat = new pc.StandardMaterial();
375
  reticleMat.diffuse = new pc.Color(0.2, 0.8, 1.0);
376
  reticleMat.opacity = 0.85;
@@ -383,13 +360,11 @@
383
  reticle.enabled = false;
384
  app.root.addChild(reticle);
385
 
386
- // Conteneur du modèle chargé
387
  var modelRoot = new pc.Entity("ModelRoot");
388
  modelRoot.enabled = false;
389
  app.root.addChild(modelRoot);
390
  var modelLoaded = false, placedOnce = false;
391
 
392
- // Ombre de contact (blob “peinte” sur un plan)
393
  var blob = null;
394
  var BLOB_SIZE = 0.4;
395
  var BLOB_OFFSET_Y = 0.005;
@@ -444,7 +419,6 @@
444
  return e;
445
  }
446
 
447
- // Rotation via slider (0..360). On mémorise X/Z pour éviter les “flip”.
448
  var baseEulerX = 0, baseEulerZ = 0;
449
  var rotationYDeg = 0;
450
  function clamp360(d) { return Math.max(0, Math.min(360, d)); }
@@ -468,14 +442,12 @@
468
  if (rotLikePlane) blob.setRotation(rotLikePlane);
469
  }
470
 
471
- // Chargement du modèle GLB (depuis config.json)
472
  app.assets.loadFromUrl(GLB_URL, "container", function (err, asset) {
473
  if (err) { console.error(err); message("Échec du chargement du modèle GLB."); return; }
474
  var instance = asset.resource.instantiateRenderEntity({ castShadows: true, receiveShadows: false });
475
  modelRoot.addChild(instance);
476
  modelRoot.setLocalScale(1, 1, 1);
477
 
478
- // Ajustements simples matériaux (évite rendu trop sombre sans IBL)
479
  var renders = instance.findComponents('render');
480
  for (var ri = 0; ri < renders.length; ri++) {
481
  var r = renders[ri];
@@ -495,13 +467,12 @@
495
  baseEulerX = initE.x; baseEulerZ = initE.z;
496
 
497
  modelLoaded = true;
498
- // >>> Message agrandi et centré (comme demandé)
499
- message("Modèle chargé. Touchez l’écran pour démarrer l’AR.");
500
  });
501
 
502
  if (!app.xr.supported) { message("WebXR n’est pas supporté sur cet appareil."); return; }
503
 
504
- // Gestion fiable du slider (capture des pointer events)
505
  var uiInteracting = false;
506
  var draggingTrack = false;
507
  var activePointerId = null;
@@ -512,7 +483,7 @@
512
  var y = (e.clientY != null) ? e.clientY : ((e.touches && e.touches[0] && e.touches[0].clientY) || 0);
513
  var ratio = (y - rect.top) / rect.height;
514
  var t = Math.max(0, Math.min(1, ratio));
515
- return (1 - t) * 360; // 360 en haut, 0 en bas
516
  }
517
 
518
  function onPointerDownCapture(e) {
@@ -520,29 +491,21 @@
520
  uiInteracting = true;
521
  draggingTrack = true;
522
  activePointerId = (e.pointerId != null) ? e.pointerId : 1;
523
- if (rotTrack.setPointerCapture) {
524
- try { rotTrack.setPointerCapture(activePointerId); } catch (er) {}
525
- }
526
  applyRotationY(degFromPointer(e));
527
- e.preventDefault();
528
- e.stopPropagation();
529
  }
530
  function onPointerMoveCapture(e) {
531
  if (!draggingTrack || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
532
  applyRotationY(degFromPointer(e));
533
- e.preventDefault();
534
- e.stopPropagation();
535
  }
536
  function endDrag(e) {
537
  if (!draggingTrack || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
538
- draggingTrack = false;
539
- uiInteracting = false;
540
- if (rotTrack.releasePointerCapture) {
541
- try { rotTrack.releasePointerCapture(activePointerId); } catch (er) {}
542
- }
543
  activePointerId = null;
544
- e.preventDefault();
545
- e.stopPropagation();
546
  }
547
 
548
  document.addEventListener("pointerdown", onPointerDownCapture, { capture: true, passive: false });
@@ -550,7 +513,7 @@
550
  document.addEventListener("pointerup", endDrag, { capture: true, passive: false });
551
  document.addEventListener("pointercancel", endDrag, { capture: true, passive: false });
552
 
553
- // Démarrage AR (tap écran, hors slider)
554
  function activateAR() {
555
  if (!app.xr.isAvailable(pc.XRTYPE_AR)) { message("AR immersive indisponible sur cet appareil."); return; }
556
  if (!app.xr.domOverlay) app.xr.domOverlay = {};
@@ -566,17 +529,15 @@
566
  }
567
  });
568
  }
569
- app.mouse.on("mousedown", function () { if (!app.xr.active && !uiInteracting) activateAR(); });
570
- if (app.touch) {
571
- app.touch.on("touchend", function (evt) {
572
- if (!app.xr.active && !uiInteracting) activateAR();
573
- evt.event.preventDefault();
574
- evt.event.stopPropagation();
575
- });
576
- }
577
  app.keyboard.on("keydown", function (evt) { if (evt.key === pc.KEY_ESCAPE && app.xr.active) app.xr.end(); });
578
 
579
- // Hit-test : détecte uniquement des surfaces quasi-horizontales (normale ~ +Y)
580
  var TMP_IN = new pc.Vec3(0, 1, 0), TMP_OUT = new pc.Vec3();
581
  function isHorizontalUpFacing(rot, minDot) {
582
  minDot = (typeof minDot === "number") ? minDot : 0.75;
@@ -584,7 +545,6 @@
584
  return TMP_OUT.y >= minDot;
585
  }
586
 
587
- // Hit Test global (alimentation du réticule + premier placement)
588
  app.xr.hitTest.on("available", function () {
589
  app.xr.hitTest.start({
590
  entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
@@ -601,7 +561,6 @@
601
  modelRoot.enabled = true;
602
  modelRoot.setPosition(pos);
603
 
604
- // Ombre de contact au placement initial
605
  blob = createBlobShadowAt(pos, rot);
606
 
607
  var e = new pc.Vec3();
@@ -618,7 +577,7 @@
618
  });
619
  });
620
 
621
- // Déplacement XR continu (drag) — ignoré si UI active
622
  var isDragging = false;
623
  app.xr.input.on("add", function (inputSource) {
624
  inputSource.on("selectstart", function () {
@@ -645,7 +604,7 @@
645
  inputSource.on("selectend", function () { isDragging = false; });
646
  });
647
 
648
- // Desktop : rotation à la souris (clic droit ou Shift+clic gauche)
649
  var rotateMode = false, lastMouseX = 0;
650
  var ROTATE_SENSITIVITY = 0.25;
651
  app.mouse.on("mousedown", function (e) {
@@ -682,12 +641,14 @@
682
  applyRotationY(v);
683
  }, { passive: true });
684
 
685
- // Événements AR (feedback utilisateur)
686
  app.xr.on("start", function () {
 
687
  message("Session AR démarrée. Visez le sol pour détecter un plan…");
688
  reticle.enabled = true;
689
  });
690
  app.xr.on("end", function () {
 
691
  message("Session AR terminée.");
692
  reticle.enabled = false;
693
  isDragging = false;
@@ -697,7 +658,7 @@
697
  app.xr.on("available:" + pc.XRTYPE_AR, function (a) {
698
  if (!a) message("AR immersive indisponible.");
699
  else if (!app.xr.hitTest.supported) message("AR Hit Test non supporté.");
700
- else message(modelLoaded ? "Touchez l’écran pour démarrer l’AR." : "Chargement du modèle…");
701
  });
702
 
703
  if (!app.xr.isAvailable(pc.XRTYPE_AR)) message("AR immersive indisponible.");
@@ -705,4 +666,3 @@
705
  else message("Chargement du modèle…");
706
  }
707
  })();
708
-
 
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
  // Utilitaires généraux
12
  // ==========================
13
 
 
14
  function getCurrentScript() {
15
  return document.currentScript || (function () {
16
  var scripts = document.getElementsByTagName('script');
 
18
  })();
19
  }
20
 
 
21
  function findConfigUrl() {
22
  var el = getCurrentScript();
23
  if (!el) return null;
 
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
  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
  // ==========================
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%;transform:translateX(-50%);bottom:72px;z-index:10003;pointer-events:auto;border:none;border-radius:12px;padding:14px 20px;background:#000;color:#fff;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;font-size:16px;line-height:1;box-shadow:0 6px 20px rgba(0,0,0,.25);cursor:pointer}",
123
+ "#ar-start-button[disabled]{opacity:.5;cursor:not-allowed}",
124
+
125
+ /* ----- Bouton iOS Quick Look (ancre rel=ar, stylée comme un bouton) ----- */
126
+ "#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)}",
127
+
128
+ /* ----- Panneau rotation ----- */
129
  ".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}",
130
  ".ar-rotation-panel .label{font-size:12px;text-align:center;opacity:.95}",
131
  "#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}",
132
  ".rotY-rail{position:absolute;left:50%;transform:translateX(-50%);width:4px;height:100%;background:rgba(255,255,255,.35);border-radius:2px;pointer-events:none}",
133
  "#ar-rotation-knob{position:absolute;left:50%;width:22px;height:22px;border-radius:50%;background:#fff;box-shadow:0 2px 8px rgba(0,0,0,.35);transform:translate(-50%,-50%);top:50%;will-change:top;touch-action:none;pointer-events:none}",
134
  "input#ar-rotation-range.rotY{position:absolute;opacity:0;pointer-events:none;width:0;height:0}",
135
+ "#ar-rotation-value{font-size:12px;opacity:.95}"
 
 
 
 
136
  ].join("\n");
137
  var styleTag = document.createElement("style");
138
  styleTag.textContent = css;
139
  document.head.appendChild(styleTag);
140
 
141
+ // Overlay root
142
  function ensureOverlayRoot() {
143
  var r = document.getElementById("ar-overlay-root");
144
  if (!r) {
 
150
  }
151
  var overlayRoot = ensureOverlayRoot();
152
 
153
+ // Messages
 
 
154
  function message(txt) {
155
  var el = overlayRoot.querySelector(".pc-ar-msg");
156
  if (!el) {
 
158
  el.className = "pc-ar-msg";
159
  overlayRoot.appendChild(el);
160
  }
161
+ // On centre/agrandit si c’est l’invite de démarrage
162
+ var isStartMsg = /Appuyez sur .Lancer l.?AR./i.test(txt);
163
  el.classList.toggle("pc-ar-msg--centerBig", isStartMsg);
164
  el.textContent = txt;
165
  }
166
 
167
+ // Quick Look URL
168
  function buildQuickLookHref(usdzUrl) {
169
  try {
170
  var u = new URL(usdzUrl, window.location.href);
 
178
  }
179
  }
180
 
181
+ // iOS: bouton rel=ar “Lancer l’AR”
182
  function ensureQuickLookButton(USDZ_URL) {
183
  var btn = document.getElementById("ar-ios-quicklook-button");
184
  if (btn) return btn;
 
185
  var anchor = document.createElement("a");
186
  anchor.id = "ar-ios-quicklook-button";
187
  anchor.setAttribute("rel", "ar");
188
  anchor.setAttribute("href", buildQuickLookHref(USDZ_URL));
189
+ anchor.textContent = "Lancer l’AR";
 
 
 
 
 
 
 
190
  document.body.appendChild(anchor);
191
  return anchor;
192
  }
193
 
194
+ // Android/Desktop: bouton “Lancer l’AR”
195
+ function ensureARStartButton(onClick) {
196
+ var btn = document.getElementById("ar-start-button");
197
+ if (!btn) {
198
+ btn = document.createElement("button");
199
+ btn.id = "ar-start-button";
200
+ btn.type = "button";
201
+ btn.textContent = "Lancer l’AR";
202
+ btn.addEventListener("click", function (e) {
203
+ e.preventDefault();
204
+ e.stopPropagation();
205
+ if (typeof onClick === "function") onClick();
206
+ });
207
+ document.body.appendChild(btn);
208
+ }
209
+ return btn;
210
+ }
211
+
212
  // ==========================
213
  // Montage du canvas (Squarespace-friendly)
214
  // ==========================
 
217
  var mountSel = scriptEl && scriptEl.getAttribute('data-mount');
218
  var desiredHeight = (scriptEl && scriptEl.getAttribute('data-height')) || '70vh';
219
 
 
220
  var mountEl = null;
221
  if (mountSel) mountEl = document.querySelector(mountSel);
222
  if (!mountEl) {
 
223
  mountEl = document.createElement('div');
224
  mountEl.id = 'ar-mount-fallback';
225
  document.body.insertBefore(mountEl, document.body.firstChild);
226
  }
227
 
 
228
  var mountStyle = mountEl.style;
229
  if (!mountStyle.position) mountStyle.position = 'relative';
230
  mountStyle.width = mountStyle.width || '100%';
 
232
  mountStyle.touchAction = mountStyle.touchAction || 'manipulation';
233
  mountStyle.webkitTapHighlightColor = 'transparent';
234
 
 
235
  var canvas = mountEl.querySelector('#application-canvas');
236
  if (!canvas) {
237
  canvas = document.createElement('canvas');
 
239
  mountEl.appendChild(canvas);
240
  }
241
 
 
242
  var cs = canvas.style;
243
  cs.position = 'absolute';
244
  cs.left = '0';
 
247
  cs.height = '100%';
248
  cs.display = 'block';
249
 
250
+ try { mountEl.scrollIntoView({ behavior: 'instant', block: 'start' }); } catch (_) {}
 
 
 
251
 
252
  return canvas;
253
  }
254
 
 
255
  function ensureRotationPanel() {
256
  var p = overlayRoot.querySelector(".ar-rotation-panel");
257
  if (p) return p;
 
259
  p.className = "ar-rotation-panel";
260
  p.innerHTML =
261
  '<div class="label">Rotation</div>' +
262
+ '<div id="ar-rotation-track" class="rotY-wrap">' +
263
  ' <div class="rotY-rail"></div>' +
264
  ' <div id="ar-rotation-knob" class="rotY-knob"></div>' +
265
  ' <input id="ar-rotation-range" class="rotY" type="range" min="0" max="360" step="1" value="0"/>' +
 
282
  cfg.usdz_url :
283
  null;
284
 
285
+ // iOS → Quick Look via bouton
286
  if (isIOS()) {
287
  if (USDZ_URL) {
288
  ensureQuickLookButton(USDZ_URL);
289
+ message("Modèle chargé. Appuyez sur « Lancer l’AR ».");
 
290
  } else {
291
  message("iOS détecté, mais aucun 'usdz_url' dans config.json.");
292
  }
293
  return;
294
  }
295
 
296
+ // Android/Desktop → PlayCanvas/WebXR (avec bouton de démarrage)
297
  try {
298
  await loadPlayCanvasRobust({ esmFirst: true, loadTimeoutMs: 15000 });
299
  } catch (e) {
 
310
  function initARApp(GLB_URL) {
311
  var pc = window.pc;
312
 
 
313
  var canvas = ensureCanvas();
314
  var rotationPanel = ensureRotationPanel();
315
  var rotTrack = rotationPanel.querySelector("#ar-rotation-track");
 
319
 
320
  window.focus();
321
 
 
322
  var app = new pc.Application(canvas, {
323
  mouse: new pc.Mouse(canvas),
324
  touch: new pc.TouchDevice(canvas),
 
334
  app.on("destroy", function () { window.removeEventListener("resize", onResize); });
335
  app.start();
336
 
 
337
  app.scene.gammaCorrection = pc.GAMMA_SRGB;
338
  app.scene.toneMapping = pc.TONEMAP_ACES;
339
  app.scene.exposure = 1;
340
  app.scene.ambientLight = new pc.Color(1, 1, 1);
341
 
 
342
  var camera = new pc.Entity("Camera");
343
  camera.addComponent("camera", { clearColor: new pc.Color(0, 0, 0, 0), farClip: 10000 });
344
  app.root.addChild(camera);
 
348
  light.setLocalEulerAngles(45, 30, 0);
349
  app.root.addChild(light);
350
 
 
351
  var reticleMat = new pc.StandardMaterial();
352
  reticleMat.diffuse = new pc.Color(0.2, 0.8, 1.0);
353
  reticleMat.opacity = 0.85;
 
360
  reticle.enabled = false;
361
  app.root.addChild(reticle);
362
 
 
363
  var modelRoot = new pc.Entity("ModelRoot");
364
  modelRoot.enabled = false;
365
  app.root.addChild(modelRoot);
366
  var modelLoaded = false, placedOnce = false;
367
 
 
368
  var blob = null;
369
  var BLOB_SIZE = 0.4;
370
  var BLOB_OFFSET_Y = 0.005;
 
419
  return e;
420
  }
421
 
 
422
  var baseEulerX = 0, baseEulerZ = 0;
423
  var rotationYDeg = 0;
424
  function clamp360(d) { return Math.max(0, Math.min(360, d)); }
 
442
  if (rotLikePlane) blob.setRotation(rotLikePlane);
443
  }
444
 
 
445
  app.assets.loadFromUrl(GLB_URL, "container", function (err, asset) {
446
  if (err) { console.error(err); message("Échec du chargement du modèle GLB."); return; }
447
  var instance = asset.resource.instantiateRenderEntity({ castShadows: true, receiveShadows: false });
448
  modelRoot.addChild(instance);
449
  modelRoot.setLocalScale(1, 1, 1);
450
 
 
451
  var renders = instance.findComponents('render');
452
  for (var ri = 0; ri < renders.length; ri++) {
453
  var r = renders[ri];
 
467
  baseEulerX = initE.x; baseEulerZ = initE.z;
468
 
469
  modelLoaded = true;
470
+ message("Modèle chargé. Appuyez sur « Lancer l’AR ».");
 
471
  });
472
 
473
  if (!app.xr.supported) { message("WebXR n’est pas supporté sur cet appareil."); return; }
474
 
475
+ // ----- Slider (pointer capture) -----
476
  var uiInteracting = false;
477
  var draggingTrack = false;
478
  var activePointerId = null;
 
483
  var y = (e.clientY != null) ? e.clientY : ((e.touches && e.touches[0] && e.touches[0].clientY) || 0);
484
  var ratio = (y - rect.top) / rect.height;
485
  var t = Math.max(0, Math.min(1, ratio));
486
+ return (1 - t) * 360;
487
  }
488
 
489
  function onPointerDownCapture(e) {
 
491
  uiInteracting = true;
492
  draggingTrack = true;
493
  activePointerId = (e.pointerId != null) ? e.pointerId : 1;
494
+ if (rotTrack.setPointerCapture) { try { rotTrack.setPointerCapture(activePointerId); } catch (er) {} }
 
 
495
  applyRotationY(degFromPointer(e));
496
+ e.preventDefault(); e.stopPropagation();
 
497
  }
498
  function onPointerMoveCapture(e) {
499
  if (!draggingTrack || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
500
  applyRotationY(degFromPointer(e));
501
+ e.preventDefault(); e.stopPropagation();
 
502
  }
503
  function endDrag(e) {
504
  if (!draggingTrack || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
505
+ draggingTrack = false; uiInteracting = false;
506
+ if (rotTrack.releasePointerCapture) { try { rotTrack.releasePointerCapture(activePointerId); } catch (er) {} }
 
 
 
507
  activePointerId = null;
508
+ e.preventDefault(); e.stopPropagation();
 
509
  }
510
 
511
  document.addEventListener("pointerdown", onPointerDownCapture, { capture: true, passive: false });
 
513
  document.addEventListener("pointerup", endDrag, { capture: true, passive: false });
514
  document.addEventListener("pointercancel", endDrag, { capture: true, passive: false });
515
 
516
+ // ----- Démarrage AR UNIQUEMENT via bouton -----
517
  function activateAR() {
518
  if (!app.xr.isAvailable(pc.XRTYPE_AR)) { message("AR immersive indisponible sur cet appareil."); return; }
519
  if (!app.xr.domOverlay) app.xr.domOverlay = {};
 
529
  }
530
  });
531
  }
532
+
533
+ // Crée le bouton et relie le clic au démarrage AR
534
+ var startBtn = ensureARStartButton(activateAR);
535
+
536
+ // IMPORTANT : on supprime tout démarrage par clic/tap sur le canvas
537
+ // (AUCUN écouteur mousedown/touchend ne lance activateAR ici)
 
 
538
  app.keyboard.on("keydown", function (evt) { if (evt.key === pc.KEY_ESCAPE && app.xr.active) app.xr.end(); });
539
 
540
+ // ----- Hit-test -----
541
  var TMP_IN = new pc.Vec3(0, 1, 0), TMP_OUT = new pc.Vec3();
542
  function isHorizontalUpFacing(rot, minDot) {
543
  minDot = (typeof minDot === "number") ? minDot : 0.75;
 
545
  return TMP_OUT.y >= minDot;
546
  }
547
 
 
548
  app.xr.hitTest.on("available", function () {
549
  app.xr.hitTest.start({
550
  entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
 
561
  modelRoot.enabled = true;
562
  modelRoot.setPosition(pos);
563
 
 
564
  blob = createBlobShadowAt(pos, rot);
565
 
566
  var e = new pc.Vec3();
 
577
  });
578
  });
579
 
580
+ // Drag XR
581
  var isDragging = false;
582
  app.xr.input.on("add", function (inputSource) {
583
  inputSource.on("selectstart", function () {
 
604
  inputSource.on("selectend", function () { isDragging = false; });
605
  });
606
 
607
+ // Desktop rotation à la souris (clic droit ou Shift+clic gauche)
608
  var rotateMode = false, lastMouseX = 0;
609
  var ROTATE_SENSITIVITY = 0.25;
610
  app.mouse.on("mousedown", function (e) {
 
641
  applyRotationY(v);
642
  }, { passive: true });
643
 
644
+ // Événements AR (feedback + visibilité du bouton)
645
  app.xr.on("start", function () {
646
+ if (startBtn) startBtn.style.display = "none";
647
  message("Session AR démarrée. Visez le sol pour détecter un plan…");
648
  reticle.enabled = true;
649
  });
650
  app.xr.on("end", function () {
651
+ if (startBtn) startBtn.style.display = "";
652
  message("Session AR terminée.");
653
  reticle.enabled = false;
654
  isDragging = false;
 
658
  app.xr.on("available:" + pc.XRTYPE_AR, function (a) {
659
  if (!a) message("AR immersive indisponible.");
660
  else if (!app.xr.hitTest.supported) message("AR Hit Test non supporté.");
661
+ else message(modelLoaded ? "Appuyez sur « Lancer l’AR »." : "Chargement du modèle…");
662
  });
663
 
664
  if (!app.xr.isAvailable(pc.XRTYPE_AR)) message("AR immersive indisponible.");
 
666
  else message("Chargement du modèle…");
667
  }
668
  })();