MikaFil commited on
Commit
af14196
·
verified ·
1 Parent(s): 8672e41

Update viewer_ar_ios.js

Browse files
Files changed (1) hide show
  1. viewer_ar_ios.js +146 -88
viewer_ar_ios.js CHANGED
@@ -1,21 +1,25 @@
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 (horizontaux uniquement) + slider 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
  */
8
 
9
  (function () {
10
- // ============ Utils: script tag / config / platform ============
 
 
 
 
11
  function getCurrentScript() {
12
- // robuste même si currentScript n'est pas dispo
13
  return document.currentScript || (function () {
14
  var scripts = document.getElementsByTagName('script');
15
  return scripts[scripts.length - 1] || null;
16
  })();
17
  }
18
 
 
19
  function findConfigUrl() {
20
  var el = getCurrentScript();
21
  if (!el) return null;
@@ -23,16 +27,19 @@
23
  return url || null;
24
  }
25
 
 
26
  function isIOS() {
27
  return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
28
  }
29
 
 
30
  function timeout(ms) {
31
  return new Promise(function (_res, rej) {
32
  setTimeout(function () { rej(new Error("timeout")); }, ms);
33
  });
34
  }
35
 
 
36
  async function loadConfigJson(url) {
37
  if (!url) return null;
38
  try {
@@ -46,13 +53,16 @@
46
  }
47
  }
48
 
49
- // ============ PlayCanvas version fixée (Android/Desktop) ============
 
 
50
  var PC_VERSION = "2.11.7";
51
  var PC_URLS = {
52
  esm: ["https://cdn.jsdelivr.net/npm/playcanvas@" + PC_VERSION + "/build/playcanvas.min.mjs"],
53
  umd: ["https://cdn.jsdelivr.net/npm/playcanvas@" + PC_VERSION + "/build/playcanvas.min.js"]
54
  };
55
 
 
56
  async function loadPlayCanvasRobust(opts) {
57
  opts = opts || {};
58
  var esmFirst = (typeof opts.esmFirst === "boolean") ? opts.esmFirst : true;
@@ -105,49 +115,74 @@
105
  }
106
  }
107
 
108
- // ============ UI / Overlay commun ============
 
 
 
 
 
 
 
 
 
 
 
109
  var css = [
 
 
 
 
110
  ".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}",
111
- "#xr-overlay-root{position:fixed;inset:0;z-index:10001;pointer-events:none}",
112
 
113
- ".ar-ui{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}",
114
- ".ar-ui .label{font-size:12px;text-align:center;opacity:.95}",
115
- ".rotY-wrap{position:relative;width:48px;height:200px;display:flex;align-items:center;justify-content:center;touch-action:none;overflow:visible;pointer-events:auto}",
 
 
 
 
116
  ".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}",
117
- ".rotY-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}",
118
- ".ar-ui input[type=\"range\"].rotY{position:absolute;opacity:0;pointer-events:none;width:0;height:0}",
119
- ".ar-ui .val{font-size:12px;opacity:.95}",
120
 
121
- /* iOS Quick Look button */
122
- "#ios-quicklook-btn{position:fixed;left:50%;transform:translateX(-50%);bottom:16px;z-index:10003;display:inline-block;pointer-events:auto}",
123
- "#ios-quicklook-btn img{display:block;height:44px;width:auto}"
124
  ].join("\n");
125
  var styleTag = document.createElement("style");
126
  styleTag.textContent = css;
127
  document.head.appendChild(styleTag);
128
 
 
129
  function ensureOverlayRoot() {
130
- var r = document.getElementById("xr-overlay-root");
131
  if (!r) {
132
  r = document.createElement("div");
133
- r.id = "xr-overlay-root";
134
  document.body.appendChild(r);
135
  }
136
  return r;
137
  }
138
  var overlayRoot = ensureOverlayRoot();
139
 
140
- function message(msg) {
 
 
 
141
  var el = overlayRoot.querySelector(".pc-ar-msg");
142
  if (!el) {
143
  el = document.createElement("div");
144
  el.className = "pc-ar-msg";
145
  overlayRoot.appendChild(el);
146
  }
147
- el.textContent = msg;
 
 
 
148
  }
149
 
150
- // iOS Quick Look URL (empêche le pinch to scale)
151
  function buildQuickLookHref(usdzUrl) {
152
  try {
153
  var u = new URL(usdzUrl, window.location.href);
@@ -161,12 +196,13 @@
161
  }
162
  }
163
 
 
164
  function ensureQuickLookButton(USDZ_URL) {
165
- var btn = document.getElementById("ios-quicklook-btn");
166
  if (btn) return btn;
167
 
168
  var anchor = document.createElement("a");
169
- anchor.id = "ios-quicklook-btn";
170
  anchor.setAttribute("rel", "ar");
171
  anchor.setAttribute("href", buildQuickLookHref(USDZ_URL));
172
 
@@ -174,30 +210,32 @@
174
  img.alt = "Voir en AR";
175
  img.src =
176
  "data:image/svg+xml;charset=utf-8," +
177
- encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="160" height="44"><rect rx="8" ry="8" width="160" height="44" fill="black"/><text x="80" y="28" font-size="14" text-anchor="middle" fill="white" font-family="system-ui, -apple-system, Segoe UI, Roboto">Voir en AR</text></svg>');
178
 
179
  anchor.appendChild(img);
180
  document.body.appendChild(anchor);
181
  return anchor;
182
  }
183
 
184
- // ============ Canvas monté dans un conteneur contrôlé (Squarespace) ============
 
 
185
  function ensureCanvas() {
186
  var scriptEl = getCurrentScript();
187
  var mountSel = scriptEl && scriptEl.getAttribute('data-mount');
188
  var desiredHeight = (scriptEl && scriptEl.getAttribute('data-height')) || '70vh';
189
 
190
- // Trouve le conteneur cible
191
  var mountEl = null;
192
  if (mountSel) mountEl = document.querySelector(mountSel);
193
  if (!mountEl) {
194
- // fallback : tout en haut du body (pour éviter "sous le footer")
195
  mountEl = document.createElement('div');
196
  mountEl.id = 'ar-mount-fallback';
197
  document.body.insertBefore(mountEl, document.body.firstChild);
198
  }
199
 
200
- // Style du conteneur
201
  var mountStyle = mountEl.style;
202
  if (!mountStyle.position) mountStyle.position = 'relative';
203
  mountStyle.width = mountStyle.width || '100%';
@@ -205,7 +243,7 @@
205
  mountStyle.touchAction = mountStyle.touchAction || 'manipulation';
206
  mountStyle.webkitTapHighlightColor = 'transparent';
207
 
208
- // Réutilise ou crée le canvas
209
  var canvas = mountEl.querySelector('#application-canvas');
210
  if (!canvas) {
211
  canvas = document.createElement('canvas');
@@ -222,7 +260,7 @@
222
  cs.height = '100%';
223
  cs.display = 'block';
224
 
225
- // Optionnel : ramener le viewport sur le viewer
226
  try {
227
  mountEl.scrollIntoView({ behavior: 'instant', block: 'start' });
228
  } catch (_) {}
@@ -230,24 +268,27 @@
230
  return canvas;
231
  }
232
 
233
- function ensureSliderUI() {
234
- var p = overlayRoot.querySelector(".ar-ui");
 
235
  if (p) return p;
236
  p = document.createElement("div");
237
- p.className = "ar-ui";
238
  p.innerHTML =
239
  '<div class="label">Rotation</div>' +
240
- '<div class="rotY-wrap" id="ar-rotY-wrap">' +
241
  ' <div class="rotY-rail"></div>' +
242
- ' <div class="rotY-knob" id="ar-rotY-knob"></div>' +
243
- ' <input class="rotY" id="ar-rotY" type="range" min="0" max="360" step="1" value="0"/>' +
244
  '</div>' +
245
- '<div class="val" id="ar-rotY-val">0°</div>';
246
  overlayRoot.appendChild(p);
247
  return p;
248
  }
249
 
250
- // ============ Boot : charge config, route iOS vs Android/Desktop ============
 
 
251
  (async function () {
252
  var cfgUrl = findConfigUrl();
253
  var cfg = await loadConfigJson(cfgUrl);
@@ -258,16 +299,19 @@
258
  cfg.usdz_url :
259
  null;
260
 
 
261
  if (isIOS()) {
262
  if (USDZ_URL) {
263
  ensureQuickLookButton(USDZ_URL);
264
- message("iOS détecté : utilisez le bouton « Voir en AR » (AR Quick Look).");
 
265
  } else {
266
  message("iOS détecté, mais aucun 'usdz_url' dans config.json.");
267
  }
268
  return;
269
  }
270
 
 
271
  try {
272
  await loadPlayCanvasRobust({ esmFirst: true, loadTimeoutMs: 15000 });
273
  } catch (e) {
@@ -278,18 +322,23 @@
278
  initARApp(GLB_URL);
279
  })();
280
 
281
- // ============ App Android/Desktop (WebXR) ============
 
 
282
  function initARApp(GLB_URL) {
283
  var pc = window.pc;
 
 
284
  var canvas = ensureCanvas();
285
- var ui = ensureSliderUI();
286
- var rotWrap = ui.querySelector("#ar-rotY-wrap");
287
- var rotKnob = ui.querySelector("#ar-rotY-knob");
288
- var rotYInput = ui.querySelector("#ar-rotY");
289
- var rotYVal = ui.querySelector("#ar-rotY-val");
290
 
291
  window.focus();
292
 
 
293
  var app = new pc.Application(canvas, {
294
  mouse: new pc.Mouse(canvas),
295
  touch: new pc.TouchDevice(canvas),
@@ -305,13 +354,13 @@
305
  app.on("destroy", function () { window.removeEventListener("resize", onResize); });
306
  app.start();
307
 
308
- // Rendu / PBR defaults
309
  app.scene.gammaCorrection = pc.GAMMA_SRGB;
310
- app.scene.toneMapping = pc.TONEMAP_ACES;
311
- app.scene.exposure = 1;
312
- app.scene.ambientLight = new pc.Color(1, 1, 1);
313
 
314
- // Camera + lumière
315
  var camera = new pc.Entity("Camera");
316
  camera.addComponent("camera", { clearColor: new pc.Color(0, 0, 0, 0), farClip: 10000 });
317
  app.root.addChild(camera);
@@ -321,7 +370,7 @@
321
  light.setLocalEulerAngles(45, 30, 0);
322
  app.root.addChild(light);
323
 
324
- // Réticule
325
  var reticleMat = new pc.StandardMaterial();
326
  reticleMat.diffuse = new pc.Color(0.2, 0.8, 1.0);
327
  reticleMat.opacity = 0.85;
@@ -334,13 +383,13 @@
334
  reticle.enabled = false;
335
  app.root.addChild(reticle);
336
 
337
- // Modèle
338
  var modelRoot = new pc.Entity("ModelRoot");
339
  modelRoot.enabled = false;
340
  app.root.addChild(modelRoot);
341
  var modelLoaded = false, placedOnce = false;
342
 
343
- // ===== Blob Shadow =====
344
  var blob = null;
345
  var BLOB_SIZE = 0.4;
346
  var BLOB_OFFSET_Y = 0.005;
@@ -395,18 +444,16 @@
395
  return e;
396
  }
397
 
398
- // Euler de base (évite inversions)
399
  var baseEulerX = 0, baseEulerZ = 0;
400
-
401
- // Rotation via slider (0..360, 360 en haut / 0 en bas)
402
  var rotationYDeg = 0;
403
  function clamp360(d) { return Math.max(0, Math.min(360, d)); }
404
 
405
  function updateKnobFromY(yDeg) {
406
  var t = 1 - (yDeg / 360);
407
  rotKnob.style.top = String(t * 100) + "%";
408
- rotYInput.value = String(Math.round(yDeg));
409
- rotYVal.textContent = String(Math.round(yDeg)) + "°";
410
  }
411
  function applyRotationY(deg) {
412
  var y = clamp360(deg);
@@ -421,14 +468,14 @@
421
  if (rotLikePlane) blob.setRotation(rotLikePlane);
422
  }
423
 
424
- // Chargement GLB (depuis config.json)
425
  app.assets.loadFromUrl(GLB_URL, "container", function (err, asset) {
426
  if (err) { console.error(err); message("Échec du chargement du modèle GLB."); return; }
427
  var instance = asset.resource.instantiateRenderEntity({ castShadows: true, receiveShadows: false });
428
  modelRoot.addChild(instance);
429
  modelRoot.setLocalScale(1, 1, 1);
430
 
431
- // Fix matériaux simples
432
  var renders = instance.findComponents('render');
433
  for (var ri = 0; ri < renders.length; ri++) {
434
  var r = renders[ri];
@@ -448,49 +495,50 @@
448
  baseEulerX = initE.x; baseEulerZ = initE.z;
449
 
450
  modelLoaded = true;
 
451
  message("Modèle chargé. Touchez l’écran pour démarrer l’AR.");
452
  });
453
 
454
  if (!app.xr.supported) { message("WebXR n’est pas supporté sur cet appareil."); return; }
455
 
456
- // Slider fiable (pointer events en capture)
457
  var uiInteracting = false;
458
- var draggingWrap = false;
459
  var activePointerId = null;
460
 
461
- function insideWrap(target) { return rotWrap.contains(target); }
462
  function degFromPointer(e) {
463
- var rect = rotWrap.getBoundingClientRect();
464
  var y = (e.clientY != null) ? e.clientY : ((e.touches && e.touches[0] && e.touches[0].clientY) || 0);
465
  var ratio = (y - rect.top) / rect.height;
466
  var t = Math.max(0, Math.min(1, ratio));
467
- return (1 - t) * 360;
468
  }
469
 
470
  function onPointerDownCapture(e) {
471
- if (!insideWrap(e.target)) return;
472
  uiInteracting = true;
473
- draggingWrap = true;
474
  activePointerId = (e.pointerId != null) ? e.pointerId : 1;
475
- if (rotWrap.setPointerCapture) {
476
- try { rotWrap.setPointerCapture(activePointerId); } catch (er) {}
477
  }
478
  applyRotationY(degFromPointer(e));
479
  e.preventDefault();
480
  e.stopPropagation();
481
  }
482
  function onPointerMoveCapture(e) {
483
- if (!draggingWrap || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
484
  applyRotationY(degFromPointer(e));
485
  e.preventDefault();
486
  e.stopPropagation();
487
  }
488
  function endDrag(e) {
489
- if (!draggingWrap || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
490
- draggingWrap = false;
491
  uiInteracting = false;
492
- if (rotWrap.releasePointerCapture) {
493
- try { rotWrap.releasePointerCapture(activePointerId); } catch (er) {}
494
  }
495
  activePointerId = null;
496
  e.preventDefault();
@@ -499,14 +547,14 @@
499
 
500
  document.addEventListener("pointerdown", onPointerDownCapture, { capture: true, passive: false });
501
  document.addEventListener("pointermove", onPointerMoveCapture, { capture: true, passive: false });
502
- document.addEventListener("pointerup", endDrag, { capture: true, passive: false });
503
- document.addEventListener("pointercancel", endDrag, { capture: true, passive: false });
504
 
505
- // --- Démarrage AR (Android/Desktop)
506
  function activateAR() {
507
  if (!app.xr.isAvailable(pc.XRTYPE_AR)) { message("AR immersive indisponible sur cet appareil."); return; }
508
  if (!app.xr.domOverlay) app.xr.domOverlay = {};
509
- app.xr.domOverlay.root = document.getElementById("xr-overlay-root");
510
  camera.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, {
511
  requiredFeatures: ["hit-test", "dom-overlay"],
512
  domOverlay: { root: app.xr.domOverlay.root },
@@ -528,7 +576,7 @@
528
  }
529
  app.keyboard.on("keydown", function (evt) { if (evt.key === pc.KEY_ESCAPE && app.xr.active) app.xr.end(); });
530
 
531
- // Hit-test HORIZONTAL uniquement
532
  var TMP_IN = new pc.Vec3(0, 1, 0), TMP_OUT = new pc.Vec3();
533
  function isHorizontalUpFacing(rot, minDot) {
534
  minDot = (typeof minDot === "number") ? minDot : 0.75;
@@ -536,7 +584,7 @@
536
  return TMP_OUT.y >= minDot;
537
  }
538
 
539
- // Hit Test global
540
  app.xr.hitTest.on("available", function () {
541
  app.xr.hitTest.start({
542
  entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
@@ -553,7 +601,7 @@
553
  modelRoot.enabled = true;
554
  modelRoot.setPosition(pos);
555
 
556
- // Ombre de contact
557
  blob = createBlobShadowAt(pos, rot);
558
 
559
  var e = new pc.Vec3();
@@ -562,7 +610,7 @@
562
  applyRotationY(y0);
563
 
564
  placedOnce = true;
565
- rotYInput.disabled = false;
566
  message("Objet placé. Glissez pour déplacer, tournez-le avec le slider →");
567
  }
568
  });
@@ -570,7 +618,7 @@
570
  });
571
  });
572
 
573
- // Déplacement XR (drag) — ignoré si UI active
574
  var isDragging = false;
575
  app.xr.input.on("add", function (inputSource) {
576
  inputSource.on("selectstart", function () {
@@ -597,7 +645,7 @@
597
  inputSource.on("selectend", function () { isDragging = false; });
598
  });
599
 
600
- // Desktop : rotation souris (ignore si UI)
601
  var rotateMode = false, lastMouseX = 0;
602
  var ROTATE_SENSITIVITY = 0.25;
603
  app.mouse.on("mousedown", function (e) {
@@ -627,16 +675,25 @@
627
  window.addEventListener("contextmenu", function (e) { e.preventDefault(); });
628
 
629
  // Slider (accessibilité clavier)
630
- rotYInput.disabled = true;
631
- rotYInput.addEventListener("input", function (e) {
632
  if (!modelRoot.enabled) return;
633
  var v = parseFloat(e.target.value || "0");
634
  applyRotationY(v);
635
  }, { passive: true });
636
 
637
- // AR events
638
- app.xr.on("start", function () { message("Session AR démarrée. Visez le sol pour détecter un plan…"); reticle.enabled = true; });
639
- app.xr.on("end", function () { message("Session AR terminée."); reticle.enabled = false; isDragging = false; rotateMode = false; rotYInput.disabled = true; });
 
 
 
 
 
 
 
 
 
640
  app.xr.on("available:" + pc.XRTYPE_AR, function (a) {
641
  if (!a) message("AR immersive indisponible.");
642
  else if (!app.xr.hitTest.supported) message("AR Hit Test non supporté.");
@@ -648,3 +705,4 @@
648
  else message("Chargement du modèle…");
649
  }
650
  })();
 
 
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
  */
8
 
9
  (function () {
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');
18
  return scripts[scripts.length - 1] || null;
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 {
 
53
  }
54
  }
55
 
56
+ // ==========================
57
+ // PlayCanvas (Android/Desktop)
58
+ // ==========================
59
  var PC_VERSION = "2.11.7";
60
  var PC_URLS = {
61
  esm: ["https://cdn.jsdelivr.net/npm/playcanvas@" + PC_VERSION + "/build/playcanvas.min.mjs"],
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;
 
115
  }
116
  }
117
 
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) {
161
  r = document.createElement("div");
162
+ r.id = "ar-overlay-root";
163
  document.body.appendChild(r);
164
  }
165
  return r;
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) {
175
  el = document.createElement("div");
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
  }
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
 
 
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
+ // ==========================
223
  function ensureCanvas() {
224
  var scriptEl = getCurrentScript();
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
  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');
 
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 (_) {}
 
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;
275
  p = document.createElement("div");
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"/>' +
283
  '</div>' +
284
+ '<div id="ar-rotation-value" class="val">0°</div>';
285
  overlayRoot.appendChild(p);
286
  return p;
287
  }
288
 
289
+ // ==========================
290
+ // Démarrage (config → iOS vs Android/Desktop)
291
+ // ==========================
292
  (async function () {
293
  var cfgUrl = findConfigUrl();
294
  var cfg = await loadConfigJson(cfgUrl);
 
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) {
 
322
  initARApp(GLB_URL);
323
  })();
324
 
325
+ // ==========================
326
+ // Application WebXR (Android/Desktop)
327
+ // ==========================
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");
335
+ var rotKnob = rotationPanel.querySelector("#ar-rotation-knob");
336
+ var rotInput = rotationPanel.querySelector("#ar-rotation-range");
337
+ var rotVal = rotationPanel.querySelector("#ar-rotation-value");
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
  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
  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
  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
  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)); }
451
 
452
  function updateKnobFromY(yDeg) {
453
  var t = 1 - (yDeg / 360);
454
  rotKnob.style.top = String(t * 100) + "%";
455
+ rotInput.value = String(Math.round(yDeg));
456
+ rotVal.textContent = String(Math.round(yDeg)) + "°";
457
  }
458
  function applyRotationY(deg) {
459
  var y = clamp360(deg);
 
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
  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;
508
 
509
+ function insideTrack(target) { return rotTrack.contains(target); }
510
  function degFromPointer(e) {
511
+ var rect = rotTrack.getBoundingClientRect();
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) {
519
+ if (!insideTrack(e.target)) return;
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();
 
547
 
548
  document.addEventListener("pointerdown", onPointerDownCapture, { capture: true, passive: false });
549
  document.addEventListener("pointermove", onPointerMoveCapture, { capture: true, passive: false });
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 = {};
557
+ app.xr.domOverlay.root = document.getElementById("ar-overlay-root");
558
  camera.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, {
559
  requiredFeatures: ["hit-test", "dom-overlay"],
560
  domOverlay: { root: app.xr.domOverlay.root },
 
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
  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
  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();
 
610
  applyRotationY(y0);
611
 
612
  placedOnce = true;
613
+ rotInput.disabled = false;
614
  message("Objet placé. Glissez pour déplacer, tournez-le avec le slider →");
615
  }
616
  });
 
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
  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) {
 
675
  window.addEventListener("contextmenu", function (e) { e.preventDefault(); });
676
 
677
  // Slider (accessibilité clavier)
678
+ rotInput.disabled = true;
679
+ rotInput.addEventListener("input", function (e) {
680
  if (!modelRoot.enabled) return;
681
  var v = parseFloat(e.target.value || "0");
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;
694
+ rotateMode = false;
695
+ rotInput.disabled = true;
696
+ });
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é.");
 
705
  else message("Chargement du modèle…");
706
  }
707
  })();
708
+