MikaFil commited on
Commit
f400f1a
·
verified ·
1 Parent(s): 1877689

Update viewer_ar_ios.js

Browse files
Files changed (1) hide show
  1. viewer_ar_ios.js +168 -162
viewer_ar_ios.js CHANGED
@@ -1,55 +1,26 @@
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) + slider yaw + blob shadow
5
  - Éclairage PBR par défaut (sans WebXR light estimation)
6
- - Bouton "Lancer l’AR" (Android & iOS)
7
- - CSS EXTERNE via data-css, **attendue avant création de l’UI**
8
- - Montable dans un conteneur dédié via data-mount et data-height (Squarespace-friendly)
9
  */
10
 
11
  (function () {
12
- // ========== Helpers: script courant / CSS / plate-forme / config ==========
13
  function getCurrentScript() {
 
14
  return document.currentScript || (function () {
15
  var scripts = document.getElementsByTagName('script');
16
  return scripts[scripts.length - 1] || null;
17
  })();
18
  }
19
 
20
- // Injecte <link rel="stylesheet"> et **attend** son chargement
21
- function ensureCssLinkAsync(href) {
22
- return new Promise(function (resolve) {
23
- if (!href) return resolve();
24
-
25
- // Déjà injectée ?
26
- var links = document.querySelectorAll('link[rel="stylesheet"]');
27
- for (var i = 0; i < links.length; i++) {
28
- try {
29
- if (new URL(links[i].href, document.baseURI).href === new URL(href, document.baseURI).href) {
30
- // Déjà présente : on considère chargée
31
- return resolve();
32
- }
33
- } catch (_) {
34
- if (links[i].href === href) return resolve();
35
- }
36
- }
37
-
38
- var linkEl = document.createElement('link');
39
- linkEl.rel = 'stylesheet';
40
- linkEl.href = href;
41
- linkEl.onload = function () { resolve(); };
42
- linkEl.onerror = function () {
43
- console.warn('[viewer_ar_ios] CSS failed to load:', href);
44
- resolve(); // on continue quand même (pas de blocage)
45
- };
46
- document.head.appendChild(linkEl);
47
- });
48
- }
49
-
50
  function findConfigUrl() {
51
  var el = getCurrentScript();
52
- return el ? (el.getAttribute('data-config') || null) : null;
 
 
53
  }
54
 
55
  function isIOS() {
@@ -67,14 +38,15 @@
67
  try {
68
  var resp = await fetch(url, { cache: 'no-store' });
69
  if (!resp.ok) throw new Error("HTTP " + resp.status);
70
- return await resp.json();
 
71
  } catch (e) {
72
  console.error("Erreur chargement config.json:", e);
73
  return null;
74
  }
75
  }
76
 
77
- // ========== PlayCanvas (Android/Desktop) ==========
78
  var PC_VERSION = "2.11.7";
79
  var PC_URLS = {
80
  esm: ["https://cdn.jsdelivr.net/npm/playcanvas@" + PC_VERSION + "/build/playcanvas.min.mjs"],
@@ -94,7 +66,10 @@
94
  try {
95
  var mod = await Promise.race([import(/* @vite-ignore */ url), timeout(loadTimeoutMs)]);
96
  var ns = (mod && (mod.pc || mod["default"])) || mod;
97
- if (ns && ns.Application) { if (!window.pc) window.pc = ns; return window.pc; }
 
 
 
98
  } catch (e) { /* continue */ }
99
  }
100
  throw new Error("ESM failed");
@@ -130,22 +105,49 @@
130
  }
131
  }
132
 
133
- // ========== Overlay / UI générique ==========
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  function ensureOverlayRoot() {
135
  var r = document.getElementById("xr-overlay-root");
136
- if (!r) { r = document.createElement("div"); r.id = "xr-overlay-root"; document.body.appendChild(r); }
 
 
 
 
137
  return r;
138
  }
139
- var overlayRoot = null;
140
 
141
  function message(msg) {
142
- if (!overlayRoot) overlayRoot = ensureOverlayRoot();
143
  var el = overlayRoot.querySelector(".pc-ar-msg");
144
- if (!el) { el = document.createElement("div"); el.className = "pc-ar-msg"; overlayRoot.appendChild(el); }
 
 
 
 
145
  el.textContent = msg;
146
  }
147
 
148
- // iOS Quick Look URL (empêcher le zoom utilisateur)
149
  function buildQuickLookHref(usdzUrl) {
150
  try {
151
  var u = new URL(usdzUrl, window.location.href);
@@ -159,41 +161,76 @@
159
  }
160
  }
161
 
162
- // Canvas monté dans un conteneur choisi (Squarespace-friendly)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  function ensureCanvas() {
164
  var scriptEl = getCurrentScript();
165
  var mountSel = scriptEl && scriptEl.getAttribute('data-mount');
166
  var desiredHeight = (scriptEl && scriptEl.getAttribute('data-height')) || '70vh';
167
 
 
168
  var mountEl = null;
169
  if (mountSel) mountEl = document.querySelector(mountSel);
170
  if (!mountEl) {
 
171
  mountEl = document.createElement('div');
172
  mountEl.id = 'ar-mount-fallback';
173
  document.body.insertBefore(mountEl, document.body.firstChild);
174
  }
175
 
176
- var ms = mountEl.style;
177
- if (!ms.position) ms.position = 'relative';
178
- ms.width = ms.width || '100%';
179
- ms.minHeight = ms.minHeight || desiredHeight;
180
- ms.touchAction = ms.touchAction || 'manipulation';
181
- ms.webkitTapHighlightColor = 'transparent';
 
182
 
 
183
  var canvas = mountEl.querySelector('#application-canvas');
184
- if (!canvas) { canvas = document.createElement('canvas'); canvas.id = 'application-canvas'; mountEl.appendChild(canvas); }
 
 
 
 
185
 
 
186
  var cs = canvas.style;
187
- cs.position = 'absolute'; cs.left = '0'; cs.top = '0';
188
- cs.width = '100%'; cs.height = '100%'; cs.display = 'block';
189
-
190
- try { mountEl.scrollIntoView({ behavior: 'instant', block: 'start' }); } catch (_) {}
 
 
 
 
 
 
 
191
 
192
  return canvas;
193
  }
194
 
195
  function ensureSliderUI() {
196
- if (!overlayRoot) overlayRoot = ensureOverlayRoot();
197
  var p = overlayRoot.querySelector(".ar-ui");
198
  if (p) return p;
199
  p = document.createElement("div");
@@ -210,93 +247,41 @@
210
  return p;
211
  }
212
 
213
- // Bouton "Lancer l’AR" + (iOS) ancre rel=ar
214
- function ensureLaunchControls(USDZ_URL) {
215
- var btn = document.getElementById('ar-launch-btn');
216
- if (!btn) {
217
- btn = document.createElement('button');
218
- btn.id = 'ar-launch-btn';
219
- btn.type = 'button';
220
- btn.textContent = 'Lancer l’AR';
221
- btn.addEventListener('pointerdown', function (e) { e.stopPropagation(); }, { passive: true });
222
- btn.addEventListener('click', function (e) { e.stopPropagation(); }, false);
223
- document.body.appendChild(btn);
224
- }
225
-
226
- var anchor = document.getElementById('ios-quicklook-anchor');
227
- if (isIOS()) {
228
- if (!anchor) {
229
- anchor = document.createElement('a');
230
- anchor.id = 'ios-quicklook-anchor';
231
- anchor.setAttribute('rel', 'ar');
232
- document.body.appendChild(anchor);
233
- }
234
- if (USDZ_URL) anchor.setAttribute('href', buildQuickLookHref(USDZ_URL));
235
- }
236
- return { btn: btn, anchor: anchor || null };
237
- }
238
-
239
- // ========== Boot : charger **CSS**, puis config, puis brancher iOS/Android ==========
240
  (async function () {
241
- var scriptEl = getCurrentScript();
242
- var cssHref = (scriptEl && scriptEl.getAttribute('data-css'))
243
- || "https://huggingface.co/spaces/MikaFil/VR/resolve/main/css/style.css";
244
-
245
- // 1) Charger la CSS et ATTENDRE le onload avant toute création d'UI
246
- await ensureCssLinkAsync(cssHref);
247
-
248
- // 2) Lire config.json
249
  var cfgUrl = findConfigUrl();
250
  var cfg = await loadConfigJson(cfgUrl);
251
- var GLB_URL = (cfg && typeof cfg.glb_url === "string" && cfg.glb_url)
252
- ? cfg.glb_url
253
- : "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/AR/tests/danae_no_metallic.glb";
254
- var USDZ_URL = (cfg && typeof cfg.usdz_url === "string" && cfg.usdz_url) ? cfg.usdz_url : null;
 
 
255
 
256
- // 3) Construire les contrôles **après** CSS
257
- var controls = ensureLaunchControls(USDZ_URL);
258
- var launchBtn = controls.btn;
259
- var iosAnchor = controls.anchor;
260
-
261
- // 4) iOS → Quick Look (pas de WebXR)
262
  if (isIOS()) {
263
  if (USDZ_URL) {
264
- message("iOS détecté : appuyez sur « Lancer l’AR » pour ouvrir Quick Look.");
265
- launchBtn.onclick = function (e) {
266
- e.preventDefault(); e.stopPropagation();
267
- if (iosAnchor && iosAnchor.href) iosAnchor.click();
268
- };
269
  } else {
270
  message("iOS détecté, mais aucun 'usdz_url' dans config.json.");
271
- launchBtn.disabled = true; launchBtn.style.opacity = '0.5';
272
  }
273
  return;
274
  }
275
 
276
- // 5) Android/Desktop → PlayCanvas
277
  try {
278
  await loadPlayCanvasRobust({ esmFirst: true, loadTimeoutMs: 15000 });
279
  } catch (e) {
280
  console.error("Chargement PlayCanvas échoué ->", e);
281
  message("Impossible de charger PlayCanvas (réseau/CDN). Réessaie plus tard.");
282
- launchBtn.disabled = true; launchBtn.style.opacity = '0.5';
283
  return;
284
  }
285
-
286
- var activateARRef = { fn: null };
287
- initARApp(GLB_URL, activateARRef);
288
-
289
- launchBtn.onclick = function (e) {
290
- e.preventDefault(); e.stopPropagation();
291
- if (typeof activateARRef.fn === 'function') activateARRef.fn();
292
- };
293
  })();
294
 
295
- // ========== App WebXR (Android/Desktop) ==========
296
- function initARApp(GLB_URL, activateARRef) {
297
  var pc = window.pc;
298
  var canvas = ensureCanvas();
299
- overlayRoot = ensureOverlayRoot();
300
  var ui = ensureSliderUI();
301
  var rotWrap = ui.querySelector("#ar-rotY-wrap");
302
  var rotKnob = ui.querySelector("#ar-rotY-knob");
@@ -320,13 +305,13 @@
320
  app.on("destroy", function () { window.removeEventListener("resize", onResize); });
321
  app.start();
322
 
323
- // Rendu / PBR
324
  app.scene.gammaCorrection = pc.GAMMA_SRGB;
325
  app.scene.toneMapping = pc.TONEMAP_ACES;
326
  app.scene.exposure = 1;
327
  app.scene.ambientLight = new pc.Color(1, 1, 1);
328
 
329
- // Caméra + lumière directionnelle
330
  var camera = new pc.Entity("Camera");
331
  camera.addComponent("camera", { clearColor: new pc.Color(0, 0, 0, 0), farClip: 10000 });
332
  app.root.addChild(camera);
@@ -342,6 +327,7 @@
342
  reticleMat.opacity = 0.85;
343
  reticleMat.blendType = pc.BLEND_NORMAL;
344
  reticleMat.update();
 
345
  var reticle = new pc.Entity("Reticle");
346
  reticle.addComponent("render", { type: "torus", material: reticleMat });
347
  reticle.setLocalScale(0.12, 0.005, 0.12);
@@ -385,6 +371,7 @@
385
 
386
  function createBlobShadowAt(pos, rot) {
387
  var tex = makeBlobTexture(app, 256);
 
388
  var blobMat = new pc.StandardMaterial();
389
  blobMat.diffuse = new pc.Color(0, 0, 0);
390
  blobMat.opacity = 1.0;
@@ -399,19 +386,22 @@
399
  var e = new pc.Entity("BlobShadow");
400
  e.addComponent("render", { type: "plane", castShadows: false, receiveShadows: false });
401
  e.render.material = blobMat;
 
402
  e.setPosition(pos.x, pos.y + BLOB_OFFSET_Y, pos.z);
403
  e.setRotation(rot);
404
  e.setLocalScale(BLOB_SIZE, 1, BLOB_SIZE);
 
405
  app.root.addChild(e);
406
  return e;
407
  }
408
 
409
- // Euler de base
410
  var baseEulerX = 0, baseEulerZ = 0;
411
 
412
- // Rotation via slider
413
  var rotationYDeg = 0;
414
  function clamp360(d) { return Math.max(0, Math.min(360, d)); }
 
415
  function updateKnobFromY(yDeg) {
416
  var t = 1 - (yDeg / 360);
417
  rotKnob.style.top = String(t * 100) + "%";
@@ -431,13 +421,14 @@
431
  if (rotLikePlane) blob.setRotation(rotLikePlane);
432
  }
433
 
434
- // Chargement GLB
435
  app.assets.loadFromUrl(GLB_URL, "container", function (err, asset) {
436
  if (err) { console.error(err); message("Échec du chargement du modèle GLB."); return; }
437
  var instance = asset.resource.instantiateRenderEntity({ castShadows: true, receiveShadows: false });
438
  modelRoot.addChild(instance);
439
  modelRoot.setLocalScale(1, 1, 1);
440
 
 
441
  var renders = instance.findComponents('render');
442
  for (var ri = 0; ri < renders.length; ri++) {
443
  var r = renders[ri];
@@ -457,71 +448,87 @@
457
  baseEulerX = initE.x; baseEulerZ = initE.z;
458
 
459
  modelLoaded = true;
460
- message("Modèle chargé. Appuyez sur « Lancer l’AR » pour démarrer.");
461
  });
462
 
463
  if (!app.xr.supported) { message("WebXR n’est pas supporté sur cet appareil."); return; }
464
 
465
- // Slider fiable
466
- var uiInteracting = false, draggingWrap = false, activePointerId = null;
 
 
 
467
  function insideWrap(target) { return rotWrap.contains(target); }
468
  function degFromPointer(e) {
469
  var rect = rotWrap.getBoundingClientRect();
470
  var y = (e.clientY != null) ? e.clientY : ((e.touches && e.touches[0] && e.touches[0].clientY) || 0);
471
- var ratio = (y - rect.top) / rect.height; var t = Math.max(0, Math.min(1, ratio));
 
472
  return (1 - t) * 360;
473
  }
 
474
  function onPointerDownCapture(e) {
475
  if (!insideWrap(e.target)) return;
476
- uiInteracting = true; draggingWrap = true;
 
477
  activePointerId = (e.pointerId != null) ? e.pointerId : 1;
478
- if (rotWrap.setPointerCapture) { try { rotWrap.setPointerCapture(activePointerId); } catch (er) {} }
 
 
479
  applyRotationY(degFromPointer(e));
480
- e.preventDefault(); 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(); e.stopPropagation();
 
486
  }
487
  function endDrag(e) {
488
  if (!draggingWrap || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
489
- draggingWrap = false; uiInteracting = false;
490
- if (rotWrap.releasePointerCapture) { try { rotWrap.releasePointerCapture(activePointerId); } catch (er) {} }
 
 
 
491
  activePointerId = null;
492
- e.preventDefault(); e.stopPropagation();
 
493
  }
 
494
  document.addEventListener("pointerdown", onPointerDownCapture, { capture: true, passive: false });
495
  document.addEventListener("pointermove", onPointerMoveCapture, { capture: true, passive: false });
496
  document.addEventListener("pointerup", endDrag, { capture: true, passive: false });
497
  document.addEventListener("pointercancel", endDrag, { capture: true, passive: false });
498
 
499
- // Démarrage AR (exposé pour le bouton)
500
  function activateAR() {
501
  if (!app.xr.isAvailable(pc.XRTYPE_AR)) { message("AR immersive indisponible sur cet appareil."); return; }
502
  if (!app.xr.domOverlay) app.xr.domOverlay = {};
503
- app.xr.domOverlay.root = document.getElementById("xr-overlay-root") || ensureOverlayRoot();
504
  camera.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, {
505
  requiredFeatures: ["hit-test", "dom-overlay"],
506
  domOverlay: { root: app.xr.domOverlay.root },
507
  callback: function (err) {
508
- if (err) { console.error("Échec du démarrage AR :", err); message("Échec du démarrage AR : " + (err.message || err)); }
 
 
 
509
  }
510
  });
511
  }
512
- if (activateARRef && typeof activateARRef === 'object') activateARRef.fn = activateAR;
513
-
514
- // Tap écran (optionnel)
515
  app.mouse.on("mousedown", function () { if (!app.xr.active && !uiInteracting) activateAR(); });
516
  if (app.touch) {
517
  app.touch.on("touchend", function (evt) {
518
  if (!app.xr.active && !uiInteracting) activateAR();
519
- evt.event.preventDefault(); evt.event.stopPropagation();
 
520
  });
521
  }
522
  app.keyboard.on("keydown", function (evt) { if (evt.key === pc.KEY_ESCAPE && app.xr.active) app.xr.end(); });
523
 
524
- // Hit-test HORIZONTAL
525
  var TMP_IN = new pc.Vec3(0, 1, 0), TMP_OUT = new pc.Vec3();
526
  function isHorizontalUpFacing(rot, minDot) {
527
  minDot = (typeof minDot === "number") ? minDot : 0.75;
@@ -529,6 +536,7 @@
529
  return TMP_OUT.y >= minDot;
530
  }
531
 
 
532
  app.xr.hitTest.on("available", function () {
533
  app.xr.hitTest.start({
534
  entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
@@ -536,6 +544,7 @@
536
  if (err) { message("Le AR hit test n’a pas pu démarrer."); return; }
537
  hitSource.on("result", function (pos, rot) {
538
  if (!isHorizontalUpFacing(rot)) return;
 
539
  reticle.enabled = true;
540
  reticle.setPosition(pos);
541
  reticle.setRotation(rot);
@@ -544,9 +553,11 @@
544
  modelRoot.enabled = true;
545
  modelRoot.setPosition(pos);
546
 
 
547
  blob = createBlobShadowAt(pos, rot);
548
 
549
- var e = new pc.Vec3(); rot.getEulerAngles(e);
 
550
  var y0 = ((e.y % 360) + 360) % 360;
551
  applyRotationY(y0);
552
 
@@ -559,7 +570,7 @@
559
  });
560
  });
561
 
562
- // Déplacement XR
563
  var isDragging = false;
564
  app.xr.input.on("add", function (inputSource) {
565
  inputSource.on("selectstart", function () {
@@ -586,7 +597,7 @@
586
  inputSource.on("selectend", function () { isDragging = false; });
587
  });
588
 
589
- // Desktop : rotation souris
590
  var rotateMode = false, lastMouseX = 0;
591
  var ROTATE_SENSITIVITY = 0.25;
592
  app.mouse.on("mousedown", function (e) {
@@ -594,7 +605,8 @@
594
  if (e.button === 0 && !e.shiftKey) {
595
  isDragging = true;
596
  } else if (e.button === 2 || (e.button === 0 && e.shiftKey)) {
597
- rotateMode = true; lastMouseX = e.x;
 
598
  }
599
  });
600
  app.mouse.on("mousemove", function (e) {
@@ -606,7 +618,8 @@
606
  updateBlobPositionUnder(p, null);
607
  }
608
  } else if (rotateMode && modelRoot.enabled) {
609
- var dx = e.x - lastMouseX; lastMouseX = e.x;
 
610
  applyRotationY(rotationYDeg + dx * ROTATE_SENSITIVITY);
611
  }
612
  });
@@ -621,20 +634,13 @@
621
  applyRotationY(v);
622
  }, { passive: true });
623
 
624
- // Événements AR
625
- app.xr.on("start", function () {
626
- message("Session AR démarrée. Visez le sol pour détecter un plan…");
627
- reticle.enabled = true;
628
- });
629
- app.xr.on("end", function () {
630
- message("Session AR terminée.");
631
- reticle.enabled = false;
632
- isDragging = false; rotateMode = false; rotYInput.disabled = true;
633
- });
634
  app.xr.on("available:" + pc.XRTYPE_AR, function (a) {
635
  if (!a) message("AR immersive indisponible.");
636
  else if (!app.xr.hitTest.supported) message("AR Hit Test non supporté.");
637
- else message(modelLoaded ? "Touchez l’écran ou « Lancer l’AR »." : "Chargement du modèle…");
638
  });
639
 
640
  if (!app.xr.isAvailable(pc.XRTYPE_AR)) message("AR immersive indisponible.");
 
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;
22
+ var url = el.getAttribute('data-config');
23
+ return url || null;
24
  }
25
 
26
  function isIOS() {
 
38
  try {
39
  var resp = await fetch(url, { cache: 'no-store' });
40
  if (!resp.ok) throw new Error("HTTP " + resp.status);
41
+ var json = await resp.json();
42
+ return json;
43
  } catch (e) {
44
  console.error("Erreur chargement config.json:", e);
45
  return null;
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"],
 
66
  try {
67
  var mod = await Promise.race([import(/* @vite-ignore */ url), timeout(loadTimeoutMs)]);
68
  var ns = (mod && (mod.pc || mod["default"])) || mod;
69
+ if (ns && ns.Application) {
70
+ if (!window.pc) window.pc = ns;
71
+ return window.pc;
72
+ }
73
  } catch (e) { /* continue */ }
74
  }
75
  throw new Error("ESM failed");
 
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
  }
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
+
173
+ var img = document.createElement("img");
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%';
204
+ mountStyle.minHeight = mountStyle.minHeight || desiredHeight;
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');
212
+ canvas.id = 'application-canvas';
213
+ mountEl.appendChild(canvas);
214
+ }
215
 
216
+ // Le canvas remplit le conteneur
217
  var cs = canvas.style;
218
+ cs.position = 'absolute';
219
+ cs.left = '0';
220
+ cs.top = '0';
221
+ cs.width = '100%';
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 (_) {}
229
 
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");
 
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);
254
+ var GLB_URL = (cfg && typeof cfg.glb_url === "string" && cfg.glb_url) ?
255
+ cfg.glb_url :
256
+ "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/AR/tests/danae_no_metallic.glb";
257
+ var USDZ_URL = (cfg && typeof cfg.usdz_url === "string" && cfg.usdz_url) ?
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) {
274
  console.error("Chargement PlayCanvas échoué ->", e);
275
  message("Impossible de charger PlayCanvas (réseau/CDN). Réessaie plus tard.");
 
276
  return;
277
  }
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");
 
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);
 
327
  reticleMat.opacity = 0.85;
328
  reticleMat.blendType = pc.BLEND_NORMAL;
329
  reticleMat.update();
330
+
331
  var reticle = new pc.Entity("Reticle");
332
  reticle.addComponent("render", { type: "torus", material: reticleMat });
333
  reticle.setLocalScale(0.12, 0.005, 0.12);
 
371
 
372
  function createBlobShadowAt(pos, rot) {
373
  var tex = makeBlobTexture(app, 256);
374
+
375
  var blobMat = new pc.StandardMaterial();
376
  blobMat.diffuse = new pc.Color(0, 0, 0);
377
  blobMat.opacity = 1.0;
 
386
  var e = new pc.Entity("BlobShadow");
387
  e.addComponent("render", { type: "plane", castShadows: false, receiveShadows: false });
388
  e.render.material = blobMat;
389
+
390
  e.setPosition(pos.x, pos.y + BLOB_OFFSET_Y, pos.z);
391
  e.setRotation(rot);
392
  e.setLocalScale(BLOB_SIZE, 1, BLOB_SIZE);
393
+
394
  app.root.addChild(e);
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) + "%";
 
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
  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();
497
+ e.stopPropagation();
498
  }
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 },
513
  callback: function (err) {
514
+ if (err) {
515
+ console.error("Échec du démarrage AR :", err);
516
+ message("Échec du démarrage AR : " + (err.message || err));
517
+ }
518
  }
519
  });
520
  }
 
 
 
521
  app.mouse.on("mousedown", function () { if (!app.xr.active && !uiInteracting) activateAR(); });
522
  if (app.touch) {
523
  app.touch.on("touchend", function (evt) {
524
  if (!app.xr.active && !uiInteracting) activateAR();
525
+ evt.event.preventDefault();
526
+ evt.event.stopPropagation();
527
  });
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
  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],
 
544
  if (err) { message("Le AR hit test n’a pas pu démarrer."); return; }
545
  hitSource.on("result", function (pos, rot) {
546
  if (!isHorizontalUpFacing(rot)) return;
547
+
548
  reticle.enabled = true;
549
  reticle.setPosition(pos);
550
  reticle.setRotation(rot);
 
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();
560
+ rot.getEulerAngles(e);
561
  var y0 = ((e.y % 360) + 360) % 360;
562
  applyRotationY(y0);
563
 
 
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
  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) {
 
605
  if (e.button === 0 && !e.shiftKey) {
606
  isDragging = true;
607
  } else if (e.button === 2 || (e.button === 0 && e.shiftKey)) {
608
+ rotateMode = true;
609
+ lastMouseX = e.x;
610
  }
611
  });
612
  app.mouse.on("mousemove", function (e) {
 
618
  updateBlobPositionUnder(p, null);
619
  }
620
  } else if (rotateMode && modelRoot.enabled) {
621
+ var dx = e.x - lastMouseX;
622
+ lastMouseX = e.x;
623
  applyRotationY(rotationYDeg + dx * ROTATE_SENSITIVITY);
624
  }
625
  });
 
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é.");
643
+ else message(modelLoaded ? "Touchez l’écran pour démarrer l’AR." : "Chargement du modèle…");
644
  });
645
 
646
  if (!app.xr.isAvailable(pc.XRTYPE_AR)) message("AR immersive indisponible.");