Update viewer_ar_ios.js
Browse files- viewer_ar_ios.js +47 -69
viewer_ar_ios.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
/* viewer_ar_ios.js — AR PlayCanvas + GLB/USDZ via config.json
|
| 2 |
- Lit config.json (data-config) => { "glb_url": "...", "usdz_url": "..." }
|
| 3 |
-
- iOS : AR Quick Look (USDZ) avec #allowsContentScaling=0 (pas de zoom) + bouton "Lancer l’AR" (
|
| 4 |
- Android/Desktop : WebXR AR (plans horizontaux uniquement) + bouton "Lancer l’AR" (centré, pas de tap écran) + curseur de rotation (yaw) + ombre blob
|
| 5 |
- Éclairage PBR par défaut (sans WebXR light estimation)
|
| 6 |
- Monté dans un conteneur choisi (data-mount="#ar-mount") pour Squarespace
|
|
@@ -10,8 +10,6 @@
|
|
| 10 |
// ==========================
|
| 11 |
// Utilitaires généraux
|
| 12 |
// ==========================
|
| 13 |
-
|
| 14 |
-
// Récupère de façon robuste le <script> courant (compatible pages CMS)
|
| 15 |
function getCurrentScript() {
|
| 16 |
return document.currentScript || (function () {
|
| 17 |
var scripts = document.getElementsByTagName('script');
|
|
@@ -19,7 +17,6 @@
|
|
| 19 |
})();
|
| 20 |
}
|
| 21 |
|
| 22 |
-
// URL du fichier config.json passée via data-config sur le <script>
|
| 23 |
function findConfigUrl() {
|
| 24 |
var el = getCurrentScript();
|
| 25 |
if (!el) return null;
|
|
@@ -27,19 +24,16 @@
|
|
| 27 |
return url || null;
|
| 28 |
}
|
| 29 |
|
| 30 |
-
// Détection iOS (pour Quick Look)
|
| 31 |
function isIOS() {
|
| 32 |
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
| 33 |
}
|
| 34 |
|
| 35 |
-
// Petit helper timeout pour sécuriser des chargements
|
| 36 |
function timeout(ms) {
|
| 37 |
return new Promise(function (_res, rej) {
|
| 38 |
setTimeout(function () { rej(new Error("timeout")); }, ms);
|
| 39 |
});
|
| 40 |
}
|
| 41 |
|
| 42 |
-
// Charge config.json (chemins GLB/USDZ)
|
| 43 |
async function loadConfigJson(url) {
|
| 44 |
if (!url) return null;
|
| 45 |
try {
|
|
@@ -62,7 +56,6 @@
|
|
| 62 |
umd: ["https://cdn.jsdelivr.net/npm/playcanvas@" + PC_VERSION + "/build/playcanvas.min.js"]
|
| 63 |
};
|
| 64 |
|
| 65 |
-
// Charge PlayCanvas de façon robuste (ESM puis fallback UMD)
|
| 66 |
async function loadPlayCanvasRobust(opts) {
|
| 67 |
opts = opts || {};
|
| 68 |
var esmFirst = (typeof opts.esmFirst === "boolean") ? opts.esmFirst : true;
|
|
@@ -118,29 +111,29 @@
|
|
| 118 |
// ==========================
|
| 119 |
// Overlay / UI (styles intégrés)
|
| 120 |
// ==========================
|
| 121 |
-
// NOTE: ids/classes explicites :
|
| 122 |
// - #ar-overlay-root : overlay global (DOM overlay XR + toasts)
|
| 123 |
// - #ar-start-button : bouton Android/Desktop
|
| 124 |
-
// - #ar-ios-quicklook-button : bouton iOS (rel="ar")
|
| 125 |
// - .ar-rotation-panel : panneau rotation
|
| 126 |
-
// - #ar-rotation
|
| 127 |
|
| 128 |
var css = [
|
| 129 |
-
/* Conteneur overlay global
|
| 130 |
"#ar-overlay-root{position:fixed;inset:0;z-index:10001;pointer-events:none}",
|
| 131 |
|
| 132 |
-
/*
|
| 133 |
".pc-ar-msg{position:fixed;left:50%;transform:translateX(-50%);bottom:16px;z-index:10002;padding:10px 14px;background:rgba(0,0,0,.65);color:#fff;border-radius:12px;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;font-size:14px;line-height:1.3;text-align:center;max-width:min(90vw,640px);box-shadow:0 6px 20px rgba(0,0,0,.25);backdrop-filter:blur(4px);pointer-events:none}",
|
| 134 |
".pc-ar-msg.pc-ar-msg--centerBig{top:50%;bottom:auto;transform:translate(-50%,-50%);font-size:20px;padding:16px 22px;max-width:min(90vw,760px)}",
|
| 135 |
|
| 136 |
-
/* ----- Bouton
|
| 137 |
"#ar-start-button{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);z-index:10003;pointer-events:auto;border:none;border-radius:16px;padding:16px 24px;font-size:18px;line-height:1;background:#000;color:#fff;box-shadow:0 10px 28px rgba(0,0,0,.28);cursor:pointer;min-width:200px;text-align:center}",
|
| 138 |
"#ar-start-button[disabled]{opacity:.5;cursor:not-allowed}",
|
| 139 |
|
| 140 |
-
/* ----- Bouton iOS Quick Look
|
| 141 |
-
"#ar-ios-quicklook-button{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);z-index:10003;
|
|
|
|
| 142 |
|
| 143 |
-
/* ----- Panneau
|
| 144 |
".ar-rotation-panel{position:absolute;right:12px;top:50%;transform:translateY(-50%);background:rgba(0,0,0,0.55);color:#fff;padding:12px 10px;border-radius:16px;width:56px;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;pointer-events:auto;display:flex;flex-direction:column;align-items:center;gap:8px;box-shadow:0 6px 18px rgba(0,0,0,.25);backdrop-filter:blur(6px);touch-action:none}",
|
| 145 |
".ar-rotation-panel .label{font-size:12px;text-align:center;opacity:.95}",
|
| 146 |
"#ar-rotation-track{position:relative;width:48px;height:200px;display:flex;align-items:center;justify-content:center;touch-action:none;overflow:visible;pointer-events:auto}",
|
|
@@ -153,7 +146,6 @@
|
|
| 153 |
styleTag.textContent = css;
|
| 154 |
document.head.appendChild(styleTag);
|
| 155 |
|
| 156 |
-
// Crée/récupère le conteneur d’overlay global
|
| 157 |
function ensureOverlayRoot() {
|
| 158 |
var r = document.getElementById("ar-overlay-root");
|
| 159 |
if (!r) {
|
|
@@ -165,7 +157,6 @@
|
|
| 165 |
}
|
| 166 |
var overlayRoot = ensureOverlayRoot();
|
| 167 |
|
| 168 |
-
// Système de message (centre + agrandi si invite de départ)
|
| 169 |
function message(txt) {
|
| 170 |
var el = overlayRoot.querySelector(".pc-ar-msg");
|
| 171 |
if (!el) {
|
|
@@ -178,7 +169,7 @@
|
|
| 178 |
el.textContent = txt;
|
| 179 |
}
|
| 180 |
|
| 181 |
-
//
|
| 182 |
function buildQuickLookHref(usdzUrl) {
|
| 183 |
try {
|
| 184 |
var u = new URL(usdzUrl, window.location.href);
|
|
@@ -192,7 +183,7 @@
|
|
| 192 |
}
|
| 193 |
}
|
| 194 |
|
| 195 |
-
//
|
| 196 |
function ensureQuickLookButton(USDZ_URL) {
|
| 197 |
var btn = document.getElementById("ar-ios-quicklook-button");
|
| 198 |
if (btn) return btn;
|
|
@@ -201,12 +192,23 @@
|
|
| 201 |
anchor.id = "ar-ios-quicklook-button";
|
| 202 |
anchor.setAttribute("rel", "ar");
|
| 203 |
anchor.setAttribute("href", buildQuickLookHref(USDZ_URL));
|
| 204 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
document.body.appendChild(anchor);
|
| 206 |
return anchor;
|
| 207 |
}
|
| 208 |
|
| 209 |
-
// Android/Desktop : bouton “Lancer l’AR”
|
| 210 |
function ensureARStartButton(onClick) {
|
| 211 |
var btn = document.getElementById("ar-start-button");
|
| 212 |
if (!btn) {
|
|
@@ -232,17 +234,14 @@
|
|
| 232 |
var mountSel = scriptEl && scriptEl.getAttribute('data-mount');
|
| 233 |
var desiredHeight = (scriptEl && scriptEl.getAttribute('data-height')) || '70vh';
|
| 234 |
|
| 235 |
-
// Trouve (ou fabrique) le conteneur d’accueil du canvas
|
| 236 |
var mountEl = null;
|
| 237 |
if (mountSel) mountEl = document.querySelector(mountSel);
|
| 238 |
if (!mountEl) {
|
| 239 |
-
// Fallback : placé en haut de <body> pour éviter d’être “sous le footer”
|
| 240 |
mountEl = document.createElement('div');
|
| 241 |
mountEl.id = 'ar-mount-fallback';
|
| 242 |
document.body.insertBefore(mountEl, document.body.firstChild);
|
| 243 |
}
|
| 244 |
|
| 245 |
-
// Style de base du conteneur
|
| 246 |
var mountStyle = mountEl.style;
|
| 247 |
if (!mountStyle.position) mountStyle.position = 'relative';
|
| 248 |
mountStyle.width = mountStyle.width || '100%';
|
|
@@ -250,7 +249,6 @@
|
|
| 250 |
mountStyle.touchAction = mountStyle.touchAction || 'manipulation';
|
| 251 |
mountStyle.webkitTapHighlightColor = 'transparent';
|
| 252 |
|
| 253 |
-
// Crée/réutilise le canvas
|
| 254 |
var canvas = mountEl.querySelector('#application-canvas');
|
| 255 |
if (!canvas) {
|
| 256 |
canvas = document.createElement('canvas');
|
|
@@ -258,7 +256,6 @@
|
|
| 258 |
mountEl.appendChild(canvas);
|
| 259 |
}
|
| 260 |
|
| 261 |
-
// Le canvas remplit le conteneur
|
| 262 |
var cs = canvas.style;
|
| 263 |
cs.position = 'absolute';
|
| 264 |
cs.left = '0';
|
|
@@ -267,15 +264,11 @@
|
|
| 267 |
cs.height = '100%';
|
| 268 |
cs.display = 'block';
|
| 269 |
|
| 270 |
-
|
| 271 |
-
try {
|
| 272 |
-
mountEl.scrollIntoView({ behavior: 'instant', block: 'start' });
|
| 273 |
-
} catch (_) {}
|
| 274 |
|
| 275 |
return canvas;
|
| 276 |
}
|
| 277 |
|
| 278 |
-
// Panneau “Rotation” vertical (slider personnalisé)
|
| 279 |
function ensureRotationPanel() {
|
| 280 |
var p = overlayRoot.querySelector(".ar-rotation-panel");
|
| 281 |
if (p) return p;
|
|
@@ -306,11 +299,10 @@
|
|
| 306 |
cfg.usdz_url :
|
| 307 |
null;
|
| 308 |
|
| 309 |
-
// iOS → Quick Look (
|
| 310 |
if (isIOS()) {
|
| 311 |
if (USDZ_URL) {
|
| 312 |
ensureQuickLookButton(USDZ_URL);
|
| 313 |
-
// Message centré + agrandi pour être bien visible
|
| 314 |
message("Modèle chargé. Appuyez sur « Lancer l’AR ».");
|
| 315 |
} else {
|
| 316 |
message("iOS détecté, mais aucun 'usdz_url' dans config.json.");
|
|
@@ -318,7 +310,7 @@
|
|
| 318 |
return;
|
| 319 |
}
|
| 320 |
|
| 321 |
-
// Android/Desktop → PlayCanvas/WebXR (
|
| 322 |
try {
|
| 323 |
await loadPlayCanvasRobust({ esmFirst: true, loadTimeoutMs: 15000 });
|
| 324 |
} catch (e) {
|
|
@@ -335,7 +327,6 @@
|
|
| 335 |
function initARApp(GLB_URL) {
|
| 336 |
var pc = window.pc;
|
| 337 |
|
| 338 |
-
// Canvas + panneau rotation
|
| 339 |
var canvas = ensureCanvas();
|
| 340 |
var rotationPanel = ensureRotationPanel();
|
| 341 |
var rotTrack = rotationPanel.querySelector("#ar-rotation-track");
|
|
@@ -345,7 +336,6 @@
|
|
| 345 |
|
| 346 |
window.focus();
|
| 347 |
|
| 348 |
-
// Application PlayCanvas
|
| 349 |
var app = new pc.Application(canvas, {
|
| 350 |
mouse: new pc.Mouse(canvas),
|
| 351 |
touch: new pc.TouchDevice(canvas),
|
|
@@ -361,7 +351,7 @@
|
|
| 361 |
app.on("destroy", function () { window.removeEventListener("resize", onResize); });
|
| 362 |
app.start();
|
| 363 |
|
| 364 |
-
//
|
| 365 |
app.scene.gammaCorrection = pc.GAMMA_SRGB;
|
| 366 |
app.scene.toneMapping = pc.TONEMAP_ACES;
|
| 367 |
app.scene.exposure = 1;
|
|
@@ -377,7 +367,7 @@
|
|
| 377 |
light.setLocalEulerAngles(45, 30, 0);
|
| 378 |
app.root.addChild(light);
|
| 379 |
|
| 380 |
-
// Réticule
|
| 381 |
var reticleMat = new pc.StandardMaterial();
|
| 382 |
reticleMat.diffuse = new pc.Color(0.2, 0.8, 1.0);
|
| 383 |
reticleMat.opacity = 0.85;
|
|
@@ -390,13 +380,13 @@
|
|
| 390 |
reticle.enabled = false;
|
| 391 |
app.root.addChild(reticle);
|
| 392 |
|
| 393 |
-
//
|
| 394 |
var modelRoot = new pc.Entity("ModelRoot");
|
| 395 |
modelRoot.enabled = false;
|
| 396 |
app.root.addChild(modelRoot);
|
| 397 |
var modelLoaded = false, placedOnce = false;
|
| 398 |
|
| 399 |
-
// Ombre
|
| 400 |
var blob = null;
|
| 401 |
var BLOB_SIZE = 0.4;
|
| 402 |
var BLOB_OFFSET_Y = 0.005;
|
|
@@ -451,7 +441,7 @@
|
|
| 451 |
return e;
|
| 452 |
}
|
| 453 |
|
| 454 |
-
// Rotation via slider
|
| 455 |
var baseEulerX = 0, baseEulerZ = 0;
|
| 456 |
var rotationYDeg = 0;
|
| 457 |
function clamp360(d) { return Math.max(0, Math.min(360, d)); }
|
|
@@ -475,14 +465,13 @@
|
|
| 475 |
if (rotLikePlane) blob.setRotation(rotLikePlane);
|
| 476 |
}
|
| 477 |
|
| 478 |
-
// Chargement
|
| 479 |
app.assets.loadFromUrl(GLB_URL, "container", function (err, asset) {
|
| 480 |
if (err) { console.error(err); message("Échec du chargement du modèle GLB."); return; }
|
| 481 |
var instance = asset.resource.instantiateRenderEntity({ castShadows: true, receiveShadows: false });
|
| 482 |
modelRoot.addChild(instance);
|
| 483 |
modelRoot.setLocalScale(1, 1, 1);
|
| 484 |
|
| 485 |
-
// Ajustements simples matériaux (évite rendu trop sombre sans IBL)
|
| 486 |
var renders = instance.findComponents('render');
|
| 487 |
for (var ri = 0; ri < renders.length; ri++) {
|
| 488 |
var r = renders[ri];
|
|
@@ -502,13 +491,12 @@
|
|
| 502 |
baseEulerX = initE.x; baseEulerZ = initE.z;
|
| 503 |
|
| 504 |
modelLoaded = true;
|
| 505 |
-
// >>> Message de démarrage
|
| 506 |
message("Modèle chargé. Appuyez sur « Lancer l’AR ».");
|
| 507 |
});
|
| 508 |
|
| 509 |
if (!app.xr.supported) { message("WebXR n’est pas supporté sur cet appareil."); return; }
|
| 510 |
|
| 511 |
-
//
|
| 512 |
var uiInteracting = false;
|
| 513 |
var draggingTrack = false;
|
| 514 |
var activePointerId = null;
|
|
@@ -519,7 +507,7 @@
|
|
| 519 |
var y = (e.clientY != null) ? e.clientY : ((e.touches && e.touches[0] && e.touches[0].clientY) || 0);
|
| 520 |
var ratio = (y - rect.top) / rect.height;
|
| 521 |
var t = Math.max(0, Math.min(1, ratio));
|
| 522 |
-
return (1 - t) * 360;
|
| 523 |
}
|
| 524 |
|
| 525 |
function onPointerDownCapture(e) {
|
|
@@ -527,29 +515,21 @@
|
|
| 527 |
uiInteracting = true;
|
| 528 |
draggingTrack = true;
|
| 529 |
activePointerId = (e.pointerId != null) ? e.pointerId : 1;
|
| 530 |
-
if (rotTrack.setPointerCapture) {
|
| 531 |
-
try { rotTrack.setPointerCapture(activePointerId); } catch (er) {}
|
| 532 |
-
}
|
| 533 |
applyRotationY(degFromPointer(e));
|
| 534 |
-
e.preventDefault();
|
| 535 |
-
e.stopPropagation();
|
| 536 |
}
|
| 537 |
function onPointerMoveCapture(e) {
|
| 538 |
if (!draggingTrack || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
|
| 539 |
applyRotationY(degFromPointer(e));
|
| 540 |
-
e.preventDefault();
|
| 541 |
-
e.stopPropagation();
|
| 542 |
}
|
| 543 |
function endDrag(e) {
|
| 544 |
if (!draggingTrack || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
|
| 545 |
-
draggingTrack = false;
|
| 546 |
-
|
| 547 |
-
if (rotTrack.releasePointerCapture) {
|
| 548 |
-
try { rotTrack.releasePointerCapture(activePointerId); } catch (er) {}
|
| 549 |
-
}
|
| 550 |
activePointerId = null;
|
| 551 |
-
e.preventDefault();
|
| 552 |
-
e.stopPropagation();
|
| 553 |
}
|
| 554 |
|
| 555 |
document.addEventListener("pointerdown", onPointerDownCapture, { capture: true, passive: false });
|
|
@@ -574,13 +554,13 @@
|
|
| 574 |
});
|
| 575 |
}
|
| 576 |
|
| 577 |
-
//
|
| 578 |
var startBtn = ensureARStartButton(activateAR);
|
| 579 |
|
| 580 |
-
//
|
| 581 |
app.keyboard.on("keydown", function (evt) { if (evt.key === pc.KEY_ESCAPE && app.xr.active) app.xr.end(); });
|
| 582 |
|
| 583 |
-
// Hit-test
|
| 584 |
var TMP_IN = new pc.Vec3(0, 1, 0), TMP_OUT = new pc.Vec3();
|
| 585 |
function isHorizontalUpFacing(rot, minDot) {
|
| 586 |
minDot = (typeof minDot === "number") ? minDot : 0.75;
|
|
@@ -588,7 +568,6 @@
|
|
| 588 |
return TMP_OUT.y >= minDot;
|
| 589 |
}
|
| 590 |
|
| 591 |
-
// Hit Test global (alimentation du réticule + premier placement)
|
| 592 |
app.xr.hitTest.on("available", function () {
|
| 593 |
app.xr.hitTest.start({
|
| 594 |
entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
|
|
@@ -605,7 +584,6 @@
|
|
| 605 |
modelRoot.enabled = true;
|
| 606 |
modelRoot.setPosition(pos);
|
| 607 |
|
| 608 |
-
// Ombre de contact au placement initial
|
| 609 |
blob = createBlobShadowAt(pos, rot);
|
| 610 |
|
| 611 |
var e = new pc.Vec3();
|
|
@@ -622,7 +600,7 @@
|
|
| 622 |
});
|
| 623 |
});
|
| 624 |
|
| 625 |
-
//
|
| 626 |
var isDragging = false;
|
| 627 |
app.xr.input.on("add", function (inputSource) {
|
| 628 |
inputSource.on("selectstart", function () {
|
|
@@ -686,7 +664,7 @@
|
|
| 686 |
applyRotationY(v);
|
| 687 |
}, { passive: true });
|
| 688 |
|
| 689 |
-
// Événements AR (feedback + visibilité
|
| 690 |
app.xr.on("start", function () {
|
| 691 |
if (startBtn) startBtn.style.display = "none";
|
| 692 |
message("Session AR démarrée. Visez le sol pour détecter un plan…");
|
|
|
|
| 1 |
/* viewer_ar_ios.js — AR PlayCanvas + GLB/USDZ via config.json
|
| 2 |
- Lit config.json (data-config) => { "glb_url": "...", "usdz_url": "..." }
|
| 3 |
+
- iOS : AR Quick Look (USDZ) avec #allowsContentScaling=0 (pas de zoom) + bouton "Lancer l’AR" (centré, déclenche Quick Look direct)
|
| 4 |
- Android/Desktop : WebXR AR (plans horizontaux uniquement) + bouton "Lancer l’AR" (centré, pas de tap écran) + curseur de rotation (yaw) + ombre blob
|
| 5 |
- Éclairage PBR par défaut (sans WebXR light estimation)
|
| 6 |
- Monté dans un conteneur choisi (data-mount="#ar-mount") pour Squarespace
|
|
|
|
| 10 |
// ==========================
|
| 11 |
// Utilitaires généraux
|
| 12 |
// ==========================
|
|
|
|
|
|
|
| 13 |
function getCurrentScript() {
|
| 14 |
return document.currentScript || (function () {
|
| 15 |
var scripts = document.getElementsByTagName('script');
|
|
|
|
| 17 |
})();
|
| 18 |
}
|
| 19 |
|
|
|
|
| 20 |
function findConfigUrl() {
|
| 21 |
var el = getCurrentScript();
|
| 22 |
if (!el) return null;
|
|
|
|
| 24 |
return url || null;
|
| 25 |
}
|
| 26 |
|
|
|
|
| 27 |
function isIOS() {
|
| 28 |
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
| 29 |
}
|
| 30 |
|
|
|
|
| 31 |
function timeout(ms) {
|
| 32 |
return new Promise(function (_res, rej) {
|
| 33 |
setTimeout(function () { rej(new Error("timeout")); }, ms);
|
| 34 |
});
|
| 35 |
}
|
| 36 |
|
|
|
|
| 37 |
async function loadConfigJson(url) {
|
| 38 |
if (!url) return null;
|
| 39 |
try {
|
|
|
|
| 56 |
umd: ["https://cdn.jsdelivr.net/npm/playcanvas@" + PC_VERSION + "/build/playcanvas.min.js"]
|
| 57 |
};
|
| 58 |
|
|
|
|
| 59 |
async function loadPlayCanvasRobust(opts) {
|
| 60 |
opts = opts || {};
|
| 61 |
var esmFirst = (typeof opts.esmFirst === "boolean") ? opts.esmFirst : true;
|
|
|
|
| 111 |
// ==========================
|
| 112 |
// Overlay / UI (styles intégrés)
|
| 113 |
// ==========================
|
|
|
|
| 114 |
// - #ar-overlay-root : overlay global (DOM overlay XR + toasts)
|
| 115 |
// - #ar-start-button : bouton Android/Desktop
|
| 116 |
+
// - #ar-ios-quicklook-button : bouton iOS (rel="ar" + <img> requis pour Quick Look)
|
| 117 |
// - .ar-rotation-panel : panneau rotation
|
| 118 |
+
// - #ar-rotation-* : slider yaw
|
| 119 |
|
| 120 |
var css = [
|
| 121 |
+
/* Conteneur overlay global */
|
| 122 |
"#ar-overlay-root{position:fixed;inset:0;z-index:10001;pointer-events:none}",
|
| 123 |
|
| 124 |
+
/* Toasts */
|
| 125 |
".pc-ar-msg{position:fixed;left:50%;transform:translateX(-50%);bottom:16px;z-index:10002;padding:10px 14px;background:rgba(0,0,0,.65);color:#fff;border-radius:12px;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;font-size:14px;line-height:1.3;text-align:center;max-width:min(90vw,640px);box-shadow:0 6px 20px rgba(0,0,0,.25);backdrop-filter:blur(4px);pointer-events:none}",
|
| 126 |
".pc-ar-msg.pc-ar-msg--centerBig{top:50%;bottom:auto;transform:translate(-50%,-50%);font-size:20px;padding:16px 22px;max-width:min(90vw,760px)}",
|
| 127 |
|
| 128 |
+
/* ----- Bouton Android/Desktop : centré + légèrement plus grand ----- */
|
| 129 |
"#ar-start-button{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);z-index:10003;pointer-events:auto;border:none;border-radius:16px;padding:16px 24px;font-size:18px;line-height:1;background:#000;color:#fff;box-shadow:0 10px 28px rgba(0,0,0,.28);cursor:pointer;min-width:200px;text-align:center}",
|
| 130 |
"#ar-start-button[disabled]{opacity:.5;cursor:not-allowed}",
|
| 131 |
|
| 132 |
+
/* ----- Bouton iOS Quick Look : centré + <img> (exigé par Quick Look) ----- */
|
| 133 |
+
"#ar-ios-quicklook-button{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);z-index:10003;pointer-events:auto;display:inline-block;text-decoration:none}",
|
| 134 |
+
"#ar-ios-quicklook-button img{display:block;height:60px;width:auto;box-shadow:0 10px 28px rgba(0,0,0,.28);border-radius:14px}",
|
| 135 |
|
| 136 |
+
/* ----- Panneau rotation ----- */
|
| 137 |
".ar-rotation-panel{position:absolute;right:12px;top:50%;transform:translateY(-50%);background:rgba(0,0,0,0.55);color:#fff;padding:12px 10px;border-radius:16px;width:56px;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;pointer-events:auto;display:flex;flex-direction:column;align-items:center;gap:8px;box-shadow:0 6px 18px rgba(0,0,0,.25);backdrop-filter:blur(6px);touch-action:none}",
|
| 138 |
".ar-rotation-panel .label{font-size:12px;text-align:center;opacity:.95}",
|
| 139 |
"#ar-rotation-track{position:relative;width:48px;height:200px;display:flex;align-items:center;justify-content:center;touch-action:none;overflow:visible;pointer-events:auto}",
|
|
|
|
| 146 |
styleTag.textContent = css;
|
| 147 |
document.head.appendChild(styleTag);
|
| 148 |
|
|
|
|
| 149 |
function ensureOverlayRoot() {
|
| 150 |
var r = document.getElementById("ar-overlay-root");
|
| 151 |
if (!r) {
|
|
|
|
| 157 |
}
|
| 158 |
var overlayRoot = ensureOverlayRoot();
|
| 159 |
|
|
|
|
| 160 |
function message(txt) {
|
| 161 |
var el = overlayRoot.querySelector(".pc-ar-msg");
|
| 162 |
if (!el) {
|
|
|
|
| 169 |
el.textContent = txt;
|
| 170 |
}
|
| 171 |
|
| 172 |
+
// iOS Quick Look URL sans zoom
|
| 173 |
function buildQuickLookHref(usdzUrl) {
|
| 174 |
try {
|
| 175 |
var u = new URL(usdzUrl, window.location.href);
|
|
|
|
| 183 |
}
|
| 184 |
}
|
| 185 |
|
| 186 |
+
// iOS : ancre rel=ar + <img> (c’est ce format qui déclenche Quick Look direct)
|
| 187 |
function ensureQuickLookButton(USDZ_URL) {
|
| 188 |
var btn = document.getElementById("ar-ios-quicklook-button");
|
| 189 |
if (btn) return btn;
|
|
|
|
| 192 |
anchor.id = "ar-ios-quicklook-button";
|
| 193 |
anchor.setAttribute("rel", "ar");
|
| 194 |
anchor.setAttribute("href", buildQuickLookHref(USDZ_URL));
|
| 195 |
+
|
| 196 |
+
// Bouton visuel en SVG embarqué, libellé "Lancer l’AR"
|
| 197 |
+
var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="240" height="60">' +
|
| 198 |
+
'<rect rx="14" ry="14" width="240" height="60" fill="black"/>' +
|
| 199 |
+
'<text x="120" y="37" font-size="18" text-anchor="middle" fill="white" font-family="system-ui, -apple-system, Segoe UI, Roboto">Lancer l’AR</text>' +
|
| 200 |
+
'</svg>';
|
| 201 |
+
|
| 202 |
+
var img = document.createElement("img");
|
| 203 |
+
img.alt = "Lancer l’AR";
|
| 204 |
+
img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
|
| 205 |
+
|
| 206 |
+
anchor.appendChild(img);
|
| 207 |
document.body.appendChild(anchor);
|
| 208 |
return anchor;
|
| 209 |
}
|
| 210 |
|
| 211 |
+
// Android/Desktop : bouton “Lancer l’AR”
|
| 212 |
function ensureARStartButton(onClick) {
|
| 213 |
var btn = document.getElementById("ar-start-button");
|
| 214 |
if (!btn) {
|
|
|
|
| 234 |
var mountSel = scriptEl && scriptEl.getAttribute('data-mount');
|
| 235 |
var desiredHeight = (scriptEl && scriptEl.getAttribute('data-height')) || '70vh';
|
| 236 |
|
|
|
|
| 237 |
var mountEl = null;
|
| 238 |
if (mountSel) mountEl = document.querySelector(mountSel);
|
| 239 |
if (!mountEl) {
|
|
|
|
| 240 |
mountEl = document.createElement('div');
|
| 241 |
mountEl.id = 'ar-mount-fallback';
|
| 242 |
document.body.insertBefore(mountEl, document.body.firstChild);
|
| 243 |
}
|
| 244 |
|
|
|
|
| 245 |
var mountStyle = mountEl.style;
|
| 246 |
if (!mountStyle.position) mountStyle.position = 'relative';
|
| 247 |
mountStyle.width = mountStyle.width || '100%';
|
|
|
|
| 249 |
mountStyle.touchAction = mountStyle.touchAction || 'manipulation';
|
| 250 |
mountStyle.webkitTapHighlightColor = 'transparent';
|
| 251 |
|
|
|
|
| 252 |
var canvas = mountEl.querySelector('#application-canvas');
|
| 253 |
if (!canvas) {
|
| 254 |
canvas = document.createElement('canvas');
|
|
|
|
| 256 |
mountEl.appendChild(canvas);
|
| 257 |
}
|
| 258 |
|
|
|
|
| 259 |
var cs = canvas.style;
|
| 260 |
cs.position = 'absolute';
|
| 261 |
cs.left = '0';
|
|
|
|
| 264 |
cs.height = '100%';
|
| 265 |
cs.display = 'block';
|
| 266 |
|
| 267 |
+
try { mountEl.scrollIntoView({ behavior: 'instant', block: 'start' }); } catch (_) {}
|
|
|
|
|
|
|
|
|
|
| 268 |
|
| 269 |
return canvas;
|
| 270 |
}
|
| 271 |
|
|
|
|
| 272 |
function ensureRotationPanel() {
|
| 273 |
var p = overlayRoot.querySelector(".ar-rotation-panel");
|
| 274 |
if (p) return p;
|
|
|
|
| 299 |
cfg.usdz_url :
|
| 300 |
null;
|
| 301 |
|
| 302 |
+
// iOS → Quick Look via ancre rel=ar + <img> (ouverture directe de l’USDZ)
|
| 303 |
if (isIOS()) {
|
| 304 |
if (USDZ_URL) {
|
| 305 |
ensureQuickLookButton(USDZ_URL);
|
|
|
|
| 306 |
message("Modèle chargé. Appuyez sur « Lancer l’AR ».");
|
| 307 |
} else {
|
| 308 |
message("iOS détecté, mais aucun 'usdz_url' dans config.json.");
|
|
|
|
| 310 |
return;
|
| 311 |
}
|
| 312 |
|
| 313 |
+
// Android/Desktop → PlayCanvas/WebXR (bouton centré)
|
| 314 |
try {
|
| 315 |
await loadPlayCanvasRobust({ esmFirst: true, loadTimeoutMs: 15000 });
|
| 316 |
} catch (e) {
|
|
|
|
| 327 |
function initARApp(GLB_URL) {
|
| 328 |
var pc = window.pc;
|
| 329 |
|
|
|
|
| 330 |
var canvas = ensureCanvas();
|
| 331 |
var rotationPanel = ensureRotationPanel();
|
| 332 |
var rotTrack = rotationPanel.querySelector("#ar-rotation-track");
|
|
|
|
| 336 |
|
| 337 |
window.focus();
|
| 338 |
|
|
|
|
| 339 |
var app = new pc.Application(canvas, {
|
| 340 |
mouse: new pc.Mouse(canvas),
|
| 341 |
touch: new pc.TouchDevice(canvas),
|
|
|
|
| 351 |
app.on("destroy", function () { window.removeEventListener("resize", onResize); });
|
| 352 |
app.start();
|
| 353 |
|
| 354 |
+
// Rendu PBR “safe”
|
| 355 |
app.scene.gammaCorrection = pc.GAMMA_SRGB;
|
| 356 |
app.scene.toneMapping = pc.TONEMAP_ACES;
|
| 357 |
app.scene.exposure = 1;
|
|
|
|
| 367 |
light.setLocalEulerAngles(45, 30, 0);
|
| 368 |
app.root.addChild(light);
|
| 369 |
|
| 370 |
+
// Réticule
|
| 371 |
var reticleMat = new pc.StandardMaterial();
|
| 372 |
reticleMat.diffuse = new pc.Color(0.2, 0.8, 1.0);
|
| 373 |
reticleMat.opacity = 0.85;
|
|
|
|
| 380 |
reticle.enabled = false;
|
| 381 |
app.root.addChild(reticle);
|
| 382 |
|
| 383 |
+
// Modèle
|
| 384 |
var modelRoot = new pc.Entity("ModelRoot");
|
| 385 |
modelRoot.enabled = false;
|
| 386 |
app.root.addChild(modelRoot);
|
| 387 |
var modelLoaded = false, placedOnce = false;
|
| 388 |
|
| 389 |
+
// Ombre blob
|
| 390 |
var blob = null;
|
| 391 |
var BLOB_SIZE = 0.4;
|
| 392 |
var BLOB_OFFSET_Y = 0.005;
|
|
|
|
| 441 |
return e;
|
| 442 |
}
|
| 443 |
|
| 444 |
+
// Rotation via slider
|
| 445 |
var baseEulerX = 0, baseEulerZ = 0;
|
| 446 |
var rotationYDeg = 0;
|
| 447 |
function clamp360(d) { return Math.max(0, Math.min(360, d)); }
|
|
|
|
| 465 |
if (rotLikePlane) blob.setRotation(rotLikePlane);
|
| 466 |
}
|
| 467 |
|
| 468 |
+
// Chargement GLB
|
| 469 |
app.assets.loadFromUrl(GLB_URL, "container", function (err, asset) {
|
| 470 |
if (err) { console.error(err); message("Échec du chargement du modèle GLB."); return; }
|
| 471 |
var instance = asset.resource.instantiateRenderEntity({ castShadows: true, receiveShadows: false });
|
| 472 |
modelRoot.addChild(instance);
|
| 473 |
modelRoot.setLocalScale(1, 1, 1);
|
| 474 |
|
|
|
|
| 475 |
var renders = instance.findComponents('render');
|
| 476 |
for (var ri = 0; ri < renders.length; ri++) {
|
| 477 |
var r = renders[ri];
|
|
|
|
| 491 |
baseEulerX = initE.x; baseEulerZ = initE.z;
|
| 492 |
|
| 493 |
modelLoaded = true;
|
|
|
|
| 494 |
message("Modèle chargé. Appuyez sur « Lancer l’AR ».");
|
| 495 |
});
|
| 496 |
|
| 497 |
if (!app.xr.supported) { message("WebXR n’est pas supporté sur cet appareil."); return; }
|
| 498 |
|
| 499 |
+
// Slider (pointer capture)
|
| 500 |
var uiInteracting = false;
|
| 501 |
var draggingTrack = false;
|
| 502 |
var activePointerId = null;
|
|
|
|
| 507 |
var y = (e.clientY != null) ? e.clientY : ((e.touches && e.touches[0] && e.touches[0].clientY) || 0);
|
| 508 |
var ratio = (y - rect.top) / rect.height;
|
| 509 |
var t = Math.max(0, Math.min(1, ratio));
|
| 510 |
+
return (1 - t) * 360;
|
| 511 |
}
|
| 512 |
|
| 513 |
function onPointerDownCapture(e) {
|
|
|
|
| 515 |
uiInteracting = true;
|
| 516 |
draggingTrack = true;
|
| 517 |
activePointerId = (e.pointerId != null) ? e.pointerId : 1;
|
| 518 |
+
if (rotTrack.setPointerCapture) { try { rotTrack.setPointerCapture(activePointerId); } catch (er) {} }
|
|
|
|
|
|
|
| 519 |
applyRotationY(degFromPointer(e));
|
| 520 |
+
e.preventDefault(); e.stopPropagation();
|
|
|
|
| 521 |
}
|
| 522 |
function onPointerMoveCapture(e) {
|
| 523 |
if (!draggingTrack || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
|
| 524 |
applyRotationY(degFromPointer(e));
|
| 525 |
+
e.preventDefault(); e.stopPropagation();
|
|
|
|
| 526 |
}
|
| 527 |
function endDrag(e) {
|
| 528 |
if (!draggingTrack || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
|
| 529 |
+
draggingTrack = false; uiInteracting = false;
|
| 530 |
+
if (rotTrack.releasePointerCapture) { try { rotTrack.releasePointerCapture(activePointerId); } catch (er) {} }
|
|
|
|
|
|
|
|
|
|
| 531 |
activePointerId = null;
|
| 532 |
+
e.preventDefault(); e.stopPropagation();
|
|
|
|
| 533 |
}
|
| 534 |
|
| 535 |
document.addEventListener("pointerdown", onPointerDownCapture, { capture: true, passive: false });
|
|
|
|
| 554 |
});
|
| 555 |
}
|
| 556 |
|
| 557 |
+
// Bouton start Android/Desktop
|
| 558 |
var startBtn = ensureARStartButton(activateAR);
|
| 559 |
|
| 560 |
+
// Pas de démarrage par tap/clic sur le canvas
|
| 561 |
app.keyboard.on("keydown", function (evt) { if (evt.key === pc.KEY_ESCAPE && app.xr.active) app.xr.end(); });
|
| 562 |
|
| 563 |
+
// Hit-test (plans quasi-horizontaux)
|
| 564 |
var TMP_IN = new pc.Vec3(0, 1, 0), TMP_OUT = new pc.Vec3();
|
| 565 |
function isHorizontalUpFacing(rot, minDot) {
|
| 566 |
minDot = (typeof minDot === "number") ? minDot : 0.75;
|
|
|
|
| 568 |
return TMP_OUT.y >= minDot;
|
| 569 |
}
|
| 570 |
|
|
|
|
| 571 |
app.xr.hitTest.on("available", function () {
|
| 572 |
app.xr.hitTest.start({
|
| 573 |
entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
|
|
|
|
| 584 |
modelRoot.enabled = true;
|
| 585 |
modelRoot.setPosition(pos);
|
| 586 |
|
|
|
|
| 587 |
blob = createBlobShadowAt(pos, rot);
|
| 588 |
|
| 589 |
var e = new pc.Vec3();
|
|
|
|
| 600 |
});
|
| 601 |
});
|
| 602 |
|
| 603 |
+
// Drag XR continu
|
| 604 |
var isDragging = false;
|
| 605 |
app.xr.input.on("add", function (inputSource) {
|
| 606 |
inputSource.on("selectstart", function () {
|
|
|
|
| 664 |
applyRotationY(v);
|
| 665 |
}, { passive: true });
|
| 666 |
|
| 667 |
+
// Événements AR (feedback + visibilité bouton)
|
| 668 |
app.xr.on("start", function () {
|
| 669 |
if (startBtn) startBtn.style.display = "none";
|
| 670 |
message("Session AR démarrée. Visez le sol pour détecter un plan…");
|