Update viewer_ar.js
Browse files- viewer_ar.js +339 -205
viewer_ar.js
CHANGED
|
@@ -6,86 +6,170 @@
|
|
| 6 |
- Blob Shadow (ombre de contact) sous l’objet
|
| 7 |
*/
|
| 8 |
|
| 9 |
-
(()
|
| 10 |
const GLB_URL = "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/AR/tests/danae_no_metallic.glb";
|
| 11 |
|
| 12 |
// ===== PlayCanvas version fixée =====
|
| 13 |
const PC_VERSION = "2.11.7";
|
| 14 |
const PC_URLS = {
|
| 15 |
-
esm: [
|
| 16 |
-
umd: [
|
| 17 |
};
|
| 18 |
|
| 19 |
-
function timeout(ms){
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
const tryUMD = async () => { for (const url of PC_URLS.umd){ try{ await Promise.race([ new Promise((res,rej)=>{const s=document.createElement("script");s.src=url;s.async=true;s.onload=()=>res();s.onerror=()=>rej(new Error("script error"));document.head.appendChild(s);}), timeout(loadTimeoutMs)]); if(window.pc?.Application) return window.pc; }catch{}} throw new Error("UMD failed"); };
|
| 24 |
-
try{ return esmFirst ? await tryESM() : await tryUMD(); }catch{ return esmFirst ? await tryUMD() : await tryESM(); }
|
| 25 |
}
|
| 26 |
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
-
function ensureCanvas()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
-
function ensureSliderUI(){
|
| 59 |
-
|
| 60 |
-
p
|
| 61 |
-
p.
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
<
|
|
|
|
|
|
|
| 69 |
overlayRoot.appendChild(p);
|
| 70 |
return p;
|
| 71 |
}
|
| 72 |
|
| 73 |
// ===== Boot =====
|
| 74 |
-
(async
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
// ===== App =====
|
| 77 |
-
function initARApp(){
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
|
| 86 |
window.focus();
|
| 87 |
|
| 88 |
-
|
| 89 |
mouse: new pc.Mouse(canvas),
|
| 90 |
touch: new pc.TouchDevice(canvas),
|
| 91 |
keyboard: new pc.Keyboard(window),
|
|
@@ -94,72 +178,89 @@
|
|
| 94 |
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
|
| 95 |
app.setCanvasResolution(pc.RESOLUTION_AUTO);
|
| 96 |
app.graphicsDevice.maxPixelRatio = window.devicePixelRatio || 1;
|
| 97 |
-
|
|
|
|
|
|
|
| 98 |
app.start();
|
| 99 |
|
| 100 |
// ===== Rendu / PBR defaults =====
|
| 101 |
app.scene.gammaCorrection = pc.GAMMA_SRGB;
|
| 102 |
-
app.scene.toneMapping
|
| 103 |
-
app.scene.exposure
|
| 104 |
-
app.scene.ambientLight
|
| 105 |
|
| 106 |
-
// Camera + lumière
|
| 107 |
-
|
| 108 |
-
camera.addComponent("camera", { clearColor: new pc.Color(0,0,0,0), farClip: 10000 });
|
| 109 |
app.root.addChild(camera);
|
| 110 |
|
| 111 |
-
|
| 112 |
-
light.addComponent("light", { type: "directional", intensity: 1.0, castShadows: true, color: new pc.Color(1,1,1) });
|
| 113 |
light.setLocalEulerAngles(45, 30, 0);
|
| 114 |
app.root.addChild(light);
|
| 115 |
|
| 116 |
// Réticule
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
// Modèle
|
| 121 |
-
|
| 122 |
-
|
|
|
|
|
|
|
| 123 |
|
| 124 |
// ===== Blob Shadow =====
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
|
| 129 |
-
function makeBlobTexture(app, size
|
| 130 |
-
|
|
|
|
| 131 |
cvs.width = cvs.height = size;
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
grd.addColorStop(0, 'rgba(0,0,0,0.35)');
|
| 136 |
-
grd.addColorStop(1, 'rgba(0,0,0,0.0)');
|
| 137 |
ctx.fillStyle = grd;
|
| 138 |
ctx.fillRect(0, 0, size, size);
|
| 139 |
|
| 140 |
-
|
| 141 |
-
width: size,
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
});
|
| 144 |
tex.setSource(cvs);
|
| 145 |
return tex;
|
| 146 |
}
|
| 147 |
|
| 148 |
function createBlobShadowAt(pos, rot) {
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
blobMat.diffuse.
|
| 152 |
-
blobMat.opacity = 1.0;
|
| 153 |
blobMat.blendType = pc.BLEND_PREMULTIPLIED;
|
| 154 |
-
blobMat.depthWrite = false;
|
| 155 |
blobMat.diffuseMap = tex;
|
| 156 |
blobMat.update();
|
| 157 |
|
| 158 |
-
|
| 159 |
e.addComponent("render", { type: "plane", castShadows: false, receiveShadows: false });
|
| 160 |
e.render.material = blobMat;
|
| 161 |
|
| 162 |
-
// place légèrement au-dessus du sol, orienté comme le plan détecté (horizontal)
|
| 163 |
e.setPosition(pos.x, pos.y + BLOB_OFFSET_Y, pos.z);
|
| 164 |
e.setRotation(rot);
|
| 165 |
e.setLocalScale(BLOB_SIZE, 1, BLOB_SIZE);
|
|
@@ -168,155 +269,170 @@
|
|
| 168 |
return e;
|
| 169 |
}
|
| 170 |
|
| 171 |
-
// Euler
|
| 172 |
-
|
| 173 |
|
| 174 |
-
// Rotation
|
| 175 |
-
|
| 176 |
-
|
| 177 |
|
| 178 |
-
function updateKnobFromY(yDeg){
|
| 179 |
-
|
| 180 |
-
rotKnob.style.top =
|
| 181 |
rotYInput.value = String(Math.round(yDeg));
|
| 182 |
-
rotYVal.textContent =
|
| 183 |
}
|
| 184 |
-
function applyRotationY(deg){
|
| 185 |
-
|
| 186 |
rotationYDeg = y;
|
| 187 |
modelRoot.setEulerAngles(baseEulerX, y, baseEulerZ);
|
| 188 |
updateKnobFromY(y);
|
| 189 |
}
|
| 190 |
|
| 191 |
-
|
| 192 |
-
function updateBlobPositionUnder(pos, rotLikePlane = null) {
|
| 193 |
if (!blob) return;
|
| 194 |
-
// on suit la position XY du modèle, et on s'aligne sur la rotation du plan si fournie
|
| 195 |
blob.setPosition(pos.x, pos.y + BLOB_OFFSET_Y, pos.z);
|
| 196 |
-
if (rotLikePlane)
|
| 197 |
-
blob.setRotation(rotLikePlane);
|
| 198 |
-
}
|
| 199 |
}
|
| 200 |
|
| 201 |
-
// Chargement GLB
|
| 202 |
-
app.assets.loadFromUrl(GLB_URL, "container", (err, asset)
|
| 203 |
-
if (err){ console.error(err); message("Échec du chargement du modèle GLB."); return; }
|
| 204 |
-
|
| 205 |
modelRoot.addChild(instance);
|
| 206 |
-
modelRoot.setLocalScale(1,1,1);
|
| 207 |
-
|
| 208 |
-
// Fix matériaux
|
| 209 |
-
|
| 210 |
-
for (
|
| 211 |
-
r
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
if (
|
| 216 |
-
|
|
|
|
| 217 |
}
|
| 218 |
-
if (
|
| 219 |
-
|
| 220 |
}
|
| 221 |
}
|
| 222 |
|
| 223 |
-
|
| 224 |
baseEulerX = initE.x; baseEulerZ = initE.z;
|
| 225 |
|
| 226 |
modelLoaded = true;
|
| 227 |
message("Modèle chargé. Touchez l’écran pour démarrer l’AR.");
|
| 228 |
});
|
| 229 |
|
| 230 |
-
if (!app.xr.supported){ message("WebXR n’est pas supporté sur cet appareil."); return; }
|
| 231 |
|
| 232 |
// ===== Slider fiable : Pointer Events en CAPTURE =====
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
return (1 - t) * 360;
|
| 244 |
-
}
|
| 245 |
-
|
| 246 |
-
|
| 247 |
if (!insideWrap(e.target)) return;
|
| 248 |
uiInteracting = true;
|
| 249 |
draggingWrap = true;
|
| 250 |
-
activePointerId = e.pointerId
|
| 251 |
-
|
|
|
|
|
|
|
| 252 |
applyRotationY(degFromPointer(e));
|
| 253 |
e.preventDefault();
|
| 254 |
e.stopPropagation();
|
| 255 |
-
}
|
| 256 |
-
|
| 257 |
-
if (!draggingWrap || (e.pointerId
|
| 258 |
applyRotationY(degFromPointer(e));
|
| 259 |
e.preventDefault();
|
| 260 |
e.stopPropagation();
|
| 261 |
-
}
|
| 262 |
-
|
| 263 |
-
if (!draggingWrap || (e.pointerId
|
| 264 |
draggingWrap = false;
|
| 265 |
uiInteracting = false;
|
| 266 |
-
|
|
|
|
|
|
|
| 267 |
activePointerId = null;
|
| 268 |
e.preventDefault();
|
| 269 |
e.stopPropagation();
|
| 270 |
-
}
|
| 271 |
|
| 272 |
document.addEventListener("pointerdown", onPointerDownCapture, { capture: true, passive: false });
|
| 273 |
-
document.addEventListener("pointermove", onPointerMoveCapture,
|
| 274 |
-
document.addEventListener("pointerup",
|
| 275 |
-
document.addEventListener("pointercancel", endDrag,
|
| 276 |
-
|
| 277 |
-
// --- Démarrage AR
|
| 278 |
-
|
| 279 |
-
if (!app.xr.isAvailable(pc.XRTYPE_AR)){ message("AR immersive indisponible sur cet appareil."); return; }
|
|
|
|
| 280 |
app.xr.domOverlay.root = document.getElementById("xr-overlay-root");
|
| 281 |
camera.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, {
|
| 282 |
-
requiredFeatures: ["hit-test","dom-overlay"],
|
| 283 |
domOverlay: { root: app.xr.domOverlay.root },
|
| 284 |
-
callback:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
});
|
| 286 |
-
};
|
| 287 |
-
app.mouse.on("mousedown", ()=>{ if(!app.xr.active && !uiInteracting) activateAR(); });
|
| 288 |
-
if (app.touch){
|
| 289 |
-
app.touch.on("touchend", (evt)=>{ if(!app.xr.active && !uiInteracting) activateAR(); evt.event.preventDefault(); evt.event.stopPropagation(); });
|
| 290 |
}
|
| 291 |
-
app.keyboard.on("keydown", (evt)
|
| 292 |
|
| 293 |
// ====== Filtre HORIZONTAL uniquement ======
|
| 294 |
-
|
| 295 |
-
function isHorizontalUpFacing(rot, minDot
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
|
| 297 |
// Hit Test global (réticule + 1er placement)
|
| 298 |
-
app.xr.hitTest.on("available", ()
|
| 299 |
app.xr.hitTest.start({
|
| 300 |
entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
|
| 301 |
-
callback: (err, hitSource)
|
| 302 |
-
if (err){ message("Le AR hit test n’a pas pu démarrer."); return; }
|
| 303 |
-
hitSource.on("result", (pos, rot)
|
| 304 |
-
if (!isHorizontalUpFacing(rot)) return;
|
| 305 |
|
| 306 |
reticle.enabled = true;
|
| 307 |
reticle.setPosition(pos);
|
| 308 |
reticle.setRotation(rot);
|
| 309 |
|
| 310 |
-
if (modelLoaded && !placedOnce){
|
| 311 |
modelRoot.enabled = true;
|
| 312 |
modelRoot.setPosition(pos);
|
| 313 |
|
| 314 |
-
//
|
| 315 |
blob = createBlobShadowAt(pos, rot);
|
| 316 |
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
applyRotationY(y0);
|
| 321 |
|
| 322 |
placedOnce = true;
|
|
@@ -329,62 +445,80 @@
|
|
| 329 |
});
|
| 330 |
|
| 331 |
// Déplacement XR (drag) — ignoré si UI active
|
| 332 |
-
|
| 333 |
-
app.xr.input.on("add", (inputSource)
|
| 334 |
-
inputSource.on("selectstart", ()
|
| 335 |
if (uiInteracting) return;
|
| 336 |
if (!placedOnce || !modelLoaded) return;
|
| 337 |
|
| 338 |
inputSource.hitTestStart({
|
| 339 |
entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
|
| 340 |
-
callback: (err, transientSource)
|
| 341 |
if (err) return;
|
| 342 |
isDragging = true;
|
| 343 |
|
| 344 |
-
transientSource.on("result", (pos, rot)
|
| 345 |
if (!isDragging) return;
|
| 346 |
if (!isHorizontalUpFacing(rot)) return;
|
| 347 |
modelRoot.setPosition(pos);
|
| 348 |
-
updateBlobPositionUnder(pos, rot);
|
| 349 |
});
|
| 350 |
|
| 351 |
-
transientSource.once("remove", ()
|
| 352 |
}
|
| 353 |
});
|
| 354 |
});
|
| 355 |
-
inputSource.on("selectend", ()
|
| 356 |
});
|
| 357 |
|
| 358 |
-
// Desktop : rotation souris
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
app.mouse.on("
|
| 362 |
-
if(!app.xr.active
|
| 363 |
-
if(
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
}
|
| 369 |
-
} else if(rotateMode && modelRoot.enabled){
|
| 370 |
-
const dx=e.x-lastMouseX; lastMouseX=e.x;
|
| 371 |
-
applyRotationY(rotationYDeg + dx*ROTATE_SENSITIVITY);
|
| 372 |
}
|
| 373 |
});
|
| 374 |
-
app.mouse.on("
|
| 375 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
|
| 377 |
// Slider (accessibilité clavier)
|
| 378 |
rotYInput.disabled = true;
|
| 379 |
-
rotYInput.addEventListener("input",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
|
| 381 |
// AR events
|
| 382 |
-
app.xr.on("start",()
|
| 383 |
-
app.xr.on("end",()
|
| 384 |
-
app.xr.on(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
|
| 386 |
-
if(!app.xr.isAvailable(pc.XRTYPE_AR)) message("AR immersive indisponible.");
|
| 387 |
-
else if(!app.xr.hitTest.supported) message("AR Hit Test non supporté.");
|
| 388 |
else message("Chargement du modèle…");
|
| 389 |
}
|
| 390 |
})();
|
|
|
|
| 6 |
- Blob Shadow (ombre de contact) sous l’objet
|
| 7 |
*/
|
| 8 |
|
| 9 |
+
(function () {
|
| 10 |
const GLB_URL = "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/AR/tests/danae_no_metallic.glb";
|
| 11 |
|
| 12 |
// ===== PlayCanvas version fixée =====
|
| 13 |
const PC_VERSION = "2.11.7";
|
| 14 |
const PC_URLS = {
|
| 15 |
+
esm: ["https://cdn.jsdelivr.net/npm/playcanvas@" + PC_VERSION + "/build/playcanvas.min.mjs"],
|
| 16 |
+
umd: ["https://cdn.jsdelivr.net/npm/playcanvas@" + PC_VERSION + "/build/playcanvas.min.js"]
|
| 17 |
};
|
| 18 |
|
| 19 |
+
function timeout(ms) {
|
| 20 |
+
return new Promise(function (_res, rej) {
|
| 21 |
+
setTimeout(function () { rej(new Error("timeout")); }, ms);
|
| 22 |
+
});
|
|
|
|
|
|
|
| 23 |
}
|
| 24 |
|
| 25 |
+
async function loadPlayCanvasRobust(opts) {
|
| 26 |
+
opts = opts || {};
|
| 27 |
+
var esmFirst = (typeof opts.esmFirst === "boolean") ? opts.esmFirst : true;
|
| 28 |
+
var loadTimeoutMs = (typeof opts.loadTimeoutMs === "number") ? opts.loadTimeoutMs : 15000;
|
| 29 |
+
|
| 30 |
+
if (window.pc && window.pc.Application) return window.pc;
|
| 31 |
+
|
| 32 |
+
async function tryESM() {
|
| 33 |
+
for (var i = 0; i < PC_URLS.esm.length; i++) {
|
| 34 |
+
var url = PC_URLS.esm[i];
|
| 35 |
+
try {
|
| 36 |
+
var mod = await Promise.race([import(/* @vite-ignore */ url), timeout(loadTimeoutMs)]);
|
| 37 |
+
var ns = (mod && (mod.pc || mod["default"])) || mod;
|
| 38 |
+
if (ns && ns.Application) {
|
| 39 |
+
if (!window.pc) window.pc = ns;
|
| 40 |
+
return window.pc;
|
| 41 |
+
}
|
| 42 |
+
} catch (e) {
|
| 43 |
+
// continue
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
throw new Error("ESM failed");
|
| 47 |
+
}
|
| 48 |
|
| 49 |
+
async function tryUMD() {
|
| 50 |
+
for (var j = 0; j < PC_URLS.umd.length; j++) {
|
| 51 |
+
var url2 = PC_URLS.umd[j];
|
| 52 |
+
try {
|
| 53 |
+
await Promise.race([
|
| 54 |
+
new Promise(function (res, rej) {
|
| 55 |
+
var s = document.createElement("script");
|
| 56 |
+
s.src = url2;
|
| 57 |
+
s.async = true;
|
| 58 |
+
s.onload = function () { res(); };
|
| 59 |
+
s.onerror = function () { rej(new Error("script error")); };
|
| 60 |
+
document.head.appendChild(s);
|
| 61 |
+
}),
|
| 62 |
+
timeout(loadTimeoutMs)
|
| 63 |
+
]);
|
| 64 |
+
if (window.pc && window.pc.Application) return window.pc;
|
| 65 |
+
} catch (e) {
|
| 66 |
+
// continue
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
throw new Error("UMD failed");
|
| 70 |
+
}
|
| 71 |
|
| 72 |
+
try {
|
| 73 |
+
if (esmFirst) return await tryESM();
|
| 74 |
+
return await tryUMD();
|
| 75 |
+
} catch (e) {
|
| 76 |
+
if (esmFirst) return await tryUMD();
|
| 77 |
+
return await tryESM();
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
|
| 81 |
+
// ===== UI / Overlay =====
|
| 82 |
+
var css = [
|
| 83 |
+
".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}",
|
| 84 |
+
"#xr-overlay-root{position:fixed;inset:0;z-index:9999;pointer-events:none}",
|
| 85 |
+
|
| 86 |
+
".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}",
|
| 87 |
+
".ar-ui .label{font-size:12px;text-align:center;opacity:.95}",
|
| 88 |
+
".rotY-wrap{position:relative;width:48px;height:200px;display:flex;align-items:center;justify-content:center;touch-action:none;overflow:visible;pointer-events:auto}",
|
| 89 |
+
".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}",
|
| 90 |
+
".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}",
|
| 91 |
+
".ar-ui input[type=\"range\"].rotY{position:absolute;opacity:0;pointer-events:none;width:0;height:0}",
|
| 92 |
+
".ar-ui .val{font-size:12px;opacity:.95}"
|
| 93 |
+
].join("\n");
|
| 94 |
+
var styleTag = document.createElement("style");
|
| 95 |
+
styleTag.textContent = css;
|
| 96 |
+
document.head.appendChild(styleTag);
|
| 97 |
+
|
| 98 |
+
function ensureOverlayRoot() {
|
| 99 |
+
var r = document.getElementById("xr-overlay-root");
|
| 100 |
+
if (!r) {
|
| 101 |
+
r = document.createElement("div");
|
| 102 |
+
r.id = "xr-overlay-root";
|
| 103 |
+
document.body.appendChild(r);
|
| 104 |
+
}
|
| 105 |
+
return r;
|
| 106 |
+
}
|
| 107 |
+
var overlayRoot = ensureOverlayRoot();
|
| 108 |
+
|
| 109 |
+
function message(msg) {
|
| 110 |
+
var el = overlayRoot.querySelector(".pc-ar-msg");
|
| 111 |
+
if (!el) {
|
| 112 |
+
el = document.createElement("div");
|
| 113 |
+
el.className = "pc-ar-msg";
|
| 114 |
+
overlayRoot.appendChild(el);
|
| 115 |
+
}
|
| 116 |
+
el.textContent = msg;
|
| 117 |
+
}
|
| 118 |
|
| 119 |
+
function ensureCanvas() {
|
| 120 |
+
var c = document.getElementById("application-canvas");
|
| 121 |
+
if (!c) {
|
| 122 |
+
c = document.createElement("canvas");
|
| 123 |
+
c.id = "application-canvas";
|
| 124 |
+
c.style.width = "100%";
|
| 125 |
+
c.style.height = "100%";
|
| 126 |
+
document.body.appendChild(c);
|
| 127 |
+
}
|
| 128 |
+
return c;
|
| 129 |
+
}
|
| 130 |
|
| 131 |
+
function ensureSliderUI() {
|
| 132 |
+
var p = overlayRoot.querySelector(".ar-ui");
|
| 133 |
+
if (p) return p;
|
| 134 |
+
p = document.createElement("div");
|
| 135 |
+
p.className = "ar-ui";
|
| 136 |
+
p.innerHTML =
|
| 137 |
+
'<div class="label">Rotation</div>' +
|
| 138 |
+
'<div class="rotY-wrap" id="ar-rotY-wrap">' +
|
| 139 |
+
' <div class="rotY-rail"></div>' +
|
| 140 |
+
' <div class="rotY-knob" id="ar-rotY-knob"></div>' +
|
| 141 |
+
' <input class="rotY" id="ar-rotY" type="range" min="0" max="360" step="1" value="0"/>' +
|
| 142 |
+
'</div>' +
|
| 143 |
+
'<div class="val" id="ar-rotY-val">0°</div>';
|
| 144 |
overlayRoot.appendChild(p);
|
| 145 |
return p;
|
| 146 |
}
|
| 147 |
|
| 148 |
// ===== Boot =====
|
| 149 |
+
(async function () {
|
| 150 |
+
try {
|
| 151 |
+
await loadPlayCanvasRobust({ esmFirst: true, loadTimeoutMs: 15000 });
|
| 152 |
+
} catch (e) {
|
| 153 |
+
console.error("Chargement PlayCanvas échoué ->", e);
|
| 154 |
+
message("Impossible de charger PlayCanvas (réseau/CDN). Réessaie plus tard.");
|
| 155 |
+
return;
|
| 156 |
+
}
|
| 157 |
+
initARApp();
|
| 158 |
+
})();
|
| 159 |
|
| 160 |
// ===== App =====
|
| 161 |
+
function initARApp() {
|
| 162 |
+
var pc = window.pc;
|
| 163 |
+
var canvas = ensureCanvas();
|
| 164 |
+
var ui = ensureSliderUI();
|
| 165 |
+
var rotWrap = ui.querySelector("#ar-rotY-wrap");
|
| 166 |
+
var rotKnob = ui.querySelector("#ar-rotY-knob");
|
| 167 |
+
var rotYInput = ui.querySelector("#ar-rotY");
|
| 168 |
+
var rotYVal = ui.querySelector("#ar-rotY-val");
|
| 169 |
|
| 170 |
window.focus();
|
| 171 |
|
| 172 |
+
var app = new pc.Application(canvas, {
|
| 173 |
mouse: new pc.Mouse(canvas),
|
| 174 |
touch: new pc.TouchDevice(canvas),
|
| 175 |
keyboard: new pc.Keyboard(window),
|
|
|
|
| 178 |
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
|
| 179 |
app.setCanvasResolution(pc.RESOLUTION_AUTO);
|
| 180 |
app.graphicsDevice.maxPixelRatio = window.devicePixelRatio || 1;
|
| 181 |
+
var onResize = function () { app.resizeCanvas(); };
|
| 182 |
+
window.addEventListener("resize", onResize);
|
| 183 |
+
app.on("destroy", function () { window.removeEventListener("resize", onResize); });
|
| 184 |
app.start();
|
| 185 |
|
| 186 |
// ===== Rendu / PBR defaults =====
|
| 187 |
app.scene.gammaCorrection = pc.GAMMA_SRGB;
|
| 188 |
+
app.scene.toneMapping = pc.TONEMAP_ACES;
|
| 189 |
+
app.scene.exposure = 1;
|
| 190 |
+
app.scene.ambientLight = new pc.Color(1, 1, 1);
|
| 191 |
|
| 192 |
+
// Camera + lumière
|
| 193 |
+
var camera = new pc.Entity("Camera");
|
| 194 |
+
camera.addComponent("camera", { clearColor: new pc.Color(0, 0, 0, 0), farClip: 10000 });
|
| 195 |
app.root.addChild(camera);
|
| 196 |
|
| 197 |
+
var light = new pc.Entity("Light");
|
| 198 |
+
light.addComponent("light", { type: "directional", intensity: 1.0, castShadows: true, color: new pc.Color(1, 1, 1) });
|
| 199 |
light.setLocalEulerAngles(45, 30, 0);
|
| 200 |
app.root.addChild(light);
|
| 201 |
|
| 202 |
// Réticule
|
| 203 |
+
var reticleMat = new pc.StandardMaterial();
|
| 204 |
+
reticleMat.diffuse = new pc.Color(0.2, 0.8, 1.0);
|
| 205 |
+
reticleMat.opacity = 0.85;
|
| 206 |
+
reticleMat.blendType = pc.BLEND_NORMAL;
|
| 207 |
+
reticleMat.update();
|
| 208 |
+
|
| 209 |
+
var reticle = new pc.Entity("Reticle");
|
| 210 |
+
reticle.addComponent("render", { type: "torus", material: reticleMat });
|
| 211 |
+
reticle.setLocalScale(0.12, 0.005, 0.12);
|
| 212 |
+
reticle.enabled = false;
|
| 213 |
+
app.root.addChild(reticle);
|
| 214 |
|
| 215 |
// Modèle
|
| 216 |
+
var modelRoot = new pc.Entity("ModelRoot");
|
| 217 |
+
modelRoot.enabled = false;
|
| 218 |
+
app.root.addChild(modelRoot);
|
| 219 |
+
var modelLoaded = false, placedOnce = false;
|
| 220 |
|
| 221 |
// ===== Blob Shadow =====
|
| 222 |
+
var blob = null; // entité plane de l’ombre
|
| 223 |
+
var BLOB_SIZE = 0.7;
|
| 224 |
+
var BLOB_OFFSET_Y = 0.005;
|
| 225 |
|
| 226 |
+
function makeBlobTexture(app, size) {
|
| 227 |
+
size = size || 256;
|
| 228 |
+
var cvs = document.createElement('canvas');
|
| 229 |
cvs.width = cvs.height = size;
|
| 230 |
+
var ctx = cvs.getContext('2d');
|
| 231 |
+
var r = size * 0.45;
|
| 232 |
+
var grd = ctx.createRadialGradient(size / 2, size / 2, r * 0.2, size / 2, size / 2, r);
|
| 233 |
+
grd.addColorStop(0, 'rgba(0,0,0,0.35)');
|
| 234 |
+
grd.addColorStop(1, 'rgba(0,0,0,0.0)');
|
| 235 |
ctx.fillStyle = grd;
|
| 236 |
ctx.fillRect(0, 0, size, size);
|
| 237 |
|
| 238 |
+
var tex = new pc.Texture(app.graphicsDevice, {
|
| 239 |
+
width: size,
|
| 240 |
+
height: size,
|
| 241 |
+
format: pc.PIXELFORMAT_R8_G8_B8_A8,
|
| 242 |
+
mipmaps: true,
|
| 243 |
+
magFilter: pc.FILTER_LINEAR,
|
| 244 |
+
minFilter: pc.FILTER_LINEAR_MIPMAP_LINEAR
|
| 245 |
});
|
| 246 |
tex.setSource(cvs);
|
| 247 |
return tex;
|
| 248 |
}
|
| 249 |
|
| 250 |
function createBlobShadowAt(pos, rot) {
|
| 251 |
+
var tex = makeBlobTexture(app, 256);
|
| 252 |
+
var blobMat = new pc.StandardMaterial();
|
| 253 |
+
blobMat.diffuse = new pc.Color(0, 0, 0);
|
| 254 |
+
blobMat.opacity = 1.0;
|
| 255 |
blobMat.blendType = pc.BLEND_PREMULTIPLIED;
|
| 256 |
+
blobMat.depthWrite = false;
|
| 257 |
blobMat.diffuseMap = tex;
|
| 258 |
blobMat.update();
|
| 259 |
|
| 260 |
+
var e = new pc.Entity("BlobShadow");
|
| 261 |
e.addComponent("render", { type: "plane", castShadows: false, receiveShadows: false });
|
| 262 |
e.render.material = blobMat;
|
| 263 |
|
|
|
|
| 264 |
e.setPosition(pos.x, pos.y + BLOB_OFFSET_Y, pos.z);
|
| 265 |
e.setRotation(rot);
|
| 266 |
e.setLocalScale(BLOB_SIZE, 1, BLOB_SIZE);
|
|
|
|
| 269 |
return e;
|
| 270 |
}
|
| 271 |
|
| 272 |
+
// Euler base
|
| 273 |
+
var baseEulerX = 0, baseEulerZ = 0;
|
| 274 |
|
| 275 |
+
// Rotation via slider
|
| 276 |
+
var rotationYDeg = 0;
|
| 277 |
+
function clamp360(d) { return Math.max(0, Math.min(360, d)); }
|
| 278 |
|
| 279 |
+
function updateKnobFromY(yDeg) {
|
| 280 |
+
var t = 1 - (yDeg / 360);
|
| 281 |
+
rotKnob.style.top = String(t * 100) + "%";
|
| 282 |
rotYInput.value = String(Math.round(yDeg));
|
| 283 |
+
rotYVal.textContent = String(Math.round(yDeg)) + "°";
|
| 284 |
}
|
| 285 |
+
function applyRotationY(deg) {
|
| 286 |
+
var y = clamp360(deg);
|
| 287 |
rotationYDeg = y;
|
| 288 |
modelRoot.setEulerAngles(baseEulerX, y, baseEulerZ);
|
| 289 |
updateKnobFromY(y);
|
| 290 |
}
|
| 291 |
|
| 292 |
+
function updateBlobPositionUnder(pos, rotLikePlane) {
|
|
|
|
| 293 |
if (!blob) return;
|
|
|
|
| 294 |
blob.setPosition(pos.x, pos.y + BLOB_OFFSET_Y, pos.z);
|
| 295 |
+
if (rotLikePlane) blob.setRotation(rotLikePlane);
|
|
|
|
|
|
|
| 296 |
}
|
| 297 |
|
| 298 |
+
// Chargement GLB
|
| 299 |
+
app.assets.loadFromUrl(GLB_URL, "container", function (err, asset) {
|
| 300 |
+
if (err) { console.error(err); message("Échec du chargement du modèle GLB."); return; }
|
| 301 |
+
var instance = asset.resource.instantiateRenderEntity({ castShadows: true, receiveShadows: false });
|
| 302 |
modelRoot.addChild(instance);
|
| 303 |
+
modelRoot.setLocalScale(1, 1, 1);
|
| 304 |
+
|
| 305 |
+
// Fix matériaux
|
| 306 |
+
var renders = instance.findComponents('render');
|
| 307 |
+
for (var ri = 0; ri < renders.length; ri++) {
|
| 308 |
+
var r = renders[ri];
|
| 309 |
+
r.castShadows = true;
|
| 310 |
+
for (var mi = 0; mi < r.meshInstances.length; mi++) {
|
| 311 |
+
var mat = r.meshInstances[mi].material;
|
| 312 |
+
if (!mat) continue;
|
| 313 |
+
if (mat.diffuse && (mat.diffuse.r !== 1 || mat.diffuse.g !== 1 || mat.diffuse.b !== 1)) {
|
| 314 |
+
mat.diffuse.set(1, 1, 1);
|
| 315 |
}
|
| 316 |
+
if (typeof mat.useSkybox !== "undefined") mat.useSkybox = true;
|
| 317 |
+
mat.update();
|
| 318 |
}
|
| 319 |
}
|
| 320 |
|
| 321 |
+
var initE = modelRoot.getEulerAngles();
|
| 322 |
baseEulerX = initE.x; baseEulerZ = initE.z;
|
| 323 |
|
| 324 |
modelLoaded = true;
|
| 325 |
message("Modèle chargé. Touchez l’écran pour démarrer l’AR.");
|
| 326 |
});
|
| 327 |
|
| 328 |
+
if (!app.xr.supported) { message("WebXR n’est pas supporté sur cet appareil."); return; }
|
| 329 |
|
| 330 |
// ===== Slider fiable : Pointer Events en CAPTURE =====
|
| 331 |
+
var uiInteracting = false;
|
| 332 |
+
var draggingWrap = false;
|
| 333 |
+
var activePointerId = null;
|
| 334 |
+
|
| 335 |
+
function insideWrap(target) { return rotWrap.contains(target); }
|
| 336 |
+
function degFromPointer(e) {
|
| 337 |
+
var rect = rotWrap.getBoundingClientRect();
|
| 338 |
+
var y = (e.clientY != null) ? e.clientY : ((e.touches && e.touches[0] && e.touches[0].clientY) || 0);
|
| 339 |
+
var ratio = (y - rect.top) / rect.height;
|
| 340 |
+
var t = Math.max(0, Math.min(1, ratio));
|
| 341 |
+
return (1 - t) * 360;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
function onPointerDownCapture(e) {
|
| 345 |
if (!insideWrap(e.target)) return;
|
| 346 |
uiInteracting = true;
|
| 347 |
draggingWrap = true;
|
| 348 |
+
activePointerId = (e.pointerId != null) ? e.pointerId : 1;
|
| 349 |
+
if (rotWrap.setPointerCapture) {
|
| 350 |
+
try { rotWrap.setPointerCapture(activePointerId); } catch (er) {}
|
| 351 |
+
}
|
| 352 |
applyRotationY(degFromPointer(e));
|
| 353 |
e.preventDefault();
|
| 354 |
e.stopPropagation();
|
| 355 |
+
}
|
| 356 |
+
function onPointerMoveCapture(e) {
|
| 357 |
+
if (!draggingWrap || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
|
| 358 |
applyRotationY(degFromPointer(e));
|
| 359 |
e.preventDefault();
|
| 360 |
e.stopPropagation();
|
| 361 |
+
}
|
| 362 |
+
function endDrag(e) {
|
| 363 |
+
if (!draggingWrap || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
|
| 364 |
draggingWrap = false;
|
| 365 |
uiInteracting = false;
|
| 366 |
+
if (rotWrap.releasePointerCapture) {
|
| 367 |
+
try { rotWrap.releasePointerCapture(activePointerId); } catch (er) {}
|
| 368 |
+
}
|
| 369 |
activePointerId = null;
|
| 370 |
e.preventDefault();
|
| 371 |
e.stopPropagation();
|
| 372 |
+
}
|
| 373 |
|
| 374 |
document.addEventListener("pointerdown", onPointerDownCapture, { capture: true, passive: false });
|
| 375 |
+
document.addEventListener("pointermove", onPointerMoveCapture, { capture: true, passive: false });
|
| 376 |
+
document.addEventListener("pointerup", endDrag, { capture: true, passive: false });
|
| 377 |
+
document.addEventListener("pointercancel", endDrag, { capture: true, passive: false });
|
| 378 |
+
|
| 379 |
+
// --- Démarrage AR
|
| 380 |
+
function activateAR() {
|
| 381 |
+
if (!app.xr.isAvailable(pc.XRTYPE_AR)) { message("AR immersive indisponible sur cet appareil."); return; }
|
| 382 |
+
if (!app.xr.domOverlay) app.xr.domOverlay = {};
|
| 383 |
app.xr.domOverlay.root = document.getElementById("xr-overlay-root");
|
| 384 |
camera.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, {
|
| 385 |
+
requiredFeatures: ["hit-test", "dom-overlay"],
|
| 386 |
domOverlay: { root: app.xr.domOverlay.root },
|
| 387 |
+
callback: function (err) {
|
| 388 |
+
if (err) {
|
| 389 |
+
console.error("Échec du démarrage AR :", err);
|
| 390 |
+
message("Échec du démarrage AR : " + (err.message || err));
|
| 391 |
+
}
|
| 392 |
+
}
|
| 393 |
+
});
|
| 394 |
+
}
|
| 395 |
+
app.mouse.on("mousedown", function () { if (!app.xr.active && !uiInteracting) activateAR(); });
|
| 396 |
+
if (app.touch) {
|
| 397 |
+
app.touch.on("touchend", function (evt) {
|
| 398 |
+
if (!app.xr.active && !uiInteracting) activateAR();
|
| 399 |
+
evt.event.preventDefault();
|
| 400 |
+
evt.event.stopPropagation();
|
| 401 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
}
|
| 403 |
+
app.keyboard.on("keydown", function (evt) { if (evt.key === pc.KEY_ESCAPE && app.xr.active) app.xr.end(); });
|
| 404 |
|
| 405 |
// ====== Filtre HORIZONTAL uniquement ======
|
| 406 |
+
var TMP_IN = new pc.Vec3(0, 1, 0), TMP_OUT = new pc.Vec3();
|
| 407 |
+
function isHorizontalUpFacing(rot, minDot) {
|
| 408 |
+
minDot = (typeof minDot === "number") ? minDot : 0.75;
|
| 409 |
+
rot.transformVector(TMP_IN, TMP_OUT);
|
| 410 |
+
return TMP_OUT.y >= minDot;
|
| 411 |
+
}
|
| 412 |
|
| 413 |
// Hit Test global (réticule + 1er placement)
|
| 414 |
+
app.xr.hitTest.on("available", function () {
|
| 415 |
app.xr.hitTest.start({
|
| 416 |
entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
|
| 417 |
+
callback: function (err, hitSource) {
|
| 418 |
+
if (err) { message("Le AR hit test n’a pas pu démarrer."); return; }
|
| 419 |
+
hitSource.on("result", function (pos, rot) {
|
| 420 |
+
if (!isHorizontalUpFacing(rot)) return;
|
| 421 |
|
| 422 |
reticle.enabled = true;
|
| 423 |
reticle.setPosition(pos);
|
| 424 |
reticle.setRotation(rot);
|
| 425 |
|
| 426 |
+
if (modelLoaded && !placedOnce) {
|
| 427 |
modelRoot.enabled = true;
|
| 428 |
modelRoot.setPosition(pos);
|
| 429 |
|
| 430 |
+
// Ombre de contact au placement initial
|
| 431 |
blob = createBlobShadowAt(pos, rot);
|
| 432 |
|
| 433 |
+
var e = new pc.Vec3();
|
| 434 |
+
rot.getEulerAngles(e);
|
| 435 |
+
var y0 = ((e.y % 360) + 360) % 360;
|
| 436 |
applyRotationY(y0);
|
| 437 |
|
| 438 |
placedOnce = true;
|
|
|
|
| 445 |
});
|
| 446 |
|
| 447 |
// Déplacement XR (drag) — ignoré si UI active
|
| 448 |
+
var isDragging = false;
|
| 449 |
+
app.xr.input.on("add", function (inputSource) {
|
| 450 |
+
inputSource.on("selectstart", function () {
|
| 451 |
if (uiInteracting) return;
|
| 452 |
if (!placedOnce || !modelLoaded) return;
|
| 453 |
|
| 454 |
inputSource.hitTestStart({
|
| 455 |
entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
|
| 456 |
+
callback: function (err, transientSource) {
|
| 457 |
if (err) return;
|
| 458 |
isDragging = true;
|
| 459 |
|
| 460 |
+
transientSource.on("result", function (pos, rot) {
|
| 461 |
if (!isDragging) return;
|
| 462 |
if (!isHorizontalUpFacing(rot)) return;
|
| 463 |
modelRoot.setPosition(pos);
|
| 464 |
+
updateBlobPositionUnder(pos, rot);
|
| 465 |
});
|
| 466 |
|
| 467 |
+
transientSource.once("remove", function () { isDragging = false; });
|
| 468 |
}
|
| 469 |
});
|
| 470 |
});
|
| 471 |
+
inputSource.on("selectend", function () { isDragging = false; });
|
| 472 |
});
|
| 473 |
|
| 474 |
+
// Desktop : rotation souris (ignore si UI)
|
| 475 |
+
var rotateMode = false, lastMouseX = 0;
|
| 476 |
+
var ROTATE_SENSITIVITY = 0.25;
|
| 477 |
+
app.mouse.on("mousedown", function (e) {
|
| 478 |
+
if (!app.xr.active || !placedOnce || uiInteracting) return;
|
| 479 |
+
if (e.button === 0 && !e.shiftKey) {
|
| 480 |
+
isDragging = true;
|
| 481 |
+
} else if (e.button === 2 || (e.button === 0 && e.shiftKey)) {
|
| 482 |
+
rotateMode = true;
|
| 483 |
+
lastMouseX = e.x;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 484 |
}
|
| 485 |
});
|
| 486 |
+
app.mouse.on("mousemove", function (e) {
|
| 487 |
+
if (!app.xr.active || !placedOnce || uiInteracting) return;
|
| 488 |
+
if (isDragging) {
|
| 489 |
+
if (reticle.enabled) {
|
| 490 |
+
var p = reticle.getPosition();
|
| 491 |
+
modelRoot.setPosition(p);
|
| 492 |
+
updateBlobPositionUnder(p, null);
|
| 493 |
+
}
|
| 494 |
+
} else if (rotateMode && modelRoot.enabled) {
|
| 495 |
+
var dx = e.x - lastMouseX;
|
| 496 |
+
lastMouseX = e.x;
|
| 497 |
+
applyRotationY(rotationYDeg + dx * ROTATE_SENSITIVITY);
|
| 498 |
+
}
|
| 499 |
+
});
|
| 500 |
+
app.mouse.on("mouseup", function () { isDragging = false; rotateMode = false; });
|
| 501 |
+
window.addEventListener("contextmenu", function (e) { e.preventDefault(); });
|
| 502 |
|
| 503 |
// Slider (accessibilité clavier)
|
| 504 |
rotYInput.disabled = true;
|
| 505 |
+
rotYInput.addEventListener("input", function (e) {
|
| 506 |
+
if (!modelRoot.enabled) return;
|
| 507 |
+
var v = parseFloat(e.target.value || "0");
|
| 508 |
+
applyRotationY(v);
|
| 509 |
+
}, { passive: true });
|
| 510 |
|
| 511 |
// AR events
|
| 512 |
+
app.xr.on("start", function () { message("Session AR démarrée. Visez le sol pour détecter un plan…"); reticle.enabled = true; });
|
| 513 |
+
app.xr.on("end", function () { message("Session AR terminée."); reticle.enabled = false; isDragging = false; rotateMode = false; rotYInput.disabled = true; });
|
| 514 |
+
app.xr.on("available:" + pc.XRTYPE_AR, function (a) {
|
| 515 |
+
if (!a) message("AR immersive indisponible.");
|
| 516 |
+
else if (!app.xr.hitTest.supported) message("AR Hit Test non supporté.");
|
| 517 |
+
else message(modelLoaded ? "Touchez l’écran pour démarrer l’AR." : "Chargement du modèle…");
|
| 518 |
+
});
|
| 519 |
|
| 520 |
+
if (!app.xr.isAvailable(pc.XRTYPE_AR)) message("AR immersive indisponible.");
|
| 521 |
+
else if (!app.xr.hitTest.supported) message("AR Hit Test non supporté.");
|
| 522 |
else message("Chargement du modèle…");
|
| 523 |
}
|
| 524 |
})();
|