| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | (function () { |
| | |
| | |
| | |
| | function getCurrentScript() { |
| | return document.currentScript || (function () { |
| | var scripts = document.getElementsByTagName('script'); |
| | return scripts[scripts.length - 1] || null; |
| | })(); |
| | } |
| |
|
| | function findConfigUrl() { |
| | var el = getCurrentScript(); |
| | if (!el) return null; |
| | var url = el.getAttribute('data-config'); |
| | return url || null; |
| | } |
| |
|
| | function isIOS() { |
| | return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; |
| | } |
| |
|
| | 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; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | 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( 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) { } |
| | } |
| | 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) { } |
| | } |
| | throw new Error("UMD failed"); |
| | } |
| |
|
| | try { |
| | if (esmFirst) return await tryESM(); |
| | return await tryUMD(); |
| | } catch (e) { |
| | if (esmFirst) return await tryUMD(); |
| | return await tryESM(); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | var css = [ |
| | "#ar-overlay-root{position:fixed;inset:0;z-index:10001;pointer-events:none}", |
| | ".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}", |
| | ".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)}", |
| | "#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}", |
| | "#ar-start-button[disabled]{opacity:.5;cursor:not-allowed}", |
| | "#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}", |
| | "#ar-ios-quicklook-button img{display:block;height:60px;width:auto;box-shadow:0 10px 28px rgba(0,0,0,.28);border-radius:14px}", |
| | ".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}", |
| | ".ar-rotation-panel .label{font-size:12px;text-align:center;opacity:.95}", |
| | "#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}", |
| | ".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}", |
| | "#ar-rotation-knob{position:absolute;left:50%;width:22px;height:22px;border-radius:50%;background:#fff;box-shadow:0 2px 8px rgba(0,0,0,.35);transform:translate(-50%,-50%);top:50%;will-change:top;touch-action:none;pointer-events:none}", |
| | "input#ar-rotation-range.rotY{position:absolute;opacity:0;pointer-events:none;width:0;height:0}", |
| | "#ar-rotation-value{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("ar-overlay-root"); |
| | if (!r) { |
| | r = document.createElement("div"); |
| | r.id = "ar-overlay-root"; |
| | document.body.appendChild(r); |
| | } |
| | return r; |
| | } |
| | var overlayRoot = ensureOverlayRoot(); |
| |
|
| | function message(txt) { |
| | var el = overlayRoot.querySelector(".pc-ar-msg"); |
| | if (!el) { |
| | el = document.createElement("div"); |
| | el.className = "pc-ar-msg"; |
| | overlayRoot.appendChild(el); |
| | } |
| | var isStartMsg = /Appuyez sur .Lancer l.?AR./i.test(txt); |
| | el.classList.toggle("pc-ar-msg--centerBig", isStartMsg); |
| | el.textContent = txt; |
| | } |
| |
|
| | |
| | function buildQuickLookHref(usdzUrl) { |
| | try { |
| | var u = new URL(usdzUrl, window.location.href); |
| | var hash = u.hash ? u.hash.replace(/^#/, '') : ''; |
| | var params = new URLSearchParams(hash); |
| | params.set('allowsContentScaling', '0'); |
| | u.hash = params.toString(); |
| | return u.toString(); |
| | } catch (e) { |
| | return usdzUrl + (usdzUrl.indexOf('#') >= 0 ? '&' : '#') + 'allowsContentScaling=0'; |
| | } |
| | } |
| |
|
| | |
| | function ensureQuickLookButton(USDZ_URL) { |
| | var btn = document.getElementById("ar-ios-quicklook-button"); |
| | if (btn) return btn; |
| |
|
| | var anchor = document.createElement("a"); |
| | anchor.id = "ar-ios-quicklook-button"; |
| | anchor.setAttribute("rel", "ar"); |
| | anchor.setAttribute("href", buildQuickLookHref(USDZ_URL)); |
| |
|
| | var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="240" height="60">' + |
| | '<rect rx="14" ry="14" width="240" height="60" fill="black"/>' + |
| | '<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>' + |
| | '</svg>'; |
| |
|
| | var img = document.createElement("img"); |
| | img.alt = "Lancer l’AR"; |
| | img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg); |
| |
|
| | anchor.appendChild(img); |
| | document.body.appendChild(anchor); |
| | return anchor; |
| | } |
| |
|
| | |
| | function ensureARStartButton(onClick) { |
| | var btn = document.getElementById("ar-start-button"); |
| | if (!btn) { |
| | btn = document.createElement("button"); |
| | btn.id = "ar-start-button"; |
| | btn.type = "button"; |
| | btn.textContent = "Lancer l’AR"; |
| | btn.addEventListener("click", function (e) { |
| | e.preventDefault(); |
| | e.stopPropagation(); |
| | if (typeof onClick === "function") onClick(); |
| | }); |
| | document.body.appendChild(btn); |
| | } |
| | return btn; |
| | } |
| |
|
| | |
| | |
| | |
| | function ensureCanvas() { |
| | var scriptEl = getCurrentScript(); |
| | var mountSel = scriptEl && scriptEl.getAttribute('data-mount'); |
| | var desiredHeight = (scriptEl && scriptEl.getAttribute('data-height')) || '70vh'; |
| |
|
| | var mountEl = null; |
| | if (mountSel) mountEl = document.querySelector(mountSel); |
| | if (!mountEl) { |
| | mountEl = document.createElement('div'); |
| | mountEl.id = 'ar-mount-fallback'; |
| | document.body.insertBefore(mountEl, document.body.firstChild); |
| | } |
| |
|
| | var mountStyle = mountEl.style; |
| | if (!mountStyle.position) mountStyle.position = 'relative'; |
| | mountStyle.width = mountStyle.width || '100%'; |
| | mountStyle.minHeight = mountStyle.minHeight || desiredHeight; |
| | mountStyle.touchAction = mountStyle.touchAction || 'manipulation'; |
| | mountStyle.webkitTapHighlightColor = 'transparent'; |
| |
|
| | var canvas = mountEl.querySelector('#application-canvas'); |
| | if (!canvas) { |
| | canvas = document.createElement('canvas'); |
| | canvas.id = 'application-canvas'; |
| | mountEl.appendChild(canvas); |
| | } |
| |
|
| | var cs = canvas.style; |
| | cs.position = 'absolute'; |
| | cs.left = '0'; |
| | cs.top = '0'; |
| | cs.width = '100%'; |
| | cs.height = '100%'; |
| | cs.display = 'block'; |
| |
|
| | try { mountEl.scrollIntoView({ behavior: 'instant', block: 'start' }); } catch (_) {} |
| |
|
| | return canvas; |
| | } |
| |
|
| | function ensureRotationPanel() { |
| | var p = overlayRoot.querySelector(".ar-rotation-panel"); |
| | if (p) return p; |
| | p = document.createElement("div"); |
| | p.className = "ar-rotation-panel"; |
| | p.innerHTML = |
| | '<div class="label">Rotation</div>' + |
| | '<div id="ar-rotation-track" class="rotY-wrap">' + |
| | ' <div class="rotY-rail"></div>' + |
| | ' <div id="ar-rotation-knob" class="rotY-knob"></div>' + |
| | ' <input id="ar-rotation-range" class="rotY" type="range" min="0" max="360" step="1" value="0"/>' + |
| | '</div>' + |
| | '<div id="ar-rotation-value" class="val">0°</div>'; |
| | overlayRoot.appendChild(p); |
| | return p; |
| | } |
| |
|
| | |
| | |
| | |
| | (async function () { |
| | 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"; |
| | var USDZ_URL = (cfg && typeof cfg.usdz_url === "string" && cfg.usdz_url) ? |
| | cfg.usdz_url : |
| | null; |
| |
|
| | |
| | var PLACE_ON_FLOOR = true; |
| | if (cfg && typeof cfg.sol === "boolean") PLACE_ON_FLOOR = !!cfg.sol; |
| |
|
| | |
| | if (isIOS()) { |
| | if (USDZ_URL) { |
| | ensureQuickLookButton(USDZ_URL); |
| |
|
| | if (PLACE_ON_FLOOR) { |
| | message("Modèle chargé. Appuyez sur « Lancer l’AR ». Dans Quick Look, placez l’objet sur le sol et ajustez-le avec les gestes iOS."); |
| | } else { |
| | message("Modèle chargé. Appuyez sur « Lancer l’AR ». Sur iOS l’alignement au mur n’est pas disponible. "+ |
| | "Glissez deux doigts pour les déplacements haut/bas. Glissez un doigts pour les déplacements avant/arrière/gauche/droite"); |
| | } |
| | } else { |
| | message("iOS détecté, mais aucun 'usdz_url' dans config.json."); |
| | } |
| | return; |
| | } |
| |
|
| | |
| | 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; |
| | } |
| | initARApp(GLB_URL, PLACE_ON_FLOOR); |
| | })(); |
| |
|
| | |
| | |
| | |
| | function initARApp(GLB_URL, PLACE_ON_FLOOR) { |
| | var pc = window.pc; |
| |
|
| | var canvas = ensureCanvas(); |
| | var rotationPanel = ensureRotationPanel(); |
| | var rotTrack = rotationPanel.querySelector("#ar-rotation-track"); |
| | var rotKnob = rotationPanel.querySelector("#ar-rotation-knob"); |
| | var rotInput = rotationPanel.querySelector("#ar-rotation-range"); |
| | var rotVal = rotationPanel.querySelector("#ar-rotation-value"); |
| |
|
| | 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(); |
| |
|
| | |
| | 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); |
| |
|
| | |
| | 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); |
| |
|
| | |
| | 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); |
| |
|
| | |
| | var modelRoot = new pc.Entity("ModelRoot"); |
| | modelRoot.enabled = false; |
| | app.root.addChild(modelRoot); |
| | var modelLoaded = false, placedOnce = false; |
| |
|
| | |
| | var UP = new pc.Vec3(0, 1, 0); |
| |
|
| | function projOnPlane(v, n) { |
| | var d = v.dot(n); |
| | return new pc.Vec3(v.x - d*n.x, v.y - d*n.y, v.z - d*n.z); |
| | } |
| |
|
| | function quatFromBasis(X, Y, Z) { |
| | |
| | var m00 = X.x, m01 = Y.x, m02 = Z.x; |
| | var m10 = X.y, m11 = Y.y, m12 = Z.y; |
| | var m20 = X.z, m21 = Y.z, m22 = Z.z; |
| |
|
| | var t = m00 + m11 + m22; |
| | var q = new pc.Quat(); |
| | if (t > 0) { |
| | var s = Math.sqrt(t + 1.0) * 2; |
| | q.w = 0.25 * s; |
| | q.x = (m21 - m12) / s; |
| | q.y = (m02 - m20) / s; |
| | q.z = (m10 - m01) / s; |
| | } else if (m00 > m11 && m00 > m22) { |
| | var s = Math.sqrt(1.0 + m00 - m11 - m22) * 2; |
| | q.w = (m21 - m12) / s; |
| | q.x = 0.25 * s; |
| | q.y = (m01 + m10) / s; |
| | q.z = (m02 + m20) / s; |
| | } else if (m11 > m22) { |
| | var s2 = Math.sqrt(1.0 + m11 - m00 - m22) * 2; |
| | q.w = (m02 - m20) / s2; |
| | q.x = (m01 + m10) / s2; |
| | q.y = 0.25 * s2; |
| | q.z = (m12 + m21) / s2; |
| | } else { |
| | var s3 = Math.sqrt(1.0 + m22 - m00 - m11) * 2; |
| | q.w = (m10 - m01) / s3; |
| | q.x = (m02 + m20) / s3; |
| | q.y = (m12 + m21) / s3; |
| | q.z = 0.25 * s3; |
| | } |
| | q.normalize(); |
| | return q; |
| | } |
| |
|
| | |
| | function computeFloorBase() { |
| | |
| | var Y = UP.clone(); |
| | var Z = new pc.Vec3(0, 0, 1); |
| | |
| | Z = projOnPlane(Z, Y); if (Z.lengthSq() < 1e-8) Z = new pc.Vec3(1,0,0); |
| | Z.normalize(); |
| | var X = new pc.Vec3(); X.cross(Y, Z).normalize(); |
| | Z.cross(X, Y).normalize(); |
| | return quatFromBasis(X, Y, Z); |
| | } |
| |
|
| | function computeWallBaseFromHit(rot) { |
| | |
| | |
| | var N = new pc.Vec3(); |
| | rot.transformVector(new pc.Vec3(0,1,0), N); |
| | N.normalize(); |
| |
|
| | |
| | var Z = projOnPlane(UP, N); |
| | if (Z.lengthSq() < 1e-8) Z = new pc.Vec3(0,0,1); |
| | Z.normalize(); |
| |
|
| | |
| | var X = new pc.Vec3(); X.cross(N, Z).normalize(); |
| | Z.cross(X, N).normalize(); |
| |
|
| | return quatFromBasis(X, N , Z); |
| | } |
| |
|
| | |
| | var wallMode = !PLACE_ON_FLOOR; |
| | var baseRot = new pc.Quat(); |
| | var userYawDeg = 0; |
| |
|
| | |
| | var blob = null; |
| | 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)'); |
| | grd.addColorStop(1, 'rgba(0,0,0,0.0)'); |
| | 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; |
| | 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; |
| | } |
| |
|
| | |
| | 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) + "%"; |
| | rotInput.value = String(Math.round(yDeg)); |
| | rotVal.textContent = String(Math.round(yDeg)) + "°"; |
| | } |
| |
|
| | function applyYawAroundLocalY(deg) { |
| | var y = clamp360(deg); |
| | userYawDeg = y; |
| | if (!modelRoot.enabled) { updateKnobFromY(y); return; } |
| | var qLocal = new pc.Quat(); |
| | qLocal.setFromAxisAngle(pc.Vec3.UP, y); |
| | var qFinal = new pc.Quat(); |
| | qFinal.mul2(baseRot, qLocal); |
| | modelRoot.setRotation(qFinal); |
| | 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); |
| | } |
| |
|
| | |
| | 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); |
| |
|
| | |
| | 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(); |
| | } |
| | } |
| |
|
| | modelLoaded = true; |
| | message("Modèle chargé. Appuyez sur « Lancer l’AR »."); |
| | }); |
| |
|
| | if (!app.xr.supported) { message("WebXR n’est pas supporté sur cet appareil."); return; } |
| |
|
| | |
| | var TMP_Y = 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_Y, TMP_OUT); |
| | return TMP_OUT.y >= minDot; |
| | } |
| |
|
| | |
| | function isVerticalFacing(rot, maxAbsY) { |
| | maxAbsY = (typeof maxAbsY === "number") ? maxAbsY : 0.35; |
| | rot.transformVector(TMP_Y, TMP_OUT); |
| | return Math.abs(TMP_OUT.y) <= maxAbsY; |
| | } |
| |
|
| | function planeMatchesMode(rot) { |
| | return wallMode ? isVerticalFacing(rot) : isHorizontalUpFacing(rot); |
| | } |
| |
|
| | |
| | var isDragging = false; |
| | var dragLockedRot = new pc.Quat(); |
| | var lastHitRot = new pc.Quat(); |
| |
|
| | |
| | var uiInteracting = false; |
| | var draggingTrack = false; |
| | var activePointerId = null; |
| |
|
| | function insideTrack(target) { return rotTrack.contains(target); } |
| | function degFromPointer(e) { |
| | var rect = rotTrack.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 (!insideTrack(e.target)) return; |
| | uiInteracting = true; |
| | draggingTrack = true; |
| | activePointerId = (e.pointerId != null) ? e.pointerId : 1; |
| | if (rotTrack.setPointerCapture) { try { rotTrack.setPointerCapture(activePointerId); } catch (er) {} } |
| | applyYawAroundLocalY(degFromPointer(e)); |
| | e.preventDefault(); e.stopPropagation(); |
| | } |
| | function onPointerMoveCapture(e) { |
| | if (!draggingTrack || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return; |
| | applyYawAroundLocalY(degFromPointer(e)); |
| | e.preventDefault(); e.stopPropagation(); |
| | } |
| | function endDrag(e) { |
| | if (!draggingTrack || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return; |
| | draggingTrack = false; uiInteracting = false; |
| | if (rotTrack.releasePointerCapture) { try { rotTrack.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 }); |
| |
|
| | |
| | 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("ar-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)); |
| | } |
| | } |
| | }); |
| | } |
| |
|
| | var startBtn = ensureARStartButton(activateAR); |
| | app.keyboard.on("keydown", function (evt) { if (evt.key === pc.KEY_ESCAPE && app.xr.active) app.xr.end(); }); |
| |
|
| | |
| | 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 (!planeMatchesMode(rot)) return; |
| |
|
| | reticle.enabled = true; |
| | reticle.setPosition(pos); |
| | reticle.setRotation(rot); |
| |
|
| | if (modelLoaded && !placedOnce) { |
| | modelRoot.enabled = true; |
| | modelRoot.setPosition(pos); |
| |
|
| | if (!wallMode) { |
| | |
| | baseRot.copy(computeFloorBase()); |
| | |
| | var e = new pc.Vec3(); rot.getEulerAngles(e); |
| | applyYawAroundLocalY(((e.y % 360) + 360) % 360); |
| | |
| | blob = createBlobShadowAt(pos, rot); |
| | } else { |
| | |
| | baseRot.copy(computeWallBaseFromHit(rot)); |
| | applyYawAroundLocalY(0); |
| | } |
| |
|
| | placedOnce = true; |
| | rotInput.disabled = false; |
| | message( |
| | wallMode |
| | ? "Objet placé contre le mur (Y=normale, Z→plafond). Glissez pour déplacer, tournez avec le slider →" |
| | : "Objet placé. Glissez pour déplacer, tournez-le avec le slider →" |
| | ); |
| | } |
| | }); |
| | } |
| | }); |
| | }); |
| |
|
| | |
| | var rotateMode = false, lastMouseX = 0; |
| | var ROTATE_SENSITIVITY = 0.25; |
| |
|
| | app.xr.input.on("add", function (inputSource) { |
| | inputSource.on("selectstart", function () { |
| | if (uiInteracting) return; |
| | if (!placedOnce || !modelLoaded) return; |
| |
|
| | |
| | dragLockedRot.copy(modelRoot.getRotation()); |
| |
|
| | 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 (!planeMatchesMode(rot)) return; |
| |
|
| | lastHitRot.copy(rot); |
| | modelRoot.setPosition(pos); |
| | modelRoot.setRotation(dragLockedRot); |
| |
|
| | if (!wallMode) updateBlobPositionUnder(pos, rot); |
| | }); |
| |
|
| | |
| | transientSource.once("remove", function () { |
| | isDragging = false; |
| | if (wallMode) { |
| | baseRot.copy(computeWallBaseFromHit(lastHitRot)); |
| | applyYawAroundLocalY(userYawDeg); |
| | } else { |
| | |
| | modelRoot.setRotation(dragLockedRot); |
| | } |
| | }); |
| | } |
| | }); |
| | }); |
| |
|
| | inputSource.on("selectend", function () { |
| | isDragging = false; |
| | }); |
| | }); |
| |
|
| | |
| | app.mouse.on("mousedown", function (e) { |
| | if (!app.xr.active || !placedOnce || uiInteracting) return; |
| | if (e.button === 0 && !e.shiftKey) { |
| | isDragging = true; |
| | dragLockedRot.copy(modelRoot.getRotation()); |
| | } 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); |
| | modelRoot.setRotation(dragLockedRot); |
| | if (!wallMode) updateBlobPositionUnder(p, null); |
| | } |
| | } else if (rotateMode && modelRoot.enabled) { |
| | var dx = e.x - lastMouseX; |
| | lastMouseX = e.x; |
| | applyYawAroundLocalY(userYawDeg + dx * ROTATE_SENSITIVITY); |
| | } |
| | }); |
| | app.mouse.on("mouseup", function () { |
| | isDragging = false; |
| | rotateMode = false; |
| | }); |
| | window.addEventListener("contextmenu", function (e) { e.preventDefault(); }); |
| |
|
| | |
| | rotInput.disabled = true; |
| | rotInput.addEventListener("input", function (e) { |
| | var v = parseFloat(e.target.value || "0"); |
| | applyYawAroundLocalY(v); |
| | }, { passive: true }); |
| |
|
| | |
| | app.xr.on("start", function () { |
| | if (startBtn) startBtn.style.display = "none"; |
| | message(wallMode |
| | ? "Session AR démarrée. Visez un mur (Y=normale du mur, Z du modèle vers le plafond)…" |
| | : "Session AR démarrée. Visez le sol pour détecter un plan…"); |
| | reticle.enabled = true; |
| | }); |
| | app.xr.on("end", function () { |
| | if (startBtn) startBtn.style.display = ""; |
| | message("Session AR terminée."); |
| | reticle.enabled = false; |
| | isDragging = false; |
| | rotInput.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 ? "Appuyez sur « Lancer 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…"); |
| | } |
| | })(); |
| |
|