Update viewer_ar_ios.js
Browse files- viewer_ar_ios.js +117 -274
viewer_ar_ios.js
CHANGED
|
@@ -1,89 +1,75 @@
|
|
|
|
|
|
|
|
| 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)
|
| 4 |
- Android/Desktop : WebXR AR (plans HORIZONTAUX uniquement) + slider yaw + blob shadow
|
| 5 |
-
- Message de démarrage centré & plus grand
|
| 6 |
- Éclairage PBR par défaut (sans WebXR light estimation)
|
| 7 |
- Monté dans un conteneur choisi (data-mount="#ar-mount") pour Squarespace
|
| 8 |
*/
|
| 9 |
|
| 10 |
(function () {
|
| 11 |
-
//
|
| 12 |
function getCurrentScript() {
|
| 13 |
-
// Robuste même si document.currentScript n'est pas dispo
|
| 14 |
return document.currentScript || (function () {
|
| 15 |
var scripts = document.getElementsByTagName('script');
|
| 16 |
return scripts[scripts.length - 1] || null;
|
| 17 |
})();
|
| 18 |
}
|
| 19 |
-
|
| 20 |
function findConfigUrl() {
|
| 21 |
var el = getCurrentScript();
|
| 22 |
if (!el) return null;
|
| 23 |
var url = el.getAttribute('data-config');
|
| 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 {
|
| 40 |
var resp = await fetch(url, { cache: 'no-store' });
|
| 41 |
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
| 42 |
-
|
| 43 |
-
return json;
|
| 44 |
} catch (e) {
|
| 45 |
console.error("Erreur chargement config.json:", e);
|
| 46 |
return null;
|
| 47 |
}
|
| 48 |
}
|
| 49 |
|
| 50 |
-
//
|
| 51 |
var PC_VERSION = "2.11.7";
|
| 52 |
var PC_URLS = {
|
| 53 |
esm: ["https://cdn.jsdelivr.net/npm/playcanvas@" + PC_VERSION + "/build/playcanvas.min.mjs"],
|
| 54 |
umd: ["https://cdn.jsdelivr.net/npm/playcanvas@" + PC_VERSION + "/build/playcanvas.min.js"]
|
| 55 |
};
|
| 56 |
-
|
| 57 |
async function loadPlayCanvasRobust(opts) {
|
| 58 |
opts = opts || {};
|
| 59 |
var esmFirst = (typeof opts.esmFirst === "boolean") ? opts.esmFirst : true;
|
| 60 |
var loadTimeoutMs = (typeof opts.loadTimeoutMs === "number") ? opts.loadTimeoutMs : 15000;
|
| 61 |
-
|
| 62 |
if (window.pc && window.pc.Application) return window.pc;
|
| 63 |
|
| 64 |
async function tryESM() {
|
| 65 |
for (var i = 0; i < PC_URLS.esm.length; i++) {
|
| 66 |
-
var url = PC_URLS.esm[i];
|
| 67 |
try {
|
| 68 |
-
var mod = await Promise.race([import(/* @vite-ignore */
|
| 69 |
var ns = (mod && (mod.pc || mod["default"])) || mod;
|
| 70 |
-
if (ns && ns.Application) {
|
| 71 |
-
|
| 72 |
-
return window.pc;
|
| 73 |
-
}
|
| 74 |
-
} catch (_e) { /* continue */ }
|
| 75 |
}
|
| 76 |
throw new Error("ESM failed");
|
| 77 |
}
|
| 78 |
-
|
| 79 |
async function tryUMD() {
|
| 80 |
for (var j = 0; j < PC_URLS.umd.length; j++) {
|
| 81 |
-
var url2 = PC_URLS.umd[j];
|
| 82 |
try {
|
| 83 |
await Promise.race([
|
| 84 |
new Promise(function (res, rej) {
|
| 85 |
var s = document.createElement("script");
|
| 86 |
-
s.src =
|
| 87 |
s.async = true;
|
| 88 |
s.onload = function () { res(); };
|
| 89 |
s.onerror = function () { rej(new Error("script error")); };
|
|
@@ -92,11 +78,10 @@
|
|
| 92 |
timeout(loadTimeoutMs)
|
| 93 |
]);
|
| 94 |
if (window.pc && window.pc.Application) return window.pc;
|
| 95 |
-
} catch (_e) {
|
| 96 |
}
|
| 97 |
throw new Error("UMD failed");
|
| 98 |
}
|
| 99 |
-
|
| 100 |
try {
|
| 101 |
if (esmFirst) return await tryESM();
|
| 102 |
return await tryUMD();
|
|
@@ -106,23 +91,23 @@
|
|
| 106 |
}
|
| 107 |
}
|
| 108 |
|
| 109 |
-
//
|
| 110 |
-
// Style minimal intégré : toasts, message centré, bouton “Lancer l’AR” (iOS & Android).
|
| 111 |
var css = [
|
| 112 |
-
/* Toast
|
| 113 |
".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}",
|
| 114 |
-
/* Variante centrée
|
| 115 |
".pc-ar-msg.pc-ar-msg--centerBig{top:50%;bottom:auto;transform:translate(-50%,-50%);font-size:18px;padding:14px 18px;max-width:min(90vw,720px)}",
|
| 116 |
|
| 117 |
-
|
|
|
|
|
|
|
| 118 |
"#ar-launch-btn{position:fixed;left:50%;transform:translateX(-50%);bottom:72px;z-index:10003;pointer-events:auto;appearance:none;border:none;border-radius:14px;padding:12px 18px;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;font-size:15px;font-weight:600;color:#fff;background:rgba(0,0,0,0.85);box-shadow:0 6px 18px rgba(0,0,0,.25);backdrop-filter:blur(6px);cursor:pointer;touch-action:manipulation;-webkit-tap-highlight-color:transparent}",
|
| 119 |
"#ar-launch-btn:active{transform:translateX(-50%) scale(0.98)}",
|
| 120 |
"#ar-launch-btn:disabled{opacity:.5;cursor:not-allowed}",
|
| 121 |
|
| 122 |
-
/*
|
| 123 |
-
"
|
| 124 |
-
|
| 125 |
-
"#xr-overlay-root{position:fixed;inset:0;z-index:10001;pointer-events:none}",
|
| 126 |
|
| 127 |
/* --- NE PAS MODIFIER : panneau Rotation (WebXR) --- */
|
| 128 |
".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}",
|
|
@@ -139,37 +124,25 @@
|
|
| 139 |
|
| 140 |
function ensureOverlayRoot() {
|
| 141 |
var r = document.getElementById("xr-overlay-root");
|
| 142 |
-
if (!r) {
|
| 143 |
-
r = document.createElement("div");
|
| 144 |
-
r.id = "xr-overlay-root";
|
| 145 |
-
document.body.appendChild(r);
|
| 146 |
-
}
|
| 147 |
return r;
|
| 148 |
}
|
| 149 |
var overlayRoot = ensureOverlayRoot();
|
| 150 |
|
| 151 |
-
//
|
| 152 |
function getMessageEl() {
|
| 153 |
var el = overlayRoot.querySelector(".pc-ar-msg");
|
| 154 |
-
if (!el) {
|
| 155 |
-
el = document.createElement("div");
|
| 156 |
-
el.className = "pc-ar-msg";
|
| 157 |
-
overlayRoot.appendChild(el);
|
| 158 |
-
}
|
| 159 |
return el;
|
| 160 |
}
|
| 161 |
-
function messageToast(
|
| 162 |
-
var el = getMessageEl();
|
| 163 |
-
el.classList.remove("pc-ar-msg--centerBig");
|
| 164 |
-
el.textContent = text;
|
| 165 |
}
|
| 166 |
-
function messageCenterBig(
|
| 167 |
-
var el = getMessageEl();
|
| 168 |
-
el.classList.add("pc-ar-msg--centerBig");
|
| 169 |
-
el.textContent = text;
|
| 170 |
}
|
| 171 |
|
| 172 |
-
// iOS Quick Look
|
| 173 |
function buildQuickLookHref(usdzUrl) {
|
| 174 |
try {
|
| 175 |
var u = new URL(usdzUrl, window.location.href);
|
|
@@ -182,92 +155,49 @@
|
|
| 182 |
return usdzUrl + (usdzUrl.indexOf('#') >= 0 ? '&' : '#') + 'allowsContentScaling=0';
|
| 183 |
}
|
| 184 |
}
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
document.body.appendChild(
|
| 195 |
-
}
|
| 196 |
-
if (usdzUrl) anchor.setAttribute("href", buildQuickLookHref(usdzUrl));
|
| 197 |
-
|
| 198 |
-
// Bouton visible “Lancer l’AR”
|
| 199 |
-
var btn = document.getElementById("ar-launch-btn");
|
| 200 |
-
if (!btn) {
|
| 201 |
-
btn = document.createElement("button");
|
| 202 |
-
btn.id = "ar-launch-btn";
|
| 203 |
-
btn.type = "button";
|
| 204 |
-
btn.textContent = "Lancer l’AR";
|
| 205 |
-
// Évite la propagation d’events vers la scène
|
| 206 |
-
btn.addEventListener("pointerdown", function (e) { e.stopPropagation(); }, { passive: true });
|
| 207 |
-
btn.addEventListener("click", function (e) { e.stopPropagation(); }, false);
|
| 208 |
-
document.body.appendChild(btn);
|
| 209 |
}
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
btn.onclick = function (e) {
|
| 213 |
-
e.preventDefault();
|
| 214 |
-
e.stopPropagation();
|
| 215 |
-
if (anchor && anchor.href) anchor.click();
|
| 216 |
-
};
|
| 217 |
-
|
| 218 |
-
return { btn: btn, anchor: anchor };
|
| 219 |
}
|
| 220 |
|
| 221 |
-
//
|
| 222 |
function ensureCanvas() {
|
| 223 |
var scriptEl = getCurrentScript();
|
| 224 |
var mountSel = scriptEl && scriptEl.getAttribute('data-mount');
|
| 225 |
var desiredHeight = (scriptEl && scriptEl.getAttribute('data-height')) || '70vh';
|
| 226 |
|
| 227 |
-
// Trouve le conteneur cible
|
| 228 |
var mountEl = null;
|
| 229 |
if (mountSel) mountEl = document.querySelector(mountSel);
|
| 230 |
-
if (!mountEl) {
|
| 231 |
-
// fallback : tout en haut du body (pour éviter "sous le footer")
|
| 232 |
-
mountEl = document.createElement('div');
|
| 233 |
-
mountEl.id = 'ar-mount-fallback';
|
| 234 |
-
document.body.insertBefore(mountEl, document.body.firstChild);
|
| 235 |
-
}
|
| 236 |
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
mountStyle.webkitTapHighlightColor = 'transparent';
|
| 244 |
|
| 245 |
-
// Réutilise ou crée le canvas
|
| 246 |
var canvas = mountEl.querySelector('#application-canvas');
|
| 247 |
-
if (!canvas) {
|
| 248 |
-
canvas = document.createElement('canvas');
|
| 249 |
-
canvas.id = 'application-canvas';
|
| 250 |
-
mountEl.appendChild(canvas);
|
| 251 |
-
}
|
| 252 |
|
| 253 |
-
// Le canvas remplit le conteneur
|
| 254 |
var cs = canvas.style;
|
| 255 |
-
cs.position = 'absolute';
|
| 256 |
-
cs.left = '0';
|
| 257 |
-
cs.top = '0';
|
| 258 |
-
cs.width = '100%';
|
| 259 |
-
cs.height = '100%';
|
| 260 |
-
cs.display = 'block';
|
| 261 |
-
|
| 262 |
-
// Optionnel : ramener le viewport sur le viewer
|
| 263 |
-
try {
|
| 264 |
-
mountEl.scrollIntoView({ behavior: 'instant', block: 'start' });
|
| 265 |
-
} catch (_e) {}
|
| 266 |
|
|
|
|
| 267 |
return canvas;
|
| 268 |
}
|
| 269 |
|
| 270 |
-
// ---------- Panneau “Rotation” (inchangé
|
| 271 |
function ensureSliderUI() {
|
| 272 |
var p = overlayRoot.querySelector(".ar-ui");
|
| 273 |
if (p) return p;
|
|
@@ -285,23 +215,19 @@
|
|
| 285 |
return p;
|
| 286 |
}
|
| 287 |
|
| 288 |
-
//
|
| 289 |
(async function () {
|
| 290 |
var cfgUrl = findConfigUrl();
|
| 291 |
var cfg = await loadConfigJson(cfgUrl);
|
| 292 |
var GLB_URL = (cfg && typeof cfg.glb_url === "string" && cfg.glb_url)
|
| 293 |
? cfg.glb_url
|
| 294 |
: "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/AR/tests/danae_no_metallic.glb";
|
| 295 |
-
var USDZ_URL = (cfg && typeof cfg.usdz_url === "string" && cfg.usdz_url)
|
| 296 |
-
? cfg.usdz_url
|
| 297 |
-
: null;
|
| 298 |
|
| 299 |
-
//
|
| 300 |
if (isIOS()) {
|
| 301 |
if (USDZ_URL) {
|
| 302 |
-
|
| 303 |
-
ensureIOSLaunchUI(USDZ_URL);
|
| 304 |
-
// Message de démarrage centré et plus grand (même UX que WebXR)
|
| 305 |
messageCenterBig("Modèle chargé. Appuyez sur « Lancer l’AR » pour démarrer.");
|
| 306 |
} else {
|
| 307 |
messageToast("iOS détecté, mais aucun 'usdz_url' dans config.json.");
|
|
@@ -309,7 +235,7 @@
|
|
| 309 |
return;
|
| 310 |
}
|
| 311 |
|
| 312 |
-
//
|
| 313 |
try {
|
| 314 |
await loadPlayCanvasRobust({ esmFirst: true, loadTimeoutMs: 15000 });
|
| 315 |
} catch (e) {
|
|
@@ -320,11 +246,10 @@
|
|
| 320 |
initARApp(GLB_URL);
|
| 321 |
})();
|
| 322 |
|
| 323 |
-
//
|
| 324 |
function initARApp(GLB_URL) {
|
| 325 |
var pc = window.pc;
|
| 326 |
|
| 327 |
-
// Canvas + UI
|
| 328 |
var canvas = ensureCanvas();
|
| 329 |
var sliderPanel = ensureSliderUI();
|
| 330 |
var rotTrack = sliderPanel.querySelector("#ar-rotY-wrap");
|
|
@@ -334,7 +259,6 @@
|
|
| 334 |
|
| 335 |
window.focus();
|
| 336 |
|
| 337 |
-
// --- Application PlayCanvas ---
|
| 338 |
var app = new pc.Application(canvas, {
|
| 339 |
mouse: new pc.Mouse(canvas),
|
| 340 |
touch: new pc.TouchDevice(canvas),
|
|
@@ -350,13 +274,13 @@
|
|
| 350 |
app.on("destroy", function () { window.removeEventListener("resize", onResize); });
|
| 351 |
app.start();
|
| 352 |
|
| 353 |
-
//
|
| 354 |
app.scene.gammaCorrection = pc.GAMMA_SRGB;
|
| 355 |
app.scene.toneMapping = pc.TONEMAP_ACES;
|
| 356 |
app.scene.exposure = 1;
|
| 357 |
app.scene.ambientLight = new pc.Color(1, 1, 1);
|
| 358 |
|
| 359 |
-
//
|
| 360 |
var camera = new pc.Entity("Camera");
|
| 361 |
camera.addComponent("camera", { clearColor: new pc.Color(0, 0, 0, 0), farClip: 10000 });
|
| 362 |
app.root.addChild(camera);
|
|
@@ -366,61 +290,47 @@
|
|
| 366 |
mainLight.setLocalEulerAngles(45, 30, 0);
|
| 367 |
app.root.addChild(mainLight);
|
| 368 |
|
| 369 |
-
//
|
| 370 |
var reticleMat = new pc.StandardMaterial();
|
| 371 |
reticleMat.diffuse = new pc.Color(0.2, 0.8, 1.0);
|
| 372 |
reticleMat.opacity = 0.85;
|
| 373 |
reticleMat.blendType = pc.BLEND_NORMAL;
|
| 374 |
reticleMat.update();
|
| 375 |
-
|
| 376 |
var reticle = new pc.Entity("Reticle");
|
| 377 |
reticle.addComponent("render", { type: "torus", material: reticleMat });
|
| 378 |
reticle.setLocalScale(0.12, 0.005, 0.12);
|
| 379 |
reticle.enabled = false;
|
| 380 |
app.root.addChild(reticle);
|
| 381 |
|
| 382 |
-
//
|
| 383 |
var modelRoot = new pc.Entity("ModelRoot");
|
| 384 |
modelRoot.enabled = false;
|
| 385 |
app.root.addChild(modelRoot);
|
| 386 |
|
| 387 |
-
var modelLoaded = false;
|
| 388 |
-
var placedOnce = false;
|
| 389 |
|
| 390 |
-
//
|
| 391 |
var blobShadowEntity = null;
|
| 392 |
-
var BLOB_SIZE = 0.4;
|
| 393 |
-
var BLOB_OFFSET_Y = 0.005;
|
| 394 |
|
| 395 |
function makeBlobTexture(appRef, size) {
|
| 396 |
var s = size || 256;
|
| 397 |
-
var cvs = document.createElement('canvas');
|
| 398 |
-
cvs.width = cvs.height = s;
|
| 399 |
var ctx = cvs.getContext('2d');
|
| 400 |
var r = s * 0.45;
|
| 401 |
-
var grd = ctx.createRadialGradient(s
|
| 402 |
-
grd.addColorStop(0, 'rgba(0,0,0,0.5)');
|
| 403 |
-
grd.
|
| 404 |
-
ctx.fillStyle = grd;
|
| 405 |
-
ctx.fillRect(0, 0, s, s);
|
| 406 |
-
|
| 407 |
var tex = new pc.Texture(appRef.graphicsDevice, {
|
| 408 |
-
width: s,
|
| 409 |
-
|
| 410 |
-
format: pc.PIXELFORMAT_R8_G8_B8_A8,
|
| 411 |
-
mipmaps: true,
|
| 412 |
-
magFilter: pc.FILTER_LINEAR,
|
| 413 |
-
minFilter: pc.FILTER_LINEAR_MIPMAP_LINEAR
|
| 414 |
});
|
| 415 |
-
tex.setSource(cvs);
|
| 416 |
-
return tex;
|
| 417 |
}
|
| 418 |
-
|
| 419 |
function createBlobShadowAt(pos, rot) {
|
| 420 |
var tex = makeBlobTexture(app, 256);
|
| 421 |
-
|
| 422 |
var blobMat = new pc.StandardMaterial();
|
| 423 |
-
blobMat.diffuse = new pc.Color(0,
|
| 424 |
blobMat.opacity = 1.0;
|
| 425 |
blobMat.opacityMap = tex;
|
| 426 |
blobMat.opacityMapChannel = 'a';
|
|
@@ -429,27 +339,20 @@
|
|
| 429 |
blobMat.depthWrite = false;
|
| 430 |
blobMat.alphaTest = 0;
|
| 431 |
blobMat.update();
|
| 432 |
-
|
| 433 |
var e = new pc.Entity("BlobShadow");
|
| 434 |
e.addComponent("render", { type: "plane", castShadows: false, receiveShadows: false });
|
| 435 |
e.render.material = blobMat;
|
| 436 |
-
|
| 437 |
e.setPosition(pos.x, pos.y + BLOB_OFFSET_Y, pos.z);
|
| 438 |
e.setRotation(rot);
|
| 439 |
e.setLocalScale(BLOB_SIZE, 1, BLOB_SIZE);
|
| 440 |
-
|
| 441 |
app.root.addChild(e);
|
| 442 |
return e;
|
| 443 |
}
|
| 444 |
|
| 445 |
-
// Euler de base
|
| 446 |
-
var baseEulerX = 0;
|
| 447 |
-
var baseEulerZ = 0;
|
| 448 |
-
|
| 449 |
-
// Rotation via slider (0..360, 360 en haut / 0 en bas)
|
| 450 |
var rotationYDeg = 0;
|
| 451 |
function clamp360(d) { return Math.max(0, Math.min(360, d)); }
|
| 452 |
-
|
| 453 |
function updateKnobFromY(yDeg) {
|
| 454 |
var t = 1 - (yDeg / 360);
|
| 455 |
rotThumb.style.top = String(t * 100) + "%";
|
|
@@ -462,31 +365,25 @@
|
|
| 462 |
modelRoot.setEulerAngles(baseEulerX, y, baseEulerZ);
|
| 463 |
updateKnobFromY(y);
|
| 464 |
}
|
| 465 |
-
|
| 466 |
function updateBlobPositionUnder(pos, rotLikePlane) {
|
| 467 |
if (!blobShadowEntity) return;
|
| 468 |
blobShadowEntity.setPosition(pos.x, pos.y + BLOB_OFFSET_Y, pos.z);
|
| 469 |
if (rotLikePlane) blobShadowEntity.setRotation(rotLikePlane);
|
| 470 |
}
|
| 471 |
|
| 472 |
-
// Chargement GLB
|
| 473 |
app.assets.loadFromUrl(GLB_URL, "container", function (err, asset) {
|
| 474 |
if (err) { console.error(err); messageToast("Échec du chargement du modèle GLB."); return; }
|
| 475 |
var instance = asset.resource.instantiateRenderEntity({ castShadows: true, receiveShadows: false });
|
| 476 |
modelRoot.addChild(instance);
|
| 477 |
-
modelRoot.setLocalScale(1,
|
| 478 |
|
| 479 |
-
// Fix matériaux simples
|
| 480 |
var renders = instance.findComponents('render');
|
| 481 |
for (var ri = 0; ri < renders.length; ri++) {
|
| 482 |
-
var r = renders[ri];
|
| 483 |
-
r.castShadows = true;
|
| 484 |
for (var mi = 0; mi < r.meshInstances.length; mi++) {
|
| 485 |
-
var mat = r.meshInstances[mi].material;
|
| 486 |
-
if (
|
| 487 |
-
if (mat.diffuse && (mat.diffuse.r !== 1 || mat.diffuse.g !== 1 || mat.diffuse.b !== 1)) {
|
| 488 |
-
mat.diffuse.set(1, 1, 1);
|
| 489 |
-
}
|
| 490 |
if (typeof mat.useSkybox !== "undefined") mat.useSkybox = true;
|
| 491 |
mat.update();
|
| 492 |
}
|
|
@@ -496,62 +393,45 @@
|
|
| 496 |
baseEulerX = initE.x; baseEulerZ = initE.z;
|
| 497 |
|
| 498 |
modelLoaded = true;
|
| 499 |
-
// Message de démarrage centré & plus grand (même UX que iOS)
|
| 500 |
messageCenterBig("Modèle chargé. Touchez l’écran pour démarrer l’AR.");
|
| 501 |
});
|
| 502 |
|
| 503 |
if (!app.xr.supported) { messageToast("WebXR n’est pas supporté sur cet appareil."); return; }
|
| 504 |
|
| 505 |
-
//
|
| 506 |
-
var isUIInteracting = false;
|
| 507 |
-
var isTrackDragging = false;
|
| 508 |
-
var activePointerId = null;
|
| 509 |
-
|
| 510 |
function insideTrack(target) { return rotTrack.contains(target); }
|
| 511 |
function degFromPointer(e) {
|
| 512 |
var rect = rotTrack.getBoundingClientRect();
|
| 513 |
var y = (e.clientY != null) ? e.clientY : ((e.touches && e.touches[0] && e.touches[0].clientY) || 0);
|
| 514 |
-
var ratio = (y - rect.top) / rect.height;
|
| 515 |
-
|
| 516 |
-
return (1 - t) * 360; // 360 en haut, 0 en bas
|
| 517 |
}
|
| 518 |
-
|
| 519 |
function onPointerDownCapture(e) {
|
| 520 |
if (!insideTrack(e.target)) return;
|
| 521 |
-
isUIInteracting = true;
|
| 522 |
-
isTrackDragging = true;
|
| 523 |
activePointerId = (e.pointerId != null) ? e.pointerId : 1;
|
| 524 |
-
if (rotTrack.setPointerCapture) {
|
| 525 |
-
try { rotTrack.setPointerCapture(activePointerId); } catch (_er) {}
|
| 526 |
-
}
|
| 527 |
applyRotationY(degFromPointer(e));
|
| 528 |
-
e.preventDefault();
|
| 529 |
-
e.stopPropagation();
|
| 530 |
}
|
| 531 |
function onPointerMoveCapture(e) {
|
| 532 |
if (!isTrackDragging || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
|
| 533 |
applyRotationY(degFromPointer(e));
|
| 534 |
-
e.preventDefault();
|
| 535 |
-
e.stopPropagation();
|
| 536 |
}
|
| 537 |
function endDrag(e) {
|
| 538 |
if (!isTrackDragging || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
|
| 539 |
-
isTrackDragging = false;
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
try { rotTrack.releasePointerCapture(activePointerId); } catch (_er) {}
|
| 543 |
-
}
|
| 544 |
-
activePointerId = null;
|
| 545 |
-
e.preventDefault();
|
| 546 |
-
e.stopPropagation();
|
| 547 |
}
|
| 548 |
-
|
| 549 |
document.addEventListener("pointerdown", onPointerDownCapture, { capture: true, passive: false });
|
| 550 |
document.addEventListener("pointermove", onPointerMoveCapture, { capture: true, passive: false });
|
| 551 |
document.addEventListener("pointerup", endDrag, { capture: true, passive: false });
|
| 552 |
document.addEventListener("pointercancel", endDrag, { capture: true, passive: false });
|
| 553 |
|
| 554 |
-
//
|
| 555 |
function activateAR() {
|
| 556 |
if (!app.xr.isAvailable(pc.XRTYPE_AR)) { messageToast("AR immersive indisponible sur cet appareil."); return; }
|
| 557 |
if (!app.xr.domOverlay) app.xr.domOverlay = {};
|
|
@@ -560,34 +440,26 @@
|
|
| 560 |
requiredFeatures: ["hit-test", "dom-overlay"],
|
| 561 |
domOverlay: { root: app.xr.domOverlay.root },
|
| 562 |
callback: function (err) {
|
| 563 |
-
if (err) {
|
| 564 |
-
console.error("Échec du démarrage AR :", err);
|
| 565 |
-
messageToast("Échec du démarrage AR : " + (err.message || err));
|
| 566 |
-
}
|
| 567 |
}
|
| 568 |
});
|
| 569 |
}
|
| 570 |
-
// Tap écran → démarre l'AR (si pas sur le slider)
|
| 571 |
app.mouse.on("mousedown", function () { if (!app.xr.active && !isUIInteracting) activateAR(); });
|
| 572 |
if (app.touch) {
|
| 573 |
app.touch.on("touchend", function (evt) {
|
| 574 |
if (!app.xr.active && !isUIInteracting) activateAR();
|
| 575 |
-
evt.event.preventDefault();
|
| 576 |
-
evt.event.stopPropagation();
|
| 577 |
});
|
| 578 |
}
|
| 579 |
app.keyboard.on("keydown", function (evt) { if (evt.key === pc.KEY_ESCAPE && app.xr.active) app.xr.end(); });
|
| 580 |
|
| 581 |
// Hit-test HORIZONTAL uniquement
|
| 582 |
-
var TMP_IN = new pc.Vec3(0,
|
| 583 |
-
var TMP_OUT = new pc.Vec3();
|
| 584 |
function isHorizontalUpFacing(rot, minDot) {
|
| 585 |
var md = (typeof minDot === "number") ? minDot : 0.75;
|
| 586 |
rot.transformVector(TMP_IN, TMP_OUT);
|
| 587 |
-
return TMP_OUT.y >= md;
|
| 588 |
}
|
| 589 |
-
|
| 590 |
-
// Hit Test global
|
| 591 |
app.xr.hitTest.on("available", function () {
|
| 592 |
app.xr.hitTest.start({
|
| 593 |
entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
|
|
@@ -595,27 +467,16 @@
|
|
| 595 |
if (err) { messageToast("Le AR hit test n’a pas pu démarrer."); return; }
|
| 596 |
hitSource.on("result", function (pos, rot) {
|
| 597 |
if (!isHorizontalUpFacing(rot)) return;
|
| 598 |
-
|
| 599 |
-
reticle.enabled = true;
|
| 600 |
-
reticle.setPosition(pos);
|
| 601 |
-
reticle.setRotation(rot);
|
| 602 |
|
| 603 |
if (modelLoaded && !placedOnce) {
|
| 604 |
-
modelRoot.enabled = true;
|
| 605 |
-
modelRoot.setPosition(pos);
|
| 606 |
-
|
| 607 |
-
// Ombre de contact
|
| 608 |
blobShadowEntity = createBlobShadowAt(pos, rot);
|
| 609 |
|
| 610 |
-
var e = new pc.Vec3();
|
| 611 |
-
|
| 612 |
-
var y0 = ((e.y % 360) + 360) % 360;
|
| 613 |
-
applyRotationY(y0);
|
| 614 |
|
| 615 |
-
placedOnce = true;
|
| 616 |
-
rotRangeInput.disabled = false;
|
| 617 |
-
|
| 618 |
-
// Après placement, toasts bas classiques
|
| 619 |
messageToast("Objet placé. Glissez pour déplacer, tournez-le avec le slider →");
|
| 620 |
}
|
| 621 |
});
|
|
@@ -623,26 +484,22 @@
|
|
| 623 |
});
|
| 624 |
});
|
| 625 |
|
| 626 |
-
//
|
| 627 |
var isModelDragging = false;
|
| 628 |
app.xr.input.on("add", function (inputSource) {
|
| 629 |
inputSource.on("selectstart", function () {
|
| 630 |
if (isUIInteracting) return;
|
| 631 |
if (!placedOnce || !modelLoaded) return;
|
| 632 |
-
|
| 633 |
inputSource.hitTestStart({
|
| 634 |
entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
|
| 635 |
callback: function (err, transientSource) {
|
| 636 |
if (err) return;
|
| 637 |
isModelDragging = true;
|
| 638 |
-
|
| 639 |
transientSource.on("result", function (pos, rot) {
|
| 640 |
if (!isModelDragging) return;
|
| 641 |
if (!isHorizontalUpFacing(rot)) return;
|
| 642 |
-
modelRoot.setPosition(pos);
|
| 643 |
-
updateBlobPositionUnder(pos, rot);
|
| 644 |
});
|
| 645 |
-
|
| 646 |
transientSource.once("remove", function () { isModelDragging = false; });
|
| 647 |
}
|
| 648 |
});
|
|
@@ -650,55 +507,40 @@
|
|
| 650 |
inputSource.on("selectend", function () { isModelDragging = false; });
|
| 651 |
});
|
| 652 |
|
| 653 |
-
// Desktop : rotation souris
|
| 654 |
-
var isRotateMode = false;
|
| 655 |
-
var lastMouseX = 0;
|
| 656 |
var ROTATE_SENSITIVITY = 0.25;
|
| 657 |
app.mouse.on("mousedown", function (e) {
|
| 658 |
if (!app.xr.active || !placedOnce || isUIInteracting) return;
|
| 659 |
-
if (e.button === 0 && !e.shiftKey) {
|
| 660 |
-
|
| 661 |
-
} else if (e.button === 2 || (e.button === 0 && e.shiftKey)) {
|
| 662 |
-
isRotateMode = true;
|
| 663 |
-
lastMouseX = e.x;
|
| 664 |
-
}
|
| 665 |
});
|
| 666 |
app.mouse.on("mousemove", function (e) {
|
| 667 |
if (!app.xr.active || !placedOnce || isUIInteracting) return;
|
| 668 |
if (isModelDragging) {
|
| 669 |
if (reticle.enabled) {
|
| 670 |
-
var p = reticle.getPosition();
|
| 671 |
-
modelRoot.setPosition(p);
|
| 672 |
-
updateBlobPositionUnder(p, null);
|
| 673 |
}
|
| 674 |
} else if (isRotateMode && modelRoot.enabled) {
|
| 675 |
-
var dx = e.x - lastMouseX;
|
| 676 |
-
lastMouseX = e.x;
|
| 677 |
applyRotationY(rotationYDeg + dx * ROTATE_SENSITIVITY);
|
| 678 |
}
|
| 679 |
});
|
| 680 |
app.mouse.on("mouseup", function () { isModelDragging = false; isRotateMode = false; });
|
| 681 |
window.addEventListener("contextmenu", function (e) { e.preventDefault(); });
|
| 682 |
|
| 683 |
-
// Slider (accessibilité
|
| 684 |
rotRangeInput.disabled = true;
|
| 685 |
rotRangeInput.addEventListener("input", function (e) {
|
| 686 |
if (!modelRoot.enabled) return;
|
| 687 |
-
var v = parseFloat(e.target.value || "0");
|
| 688 |
-
applyRotationY(v);
|
| 689 |
}, { passive: true });
|
| 690 |
|
| 691 |
-
// AR
|
| 692 |
-
app.xr.on("start", function () {
|
| 693 |
-
messageToast("Session AR démarrée. Visez le sol pour détecter un plan…");
|
| 694 |
-
reticle.enabled = true;
|
| 695 |
-
});
|
| 696 |
app.xr.on("end", function () {
|
| 697 |
messageToast("Session AR terminée.");
|
| 698 |
-
reticle.enabled = false;
|
| 699 |
-
isModelDragging = false;
|
| 700 |
-
isRotateMode = false;
|
| 701 |
-
rotRangeInput.disabled = true;
|
| 702 |
});
|
| 703 |
app.xr.on("available:" + pc.XRTYPE_AR, function (a) {
|
| 704 |
if (!a) messageToast("AR immersive indisponible.");
|
|
@@ -711,3 +553,4 @@
|
|
| 711 |
else messageToast("Chargement du modèle…");
|
| 712 |
}
|
| 713 |
})();
|
|
|
|
|
|
| 1 |
+
<!-- viewer_ar_ios.js -->
|
| 2 |
+
<script>
|
| 3 |
/* viewer_ar_ios.js — AR PlayCanvas + GLB/USDZ via config.json
|
| 4 |
- Lit config.json (data-config) => { "glb_url": "...", "usdz_url": "..." }
|
| 5 |
+
- iOS : AR Quick Look (USDZ) sans zoom, **ouverture directe via <a rel="ar">**
|
| 6 |
- Android/Desktop : WebXR AR (plans HORIZONTAUX uniquement) + slider yaw + blob shadow
|
| 7 |
+
- Message de démarrage centré & plus grand (iOS ET Android)
|
| 8 |
- Éclairage PBR par défaut (sans WebXR light estimation)
|
| 9 |
- Monté dans un conteneur choisi (data-mount="#ar-mount") pour Squarespace
|
| 10 |
*/
|
| 11 |
|
| 12 |
(function () {
|
| 13 |
+
// ===================== Utilitaires: script/config/plateforme =====================
|
| 14 |
function getCurrentScript() {
|
|
|
|
| 15 |
return document.currentScript || (function () {
|
| 16 |
var scripts = document.getElementsByTagName('script');
|
| 17 |
return scripts[scripts.length - 1] || null;
|
| 18 |
})();
|
| 19 |
}
|
|
|
|
| 20 |
function findConfigUrl() {
|
| 21 |
var el = getCurrentScript();
|
| 22 |
if (!el) return null;
|
| 23 |
var url = el.getAttribute('data-config');
|
| 24 |
return url || null;
|
| 25 |
}
|
|
|
|
| 26 |
function isIOS() {
|
| 27 |
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
| 28 |
}
|
|
|
|
| 29 |
function timeout(ms) {
|
| 30 |
+
return new Promise(function (_res, rej) { setTimeout(function () { rej(new Error("timeout")); }, ms); });
|
|
|
|
|
|
|
| 31 |
}
|
|
|
|
| 32 |
async function loadConfigJson(url) {
|
| 33 |
if (!url) return null;
|
| 34 |
try {
|
| 35 |
var resp = await fetch(url, { cache: 'no-store' });
|
| 36 |
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
| 37 |
+
return await resp.json();
|
|
|
|
| 38 |
} catch (e) {
|
| 39 |
console.error("Erreur chargement config.json:", e);
|
| 40 |
return null;
|
| 41 |
}
|
| 42 |
}
|
| 43 |
|
| 44 |
+
// ===================== PlayCanvas (Android/Desktop) =====================
|
| 45 |
var PC_VERSION = "2.11.7";
|
| 46 |
var PC_URLS = {
|
| 47 |
esm: ["https://cdn.jsdelivr.net/npm/playcanvas@" + PC_VERSION + "/build/playcanvas.min.mjs"],
|
| 48 |
umd: ["https://cdn.jsdelivr.net/npm/playcanvas@" + PC_VERSION + "/build/playcanvas.min.js"]
|
| 49 |
};
|
|
|
|
| 50 |
async function loadPlayCanvasRobust(opts) {
|
| 51 |
opts = opts || {};
|
| 52 |
var esmFirst = (typeof opts.esmFirst === "boolean") ? opts.esmFirst : true;
|
| 53 |
var loadTimeoutMs = (typeof opts.loadTimeoutMs === "number") ? opts.loadTimeoutMs : 15000;
|
|
|
|
| 54 |
if (window.pc && window.pc.Application) return window.pc;
|
| 55 |
|
| 56 |
async function tryESM() {
|
| 57 |
for (var i = 0; i < PC_URLS.esm.length; i++) {
|
|
|
|
| 58 |
try {
|
| 59 |
+
var mod = await Promise.race([import(/* @vite-ignore */ PC_URLS.esm[i]), timeout(loadTimeoutMs)]);
|
| 60 |
var ns = (mod && (mod.pc || mod["default"])) || mod;
|
| 61 |
+
if (ns && ns.Application) { if (!window.pc) window.pc = ns; return window.pc; }
|
| 62 |
+
} catch (_e) {}
|
|
|
|
|
|
|
|
|
|
| 63 |
}
|
| 64 |
throw new Error("ESM failed");
|
| 65 |
}
|
|
|
|
| 66 |
async function tryUMD() {
|
| 67 |
for (var j = 0; j < PC_URLS.umd.length; j++) {
|
|
|
|
| 68 |
try {
|
| 69 |
await Promise.race([
|
| 70 |
new Promise(function (res, rej) {
|
| 71 |
var s = document.createElement("script");
|
| 72 |
+
s.src = PC_URLS.umd[j];
|
| 73 |
s.async = true;
|
| 74 |
s.onload = function () { res(); };
|
| 75 |
s.onerror = function () { rej(new Error("script error")); };
|
|
|
|
| 78 |
timeout(loadTimeoutMs)
|
| 79 |
]);
|
| 80 |
if (window.pc && window.pc.Application) return window.pc;
|
| 81 |
+
} catch (_e) {}
|
| 82 |
}
|
| 83 |
throw new Error("UMD failed");
|
| 84 |
}
|
|
|
|
| 85 |
try {
|
| 86 |
if (esmFirst) return await tryESM();
|
| 87 |
return await tryUMD();
|
|
|
|
| 91 |
}
|
| 92 |
}
|
| 93 |
|
| 94 |
+
// ===================== UI / Overlay (styles minimaux intégrés) =====================
|
|
|
|
| 95 |
var css = [
|
| 96 |
+
/* Toast en bas */
|
| 97 |
".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}",
|
| 98 |
+
/* Variante centrée & plus grande (message de démarrage) */
|
| 99 |
".pc-ar-msg.pc-ar-msg--centerBig{top:50%;bottom:auto;transform:translate(-50%,-50%);font-size:18px;padding:14px 18px;max-width:min(90vw,720px)}",
|
| 100 |
|
| 101 |
+
"#xr-overlay-root{position:fixed;inset:0;z-index:10001;pointer-events:none}",
|
| 102 |
+
|
| 103 |
+
/* Bouton Android/Desktop (optionnel si tu veux un bouton aussi hors-iOS) */
|
| 104 |
"#ar-launch-btn{position:fixed;left:50%;transform:translateX(-50%);bottom:72px;z-index:10003;pointer-events:auto;appearance:none;border:none;border-radius:14px;padding:12px 18px;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;font-size:15px;font-weight:600;color:#fff;background:rgba(0,0,0,0.85);box-shadow:0 6px 18px rgba(0,0,0,.25);backdrop-filter:blur(6px);cursor:pointer;touch-action:manipulation;-webkit-tap-highlight-color:transparent}",
|
| 105 |
"#ar-launch-btn:active{transform:translateX(-50%) scale(0.98)}",
|
| 106 |
"#ar-launch-btn:disabled{opacity:.5;cursor:not-allowed}",
|
| 107 |
|
| 108 |
+
/* Lien Quick Look iOS — visuel type bouton */
|
| 109 |
+
"a.ar-launch-link{position:fixed;left:50%;transform:translateX(-50%);bottom:72px;z-index:10003;pointer-events:auto;display:inline-block;text-decoration:none;text-align:center;border-radius:14px;padding:12px 18px;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;font-size:15px;font-weight:600;color:#fff;background:rgba(0,0,0,0.85);box-shadow:0 6px 18px rgba(0,0,0,.25);backdrop-filter:blur(6px);-webkit-tap-highlight-color:transparent}",
|
| 110 |
+
"a.ar-launch-link:active{transform:translateX(-50%) scale(0.98)}",
|
|
|
|
| 111 |
|
| 112 |
/* --- NE PAS MODIFIER : panneau Rotation (WebXR) --- */
|
| 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}",
|
|
|
|
| 124 |
|
| 125 |
function ensureOverlayRoot() {
|
| 126 |
var r = document.getElementById("xr-overlay-root");
|
| 127 |
+
if (!r) { r = document.createElement("div"); r.id = "xr-overlay-root"; document.body.appendChild(r); }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
return r;
|
| 129 |
}
|
| 130 |
var overlayRoot = ensureOverlayRoot();
|
| 131 |
|
| 132 |
+
// --------- Système de messages : toast bas OU centre agrandi ----------
|
| 133 |
function getMessageEl() {
|
| 134 |
var el = overlayRoot.querySelector(".pc-ar-msg");
|
| 135 |
+
if (!el) { el = document.createElement("div"); el.className = "pc-ar-msg"; overlayRoot.appendChild(el); }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
return el;
|
| 137 |
}
|
| 138 |
+
function messageToast(txt) {
|
| 139 |
+
var el = getMessageEl(); el.classList.remove("pc-ar-msg--centerBig"); el.textContent = txt;
|
|
|
|
|
|
|
| 140 |
}
|
| 141 |
+
function messageCenterBig(txt) {
|
| 142 |
+
var el = getMessageEl(); el.classList.add("pc-ar-msg--centerBig"); el.textContent = txt;
|
|
|
|
|
|
|
| 143 |
}
|
| 144 |
|
| 145 |
+
// ===================== iOS Quick Look helpers =====================
|
| 146 |
function buildQuickLookHref(usdzUrl) {
|
| 147 |
try {
|
| 148 |
var u = new URL(usdzUrl, window.location.href);
|
|
|
|
| 155 |
return usdzUrl + (usdzUrl.indexOf('#') >= 0 ? '&' : '#') + 'allowsContentScaling=0';
|
| 156 |
}
|
| 157 |
}
|
| 158 |
+
// Affiche une **ancre visible** rel="ar" (obligatoire pour ouverture directe)
|
| 159 |
+
function ensureIOSQuickLookLink(usdzUrl) {
|
| 160 |
+
var link = document.getElementById("ar-launch-link");
|
| 161 |
+
if (!link) {
|
| 162 |
+
link = document.createElement("a");
|
| 163 |
+
link.id = "ar-launch-link";
|
| 164 |
+
link.className = "ar-launch-link";
|
| 165 |
+
link.setAttribute("rel", "ar");
|
| 166 |
+
link.textContent = "Lancer l’AR";
|
| 167 |
+
document.body.appendChild(link);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
}
|
| 169 |
+
link.setAttribute("href", buildQuickLookHref(usdzUrl));
|
| 170 |
+
return link;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
}
|
| 172 |
|
| 173 |
+
// ===================== Canvas monté dans un conteneur (Squarespace ok) =====================
|
| 174 |
function ensureCanvas() {
|
| 175 |
var scriptEl = getCurrentScript();
|
| 176 |
var mountSel = scriptEl && scriptEl.getAttribute('data-mount');
|
| 177 |
var desiredHeight = (scriptEl && scriptEl.getAttribute('data-height')) || '70vh';
|
| 178 |
|
|
|
|
| 179 |
var mountEl = null;
|
| 180 |
if (mountSel) mountEl = document.querySelector(mountSel);
|
| 181 |
+
if (!mountEl) { mountEl = document.createElement('div'); mountEl.id = 'ar-mount-fallback'; document.body.insertBefore(mountEl, document.body.firstChild); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
|
| 183 |
+
var ms = mountEl.style;
|
| 184 |
+
if (!ms.position) ms.position = 'relative';
|
| 185 |
+
ms.width = ms.width || '100%';
|
| 186 |
+
ms.minHeight = ms.minHeight || desiredHeight;
|
| 187 |
+
ms.touchAction = ms.touchAction || 'manipulation';
|
| 188 |
+
ms.webkitTapHighlightColor = 'transparent';
|
|
|
|
| 189 |
|
|
|
|
| 190 |
var canvas = mountEl.querySelector('#application-canvas');
|
| 191 |
+
if (!canvas) { canvas = document.createElement('canvas'); canvas.id = 'application-canvas'; mountEl.appendChild(canvas); }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
|
|
|
|
| 193 |
var cs = canvas.style;
|
| 194 |
+
cs.position = 'absolute'; cs.left = '0'; cs.top = '0'; cs.width = '100%'; cs.height = '100%'; cs.display = 'block';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
+
try { mountEl.scrollIntoView({ behavior: 'instant', block: 'start' }); } catch (_e) {}
|
| 197 |
return canvas;
|
| 198 |
}
|
| 199 |
|
| 200 |
+
// ---------- Panneau “Rotation” (inchangé) ----------
|
| 201 |
function ensureSliderUI() {
|
| 202 |
var p = overlayRoot.querySelector(".ar-ui");
|
| 203 |
if (p) return p;
|
|
|
|
| 215 |
return p;
|
| 216 |
}
|
| 217 |
|
| 218 |
+
// ===================== Boot : charge config, iOS vs Android/Desktop =====================
|
| 219 |
(async function () {
|
| 220 |
var cfgUrl = findConfigUrl();
|
| 221 |
var cfg = await loadConfigJson(cfgUrl);
|
| 222 |
var GLB_URL = (cfg && typeof cfg.glb_url === "string" && cfg.glb_url)
|
| 223 |
? cfg.glb_url
|
| 224 |
: "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/AR/tests/danae_no_metallic.glb";
|
| 225 |
+
var USDZ_URL = (cfg && typeof cfg.usdz_url === "string" && cfg.usdz_url) ? cfg.usdz_url : null;
|
|
|
|
|
|
|
| 226 |
|
| 227 |
+
// ---- iOS : Quick Look via lien <a rel="ar"> (ouverture directe) ----
|
| 228 |
if (isIOS()) {
|
| 229 |
if (USDZ_URL) {
|
| 230 |
+
ensureIOSQuickLookLink(USDZ_URL);
|
|
|
|
|
|
|
| 231 |
messageCenterBig("Modèle chargé. Appuyez sur « Lancer l’AR » pour démarrer.");
|
| 232 |
} else {
|
| 233 |
messageToast("iOS détecté, mais aucun 'usdz_url' dans config.json.");
|
|
|
|
| 235 |
return;
|
| 236 |
}
|
| 237 |
|
| 238 |
+
// ---- Android/Desktop : WebXR ----
|
| 239 |
try {
|
| 240 |
await loadPlayCanvasRobust({ esmFirst: true, loadTimeoutMs: 15000 });
|
| 241 |
} catch (e) {
|
|
|
|
| 246 |
initARApp(GLB_URL);
|
| 247 |
})();
|
| 248 |
|
| 249 |
+
// ===================== App WebXR (Android/Desktop) =====================
|
| 250 |
function initARApp(GLB_URL) {
|
| 251 |
var pc = window.pc;
|
| 252 |
|
|
|
|
| 253 |
var canvas = ensureCanvas();
|
| 254 |
var sliderPanel = ensureSliderUI();
|
| 255 |
var rotTrack = sliderPanel.querySelector("#ar-rotY-wrap");
|
|
|
|
| 259 |
|
| 260 |
window.focus();
|
| 261 |
|
|
|
|
| 262 |
var app = new pc.Application(canvas, {
|
| 263 |
mouse: new pc.Mouse(canvas),
|
| 264 |
touch: new pc.TouchDevice(canvas),
|
|
|
|
| 274 |
app.on("destroy", function () { window.removeEventListener("resize", onResize); });
|
| 275 |
app.start();
|
| 276 |
|
| 277 |
+
// Rendu / PBR par défaut
|
| 278 |
app.scene.gammaCorrection = pc.GAMMA_SRGB;
|
| 279 |
app.scene.toneMapping = pc.TONEMAP_ACES;
|
| 280 |
app.scene.exposure = 1;
|
| 281 |
app.scene.ambientLight = new pc.Color(1, 1, 1);
|
| 282 |
|
| 283 |
+
// Caméra + lumière
|
| 284 |
var camera = new pc.Entity("Camera");
|
| 285 |
camera.addComponent("camera", { clearColor: new pc.Color(0, 0, 0, 0), farClip: 10000 });
|
| 286 |
app.root.addChild(camera);
|
|
|
|
| 290 |
mainLight.setLocalEulerAngles(45, 30, 0);
|
| 291 |
app.root.addChild(mainLight);
|
| 292 |
|
| 293 |
+
// Réticule
|
| 294 |
var reticleMat = new pc.StandardMaterial();
|
| 295 |
reticleMat.diffuse = new pc.Color(0.2, 0.8, 1.0);
|
| 296 |
reticleMat.opacity = 0.85;
|
| 297 |
reticleMat.blendType = pc.BLEND_NORMAL;
|
| 298 |
reticleMat.update();
|
|
|
|
| 299 |
var reticle = new pc.Entity("Reticle");
|
| 300 |
reticle.addComponent("render", { type: "torus", material: reticleMat });
|
| 301 |
reticle.setLocalScale(0.12, 0.005, 0.12);
|
| 302 |
reticle.enabled = false;
|
| 303 |
app.root.addChild(reticle);
|
| 304 |
|
| 305 |
+
// Modèle
|
| 306 |
var modelRoot = new pc.Entity("ModelRoot");
|
| 307 |
modelRoot.enabled = false;
|
| 308 |
app.root.addChild(modelRoot);
|
| 309 |
|
| 310 |
+
var modelLoaded = false, placedOnce = false;
|
|
|
|
| 311 |
|
| 312 |
+
// Ombre de contact (blob)
|
| 313 |
var blobShadowEntity = null;
|
| 314 |
+
var BLOB_SIZE = 0.4, BLOB_OFFSET_Y = 0.005;
|
|
|
|
| 315 |
|
| 316 |
function makeBlobTexture(appRef, size) {
|
| 317 |
var s = size || 256;
|
| 318 |
+
var cvs = document.createElement('canvas'); cvs.width = cvs.height = s;
|
|
|
|
| 319 |
var ctx = cvs.getContext('2d');
|
| 320 |
var r = s * 0.45;
|
| 321 |
+
var grd = ctx.createRadialGradient(s/2, s/2, r*0.2, s/2, s/2, r);
|
| 322 |
+
grd.addColorStop(0, 'rgba(0,0,0,0.5)'); grd.addColorStop(1, 'rgba(0,0,0,0)');
|
| 323 |
+
ctx.fillStyle = grd; ctx.fillRect(0,0,s,s);
|
|
|
|
|
|
|
|
|
|
| 324 |
var tex = new pc.Texture(appRef.graphicsDevice, {
|
| 325 |
+
width: s, height: s, format: pc.PIXELFORMAT_R8_G8_B8_A8,
|
| 326 |
+
mipmaps: true, magFilter: pc.FILTER_LINEAR, minFilter: pc.FILTER_LINEAR_MIPMAP_LINEAR
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
});
|
| 328 |
+
tex.setSource(cvs); return tex;
|
|
|
|
| 329 |
}
|
|
|
|
| 330 |
function createBlobShadowAt(pos, rot) {
|
| 331 |
var tex = makeBlobTexture(app, 256);
|
|
|
|
| 332 |
var blobMat = new pc.StandardMaterial();
|
| 333 |
+
blobMat.diffuse = new pc.Color(0,0,0);
|
| 334 |
blobMat.opacity = 1.0;
|
| 335 |
blobMat.opacityMap = tex;
|
| 336 |
blobMat.opacityMapChannel = 'a';
|
|
|
|
| 339 |
blobMat.depthWrite = false;
|
| 340 |
blobMat.alphaTest = 0;
|
| 341 |
blobMat.update();
|
|
|
|
| 342 |
var e = new pc.Entity("BlobShadow");
|
| 343 |
e.addComponent("render", { type: "plane", castShadows: false, receiveShadows: false });
|
| 344 |
e.render.material = blobMat;
|
|
|
|
| 345 |
e.setPosition(pos.x, pos.y + BLOB_OFFSET_Y, pos.z);
|
| 346 |
e.setRotation(rot);
|
| 347 |
e.setLocalScale(BLOB_SIZE, 1, BLOB_SIZE);
|
|
|
|
| 348 |
app.root.addChild(e);
|
| 349 |
return e;
|
| 350 |
}
|
| 351 |
|
| 352 |
+
// Euler de base & rotation slider
|
| 353 |
+
var baseEulerX = 0, baseEulerZ = 0;
|
|
|
|
|
|
|
|
|
|
| 354 |
var rotationYDeg = 0;
|
| 355 |
function clamp360(d) { return Math.max(0, Math.min(360, d)); }
|
|
|
|
| 356 |
function updateKnobFromY(yDeg) {
|
| 357 |
var t = 1 - (yDeg / 360);
|
| 358 |
rotThumb.style.top = String(t * 100) + "%";
|
|
|
|
| 365 |
modelRoot.setEulerAngles(baseEulerX, y, baseEulerZ);
|
| 366 |
updateKnobFromY(y);
|
| 367 |
}
|
|
|
|
| 368 |
function updateBlobPositionUnder(pos, rotLikePlane) {
|
| 369 |
if (!blobShadowEntity) return;
|
| 370 |
blobShadowEntity.setPosition(pos.x, pos.y + BLOB_OFFSET_Y, pos.z);
|
| 371 |
if (rotLikePlane) blobShadowEntity.setRotation(rotLikePlane);
|
| 372 |
}
|
| 373 |
|
| 374 |
+
// Chargement GLB
|
| 375 |
app.assets.loadFromUrl(GLB_URL, "container", function (err, asset) {
|
| 376 |
if (err) { console.error(err); messageToast("Échec du chargement du modèle GLB."); return; }
|
| 377 |
var instance = asset.resource.instantiateRenderEntity({ castShadows: true, receiveShadows: false });
|
| 378 |
modelRoot.addChild(instance);
|
| 379 |
+
modelRoot.setLocalScale(1,1,1);
|
| 380 |
|
|
|
|
| 381 |
var renders = instance.findComponents('render');
|
| 382 |
for (var ri = 0; ri < renders.length; ri++) {
|
| 383 |
+
var r = renders[ri]; r.castShadows = true;
|
|
|
|
| 384 |
for (var mi = 0; mi < r.meshInstances.length; mi++) {
|
| 385 |
+
var mat = r.meshInstances[mi].material; if (!mat) continue;
|
| 386 |
+
if (mat.diffuse && (mat.diffuse.r !== 1 || mat.diffuse.g !== 1 || mat.diffuse.b !== 1)) mat.diffuse.set(1,1,1);
|
|
|
|
|
|
|
|
|
|
| 387 |
if (typeof mat.useSkybox !== "undefined") mat.useSkybox = true;
|
| 388 |
mat.update();
|
| 389 |
}
|
|
|
|
| 393 |
baseEulerX = initE.x; baseEulerZ = initE.z;
|
| 394 |
|
| 395 |
modelLoaded = true;
|
|
|
|
| 396 |
messageCenterBig("Modèle chargé. Touchez l’écran pour démarrer l’AR.");
|
| 397 |
});
|
| 398 |
|
| 399 |
if (!app.xr.supported) { messageToast("WebXR n’est pas supporté sur cet appareil."); return; }
|
| 400 |
|
| 401 |
+
// Slider robuste (pointer events capture)
|
| 402 |
+
var isUIInteracting = false, isTrackDragging = false, activePointerId = null;
|
|
|
|
|
|
|
|
|
|
| 403 |
function insideTrack(target) { return rotTrack.contains(target); }
|
| 404 |
function degFromPointer(e) {
|
| 405 |
var rect = rotTrack.getBoundingClientRect();
|
| 406 |
var y = (e.clientY != null) ? e.clientY : ((e.touches && e.touches[0] && e.touches[0].clientY) || 0);
|
| 407 |
+
var ratio = (y - rect.top) / rect.height; var t = Math.max(0, Math.min(1, ratio));
|
| 408 |
+
return (1 - t) * 360;
|
|
|
|
| 409 |
}
|
|
|
|
| 410 |
function onPointerDownCapture(e) {
|
| 411 |
if (!insideTrack(e.target)) return;
|
| 412 |
+
isUIInteracting = true; isTrackDragging = true;
|
|
|
|
| 413 |
activePointerId = (e.pointerId != null) ? e.pointerId : 1;
|
| 414 |
+
if (rotTrack.setPointerCapture) { try { rotTrack.setPointerCapture(activePointerId); } catch (_er) {} }
|
|
|
|
|
|
|
| 415 |
applyRotationY(degFromPointer(e));
|
| 416 |
+
e.preventDefault(); e.stopPropagation();
|
|
|
|
| 417 |
}
|
| 418 |
function onPointerMoveCapture(e) {
|
| 419 |
if (!isTrackDragging || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
|
| 420 |
applyRotationY(degFromPointer(e));
|
| 421 |
+
e.preventDefault(); e.stopPropagation();
|
|
|
|
| 422 |
}
|
| 423 |
function endDrag(e) {
|
| 424 |
if (!isTrackDragging || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
|
| 425 |
+
isTrackDragging = false; isUIInteracting = false;
|
| 426 |
+
if (rotTrack.releasePointerCapture) { try { rotTrack.releasePointerCapture(activePointerId); } catch (_er) {} }
|
| 427 |
+
activePointerId = null; e.preventDefault(); e.stopPropagation();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
}
|
|
|
|
| 429 |
document.addEventListener("pointerdown", onPointerDownCapture, { capture: true, passive: false });
|
| 430 |
document.addEventListener("pointermove", onPointerMoveCapture, { capture: true, passive: false });
|
| 431 |
document.addEventListener("pointerup", endDrag, { capture: true, passive: false });
|
| 432 |
document.addEventListener("pointercancel", endDrag, { capture: true, passive: false });
|
| 433 |
|
| 434 |
+
// Démarrage AR (tap écran)
|
| 435 |
function activateAR() {
|
| 436 |
if (!app.xr.isAvailable(pc.XRTYPE_AR)) { messageToast("AR immersive indisponible sur cet appareil."); return; }
|
| 437 |
if (!app.xr.domOverlay) app.xr.domOverlay = {};
|
|
|
|
| 440 |
requiredFeatures: ["hit-test", "dom-overlay"],
|
| 441 |
domOverlay: { root: app.xr.domOverlay.root },
|
| 442 |
callback: function (err) {
|
| 443 |
+
if (err) { console.error("Échec du démarrage AR :", err); messageToast("Échec du démarrage AR : " + (err.message || err)); }
|
|
|
|
|
|
|
|
|
|
| 444 |
}
|
| 445 |
});
|
| 446 |
}
|
|
|
|
| 447 |
app.mouse.on("mousedown", function () { if (!app.xr.active && !isUIInteracting) activateAR(); });
|
| 448 |
if (app.touch) {
|
| 449 |
app.touch.on("touchend", function (evt) {
|
| 450 |
if (!app.xr.active && !isUIInteracting) activateAR();
|
| 451 |
+
evt.event.preventDefault(); evt.event.stopPropagation();
|
|
|
|
| 452 |
});
|
| 453 |
}
|
| 454 |
app.keyboard.on("keydown", function (evt) { if (evt.key === pc.KEY_ESCAPE && app.xr.active) app.xr.end(); });
|
| 455 |
|
| 456 |
// Hit-test HORIZONTAL uniquement
|
| 457 |
+
var TMP_IN = new pc.Vec3(0,1,0), TMP_OUT = new pc.Vec3();
|
|
|
|
| 458 |
function isHorizontalUpFacing(rot, minDot) {
|
| 459 |
var md = (typeof minDot === "number") ? minDot : 0.75;
|
| 460 |
rot.transformVector(TMP_IN, TMP_OUT);
|
| 461 |
+
return TMP_OUT.y >= md;
|
| 462 |
}
|
|
|
|
|
|
|
| 463 |
app.xr.hitTest.on("available", function () {
|
| 464 |
app.xr.hitTest.start({
|
| 465 |
entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
|
|
|
|
| 467 |
if (err) { messageToast("Le AR hit test n’a pas pu démarrer."); return; }
|
| 468 |
hitSource.on("result", function (pos, rot) {
|
| 469 |
if (!isHorizontalUpFacing(rot)) return;
|
| 470 |
+
reticle.enabled = true; reticle.setPosition(pos); reticle.setRotation(rot);
|
|
|
|
|
|
|
|
|
|
| 471 |
|
| 472 |
if (modelLoaded && !placedOnce) {
|
| 473 |
+
modelRoot.enabled = true; modelRoot.setPosition(pos);
|
|
|
|
|
|
|
|
|
|
| 474 |
blobShadowEntity = createBlobShadowAt(pos, rot);
|
| 475 |
|
| 476 |
+
var e = new pc.Vec3(); rot.getEulerAngles(e);
|
| 477 |
+
var y0 = ((e.y % 360) + 360) % 360; applyRotationY(y0);
|
|
|
|
|
|
|
| 478 |
|
| 479 |
+
placedOnce = true; rotRangeInput.disabled = false;
|
|
|
|
|
|
|
|
|
|
| 480 |
messageToast("Objet placé. Glissez pour déplacer, tournez-le avec le slider →");
|
| 481 |
}
|
| 482 |
});
|
|
|
|
| 484 |
});
|
| 485 |
});
|
| 486 |
|
| 487 |
+
// Drag XR — ignoré si UI active
|
| 488 |
var isModelDragging = false;
|
| 489 |
app.xr.input.on("add", function (inputSource) {
|
| 490 |
inputSource.on("selectstart", function () {
|
| 491 |
if (isUIInteracting) return;
|
| 492 |
if (!placedOnce || !modelLoaded) return;
|
|
|
|
| 493 |
inputSource.hitTestStart({
|
| 494 |
entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
|
| 495 |
callback: function (err, transientSource) {
|
| 496 |
if (err) return;
|
| 497 |
isModelDragging = true;
|
|
|
|
| 498 |
transientSource.on("result", function (pos, rot) {
|
| 499 |
if (!isModelDragging) return;
|
| 500 |
if (!isHorizontalUpFacing(rot)) return;
|
| 501 |
+
modelRoot.setPosition(pos); updateBlobPositionUnder(pos, rot);
|
|
|
|
| 502 |
});
|
|
|
|
| 503 |
transientSource.once("remove", function () { isModelDragging = false; });
|
| 504 |
}
|
| 505 |
});
|
|
|
|
| 507 |
inputSource.on("selectend", function () { isModelDragging = false; });
|
| 508 |
});
|
| 509 |
|
| 510 |
+
// Desktop : rotation souris
|
| 511 |
+
var isRotateMode = false, lastMouseX = 0;
|
|
|
|
| 512 |
var ROTATE_SENSITIVITY = 0.25;
|
| 513 |
app.mouse.on("mousedown", function (e) {
|
| 514 |
if (!app.xr.active || !placedOnce || isUIInteracting) return;
|
| 515 |
+
if (e.button === 0 && !e.shiftKey) { isModelDragging = true; }
|
| 516 |
+
else if (e.button === 2 || (e.button === 0 && e.shiftKey)) { isRotateMode = true; lastMouseX = e.x; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 517 |
});
|
| 518 |
app.mouse.on("mousemove", function (e) {
|
| 519 |
if (!app.xr.active || !placedOnce || isUIInteracting) return;
|
| 520 |
if (isModelDragging) {
|
| 521 |
if (reticle.enabled) {
|
| 522 |
+
var p = reticle.getPosition(); modelRoot.setPosition(p); updateBlobPositionUnder(p, null);
|
|
|
|
|
|
|
| 523 |
}
|
| 524 |
} else if (isRotateMode && modelRoot.enabled) {
|
| 525 |
+
var dx = e.x - lastMouseX; lastMouseX = e.x;
|
|
|
|
| 526 |
applyRotationY(rotationYDeg + dx * ROTATE_SENSITIVITY);
|
| 527 |
}
|
| 528 |
});
|
| 529 |
app.mouse.on("mouseup", function () { isModelDragging = false; isRotateMode = false; });
|
| 530 |
window.addEventListener("contextmenu", function (e) { e.preventDefault(); });
|
| 531 |
|
| 532 |
+
// Slider (input range, accessibilité)
|
| 533 |
rotRangeInput.disabled = true;
|
| 534 |
rotRangeInput.addEventListener("input", function (e) {
|
| 535 |
if (!modelRoot.enabled) return;
|
| 536 |
+
var v = parseFloat(e.target.value || "0"); applyRotationY(v);
|
|
|
|
| 537 |
}, { passive: true });
|
| 538 |
|
| 539 |
+
// Événements AR
|
| 540 |
+
app.xr.on("start", function () { messageToast("Session AR démarrée. Visez le sol pour détecter un plan…"); reticle.enabled = true; });
|
|
|
|
|
|
|
|
|
|
| 541 |
app.xr.on("end", function () {
|
| 542 |
messageToast("Session AR terminée.");
|
| 543 |
+
reticle.enabled = false; isModelDragging = false; isRotateMode = false; rotRangeInput.disabled = true;
|
|
|
|
|
|
|
|
|
|
| 544 |
});
|
| 545 |
app.xr.on("available:" + pc.XRTYPE_AR, function (a) {
|
| 546 |
if (!a) messageToast("AR immersive indisponible.");
|
|
|
|
| 553 |
else messageToast("Chargement du modèle…");
|
| 554 |
}
|
| 555 |
})();
|
| 556 |
+
</script>
|