/* script_ar.js — AR PlayCanvas + GLB HuggingFace (GLB via config.json) - Lit config.json (data-config) => { "glb_url": "..." } - Hit-test AR (HORIZONTAUX uniquement) + placement + drag (XR) - Slider custom Yaw (360° en haut → 0° en bas), knob centré, rail plein - Blocage total des interactions scène quand on touche le slider - Éclairage PBR par défaut (sans WebXR light estimation) - Blob Shadow (ombre de contact) sous l’objet */ (function () { // ===== Récup config.json depuis data-config ===== function findConfigUrl() { var el = document.currentScript || null; if (!el) { var scripts = document.getElementsByTagName('script'); for (var i = scripts.length - 1; i >= 0; i--) { if (scripts[i].getAttribute && scripts[i].getAttribute('data-config')) { el = scripts[i]; break; } } } if (!el) return null; var url = el.getAttribute('data-config'); return url || null; } function timeout(ms) { return new Promise(function (_res, rej) { setTimeout(function () { rej(new Error("timeout")); }, ms); }); } async function loadConfigJson(url) { if (!url) return null; try { var resp = await fetch(url, { cache: 'no-store' }); if (!resp.ok) throw new Error("HTTP " + resp.status); var json = await resp.json(); return json; } catch (e) { console.error("Erreur chargement config.json:", e); return null; } } // ===== PlayCanvas version fixée ===== var PC_VERSION = "2.11.7"; var PC_URLS = { esm: ["https://cdn.jsdelivr.net/npm/playcanvas@" + PC_VERSION + "/build/playcanvas.min.mjs"], umd: ["https://cdn.jsdelivr.net/npm/playcanvas@" + PC_VERSION + "/build/playcanvas.min.js"] }; async function loadPlayCanvasRobust(opts) { opts = opts || {}; var esmFirst = (typeof opts.esmFirst === "boolean") ? opts.esmFirst : true; var loadTimeoutMs = (typeof opts.loadTimeoutMs === "number") ? opts.loadTimeoutMs : 15000; if (window.pc && window.pc.Application) return window.pc; async function tryESM() { for (var i = 0; i < PC_URLS.esm.length; i++) { var url = PC_URLS.esm[i]; try { var mod = await Promise.race([import(/* @vite-ignore */ url), timeout(loadTimeoutMs)]); var ns = (mod && (mod.pc || mod["default"])) || mod; if (ns && ns.Application) { if (!window.pc) window.pc = ns; return window.pc; } } catch (e) { /* continue */ } } throw new Error("ESM failed"); } async function tryUMD() { for (var j = 0; j < PC_URLS.umd.length; j++) { var url2 = PC_URLS.umd[j]; try { await Promise.race([ new Promise(function (res, rej) { var s = document.createElement("script"); s.src = url2; s.async = true; s.onload = function () { res(); }; s.onerror = function () { rej(new Error("script error")); }; document.head.appendChild(s); }), timeout(loadTimeoutMs) ]); if (window.pc && window.pc.Application) return window.pc; } catch (e) { /* continue */ } } throw new Error("UMD failed"); } try { if (esmFirst) return await tryESM(); return await tryUMD(); } catch (e) { if (esmFirst) return await tryUMD(); return await tryESM(); } } // ===== UI / Overlay ===== var css = [ ".pc-ar-msg{position:fixed;left:50%;transform:translateX(-50%);bottom:16px;z-index:2;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}", "#xr-overlay-root{position:fixed;inset:0;z-index:9999;pointer-events:none}", ".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}", ".ar-ui .label{font-size:12px;text-align:center;opacity:.95}", ".rotY-wrap{position:relative;width:48px;height:200px;display:flex;align-items:center;justify-content:center;touch-action:none;overflow:visible;pointer-events:auto}", ".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}", ".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}", ".ar-ui input[type=\"range\"].rotY{position:absolute;opacity:0;pointer-events:none;width:0;height:0}", ".ar-ui .val{font-size:12px;opacity:.95}" ].join("\n"); var styleTag = document.createElement("style"); styleTag.textContent = css; document.head.appendChild(styleTag); function ensureOverlayRoot() { var r = document.getElementById("xr-overlay-root"); if (!r) { r = document.createElement("div"); r.id = "xr-overlay-root"; document.body.appendChild(r); } return r; } var overlayRoot = ensureOverlayRoot(); function message(msg) { var el = overlayRoot.querySelector(".pc-ar-msg"); if (!el) { el = document.createElement("div"); el.className = "pc-ar-msg"; overlayRoot.appendChild(el); } el.textContent = msg; } function ensureCanvas() { var c = document.getElementById("application-canvas"); if (!c) { c = document.createElement("canvas"); c.id = "application-canvas"; c.style.width = "100%"; c.style.height = "100%"; document.body.appendChild(c); } return c; } function ensureSliderUI() { var p = overlayRoot.querySelector(".ar-ui"); if (p) return p; p = document.createElement("div"); p.className = "ar-ui"; p.innerHTML = '
Rotation
' + '
' + '
' + '
' + ' ' + '
' + '
'; overlayRoot.appendChild(p); return p; } // ===== Boot : charge config.json => GLB_URL => PlayCanvas => App ===== (async function () { // 1) Lire la config (glb_url) var cfgUrl = findConfigUrl(); var cfg = await loadConfigJson(cfgUrl); var GLB_URL = (cfg && typeof cfg.glb_url === "string" && cfg.glb_url) ? cfg.glb_url : "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/AR/tests/danae_no_metallic.glb"; // 2) Charger PlayCanvas try { await loadPlayCanvasRobust({ esmFirst: true, loadTimeoutMs: 15000 }); } catch (e) { console.error("Chargement PlayCanvas échoué ->", e); message("Impossible de charger PlayCanvas (réseau/CDN). Réessaie plus tard."); return; } // 3) Lancer l'app avec l'URL lue initARApp(GLB_URL); })(); // ===== App ===== function initARApp(GLB_URL) { var pc = window.pc; var canvas = ensureCanvas(); var ui = ensureSliderUI(); var rotWrap = ui.querySelector("#ar-rotY-wrap"); var rotKnob = ui.querySelector("#ar-rotY-knob"); var rotYInput = ui.querySelector("#ar-rotY"); var rotYVal = ui.querySelector("#ar-rotY-val"); window.focus(); var app = new pc.Application(canvas, { mouse: new pc.Mouse(canvas), touch: new pc.TouchDevice(canvas), keyboard: new pc.Keyboard(window), graphicsDeviceOptions: { alpha: true } }); app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW); app.setCanvasResolution(pc.RESOLUTION_AUTO); app.graphicsDevice.maxPixelRatio = window.devicePixelRatio || 1; var onResize = function () { app.resizeCanvas(); }; window.addEventListener("resize", onResize); app.on("destroy", function () { window.removeEventListener("resize", onResize); }); app.start(); // ===== Rendu / PBR defaults ===== app.scene.gammaCorrection = pc.GAMMA_SRGB; app.scene.toneMapping = pc.TONEMAP_ACES; app.scene.exposure = 1; app.scene.ambientLight = new pc.Color(1, 1, 1); // Camera + lumière var camera = new pc.Entity("Camera"); camera.addComponent("camera", { clearColor: new pc.Color(0, 0, 0, 0), farClip: 10000 }); app.root.addChild(camera); var light = new pc.Entity("Light"); light.addComponent("light", { type: "directional", intensity: 1.0, castShadows: true, color: new pc.Color(1, 1, 1) }); light.setLocalEulerAngles(45, 30, 0); app.root.addChild(light); // Réticule var reticleMat = new pc.StandardMaterial(); reticleMat.diffuse = new pc.Color(0.2, 0.8, 1.0); reticleMat.opacity = 0.85; reticleMat.blendType = pc.BLEND_NORMAL; reticleMat.update(); var reticle = new pc.Entity("Reticle"); reticle.addComponent("render", { type: "torus", material: reticleMat }); reticle.setLocalScale(0.12, 0.005, 0.12); reticle.enabled = false; app.root.addChild(reticle); // Modèle var modelRoot = new pc.Entity("ModelRoot"); modelRoot.enabled = false; app.root.addChild(modelRoot); var modelLoaded = false, placedOnce = false; // ===== Blob Shadow ===== var blob = null; // entité plane de l’ombre var BLOB_SIZE = 0.4; var BLOB_OFFSET_Y = 0.005; function makeBlobTexture(app, size) { size = size || 256; var cvs = document.createElement('canvas'); cvs.width = cvs.height = size; var ctx = cvs.getContext('2d'); var r = size * 0.45; var grd = ctx.createRadialGradient(size/2, size/2, r*0.2, size/2, size/2, r); grd.addColorStop(0, 'rgba(0,0,0,0.5)'); // centre sombre grd.addColorStop(1, 'rgba(0,0,0,0.0)'); // bords transparents ctx.fillStyle = grd; ctx.fillRect(0, 0, size, size); var tex = new pc.Texture(app.graphicsDevice, { width: size, height: size, format: pc.PIXELFORMAT_R8_G8_B8_A8, mipmaps: true, magFilter: pc.FILTER_LINEAR, minFilter: pc.FILTER_LINEAR_MIPMAP_LINEAR }); tex.setSource(cvs); return tex; } function createBlobShadowAt(pos, rot) { var tex = makeBlobTexture(app, 256); var blobMat = new pc.StandardMaterial(); blobMat.diffuse = new pc.Color(0, 0, 0); blobMat.opacity = 1.0; blobMat.opacityMap = tex; // utilise alpha pour la transparence blobMat.opacityMapChannel = 'a'; blobMat.useLighting = false; blobMat.blendType = pc.BLEND_NORMAL; blobMat.depthWrite = false; blobMat.alphaTest = 0; blobMat.update(); var e = new pc.Entity("BlobShadow"); e.addComponent("render", { type: "plane", castShadows: false, receiveShadows: false }); e.render.material = blobMat; e.setPosition(pos.x, pos.y + BLOB_OFFSET_Y, pos.z); e.setRotation(rot); e.setLocalScale(BLOB_SIZE, 1, BLOB_SIZE); app.root.addChild(e); return e; } // Euler base var baseEulerX = 0, baseEulerZ = 0; // Rotation via slider var rotationYDeg = 0; function clamp360(d) { return Math.max(0, Math.min(360, d)); } function updateKnobFromY(yDeg) { var t = 1 - (yDeg / 360); rotKnob.style.top = String(t * 100) + "%"; rotYInput.value = String(Math.round(yDeg)); rotYVal.textContent = String(Math.round(yDeg)) + "°"; } function applyRotationY(deg) { var y = clamp360(deg); rotationYDeg = y; modelRoot.setEulerAngles(baseEulerX, y, baseEulerZ); updateKnobFromY(y); } function updateBlobPositionUnder(pos, rotLikePlane) { if (!blob) return; blob.setPosition(pos.x, pos.y + BLOB_OFFSET_Y, pos.z); if (rotLikePlane) blob.setRotation(rotLikePlane); } // Chargement GLB (depuis config.json) app.assets.loadFromUrl(GLB_URL, "container", function (err, asset) { if (err) { console.error(err); message("Échec du chargement du modèle GLB."); return; } var instance = asset.resource.instantiateRenderEntity({ castShadows: true, receiveShadows: false }); modelRoot.addChild(instance); modelRoot.setLocalScale(1, 1, 1); // Fix matériaux (anti-objets trop sombres) var renders = instance.findComponents('render'); for (var ri = 0; ri < renders.length; ri++) { var r = renders[ri]; r.castShadows = true; for (var mi = 0; mi < r.meshInstances.length; mi++) { var mat = r.meshInstances[mi].material; if (!mat) continue; if (mat.diffuse && (mat.diffuse.r !== 1 || mat.diffuse.g !== 1 || mat.diffuse.b !== 1)) { mat.diffuse.set(1, 1, 1); } if (typeof mat.useSkybox !== "undefined") mat.useSkybox = true; mat.update(); } } var initE = modelRoot.getEulerAngles(); baseEulerX = initE.x; baseEulerZ = initE.z; modelLoaded = true; message("Modèle chargé. Touchez l’écran pour démarrer l’AR."); }); if (!app.xr.supported) { message("WebXR n’est pas supporté sur cet appareil."); return; } // ===== Slider fiable : Pointer Events en CAPTURE ===== var uiInteracting = false; var draggingWrap = false; var activePointerId = null; function insideWrap(target) { return rotWrap.contains(target); } function degFromPointer(e) { var rect = rotWrap.getBoundingClientRect(); var y = (e.clientY != null) ? e.clientY : ((e.touches && e.touches[0] && e.touches[0].clientY) || 0); var ratio = (y - rect.top) / rect.height; var t = Math.max(0, Math.min(1, ratio)); return (1 - t) * 360; } function onPointerDownCapture(e) { if (!insideWrap(e.target)) return; uiInteracting = true; draggingWrap = true; activePointerId = (e.pointerId != null) ? e.pointerId : 1; if (rotWrap.setPointerCapture) { try { rotWrap.setPointerCapture(activePointerId); } catch (er) {} } applyRotationY(degFromPointer(e)); e.preventDefault(); e.stopPropagation(); } function onPointerMoveCapture(e) { if (!draggingWrap || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return; applyRotationY(degFromPointer(e)); e.preventDefault(); e.stopPropagation(); } function endDrag(e) { if (!draggingWrap || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return; draggingWrap = false; uiInteracting = false; if (rotWrap.releasePointerCapture) { try { rotWrap.releasePointerCapture(activePointerId); } catch (er) {} } activePointerId = null; e.preventDefault(); e.stopPropagation(); } document.addEventListener("pointerdown", onPointerDownCapture, { capture: true, passive: false }); document.addEventListener("pointermove", onPointerMoveCapture, { capture: true, passive: false }); document.addEventListener("pointerup", endDrag, { capture: true, passive: false }); document.addEventListener("pointercancel", endDrag, { capture: true, passive: false }); // --- Démarrage AR function activateAR() { if (!app.xr.isAvailable(pc.XRTYPE_AR)) { message("AR immersive indisponible sur cet appareil."); return; } if (!app.xr.domOverlay) app.xr.domOverlay = {}; app.xr.domOverlay.root = document.getElementById("xr-overlay-root"); camera.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, { requiredFeatures: ["hit-test", "dom-overlay"], domOverlay: { root: app.xr.domOverlay.root }, callback: function (err) { if (err) { console.error("Échec du démarrage AR :", err); message("Échec du démarrage AR : " + (err.message || err)); } } }); } app.mouse.on("mousedown", function () { if (!app.xr.active && !uiInteracting) activateAR(); }); if (app.touch) { app.touch.on("touchend", function (evt) { if (!app.xr.active && !uiInteracting) activateAR(); evt.event.preventDefault(); evt.event.stopPropagation(); }); } app.keyboard.on("keydown", function (evt) { if (evt.key === pc.KEY_ESCAPE && app.xr.active) app.xr.end(); }); // ====== Filtre HORIZONTAL uniquement ====== var TMP_IN = new pc.Vec3(0, 1, 0), TMP_OUT = new pc.Vec3(); function isHorizontalUpFacing(rot, minDot) { minDot = (typeof minDot === "number") ? minDot : 0.75; rot.transformVector(TMP_IN, TMP_OUT); return TMP_OUT.y >= minDot; } // Hit Test global (réticule + 1er placement) app.xr.hitTest.on("available", function () { app.xr.hitTest.start({ entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE], callback: function (err, hitSource) { if (err) { message("Le AR hit test n’a pas pu démarrer."); return; } hitSource.on("result", function (pos, rot) { if (!isHorizontalUpFacing(rot)) return; reticle.enabled = true; reticle.setPosition(pos); reticle.setRotation(rot); if (modelLoaded && !placedOnce) { modelRoot.enabled = true; modelRoot.setPosition(pos); // Ombre de contact au placement initial blob = createBlobShadowAt(pos, rot); var e = new pc.Vec3(); rot.getEulerAngles(e); var y0 = ((e.y % 360) + 360) % 360; applyRotationY(y0); placedOnce = true; rotYInput.disabled = false; message("Objet placé. Glissez pour déplacer, tournez-le avec le slider →"); } }); } }); }); // Déplacement XR (drag) — ignoré si UI active var isDragging = false; app.xr.input.on("add", function (inputSource) { inputSource.on("selectstart", function () { if (uiInteracting) return; if (!placedOnce || !modelLoaded) return; inputSource.hitTestStart({ entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE], callback: function (err, transientSource) { if (err) return; isDragging = true; transientSource.on("result", function (pos, rot) { if (!isDragging) return; if (!isHorizontalUpFacing(rot)) return; modelRoot.setPosition(pos); updateBlobPositionUnder(pos, rot); }); transientSource.once("remove", function () { isDragging = false; }); } }); }); inputSource.on("selectend", function () { isDragging = false; }); }); // Desktop : rotation souris (ignore si UI) var rotateMode = false, lastMouseX = 0; var ROTATE_SENSITIVITY = 0.25; app.mouse.on("mousedown", function (e) { if (!app.xr.active || !placedOnce || uiInteracting) return; if (e.button === 0 && !e.shiftKey) { isDragging = true; } else if (e.button === 2 || (e.button === 0 && e.shiftKey)) { rotateMode = true; lastMouseX = e.x; } }); app.mouse.on("mousemove", function (e) { if (!app.xr.active || !placedOnce || uiInteracting) return; if (isDragging) { if (reticle.enabled) { var p = reticle.getPosition(); modelRoot.setPosition(p); updateBlobPositionUnder(p, null); } } else if (rotateMode && modelRoot.enabled) { var dx = e.x - lastMouseX; lastMouseX = e.x; applyRotationY(rotationYDeg + dx * ROTATE_SENSITIVITY); } }); app.mouse.on("mouseup", function () { isDragging = false; rotateMode = false; }); window.addEventListener("contextmenu", function (e) { e.preventDefault(); }); // Slider (accessibilité clavier) rotYInput.disabled = true; rotYInput.addEventListener("input", function (e) { if (!modelRoot.enabled) return; var v = parseFloat(e.target.value || "0"); applyRotationY(v); }, { passive: true }); // AR events app.xr.on("start", function () { message("Session AR démarrée. Visez le sol pour détecter un plan…"); reticle.enabled = true; }); app.xr.on("end", function () { message("Session AR terminée."); reticle.enabled = false; isDragging = false; rotateMode = false; rotYInput.disabled = true; }); app.xr.on("available:" + pc.XRTYPE_AR, function (a) { if (!a) message("AR immersive indisponible."); else if (!app.xr.hitTest.supported) message("AR Hit Test non supporté."); else message(modelLoaded ? "Touchez l’écran pour démarrer l’AR." : "Chargement du modèle…"); }); if (!app.xr.isAvailable(pc.XRTYPE_AR)) message("AR immersive indisponible."); else if (!app.xr.hitTest.supported) message("AR Hit Test non supporté."); else message("Chargement du modèle…"); } })();