Update viewer_ar_ios.js
Browse files- viewer_ar_ios.js +111 -81
viewer_ar_ios.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
/* viewer_ar_ios.js — AR PlayCanvas + GLB/USDZ via config.json
|
| 2 |
- Lit config.json (data-config) => { "glb_url": "...", "usdz_url": "..." }
|
| 3 |
- iOS : AR Quick Look (USDZ) avec #allowsContentScaling=0 (pas de zoom)
|
|
@@ -106,17 +107,24 @@
|
|
| 106 |
}
|
| 107 |
|
| 108 |
// ============ UI / Overlay commun ============
|
|
|
|
|
|
|
| 109 |
var css = [
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
"#xr-overlay-root{position:fixed;inset:0;z-index:10001;pointer-events:none}",
|
| 112 |
|
|
|
|
| 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}",
|
| 114 |
-
".ar-ui .label{font-size:
|
| 115 |
".rotY-wrap{position:relative;width:48px;height:200px;display:flex;align-items:center;justify-content:center;touch-action:none;overflow:visible;pointer-events:auto}",
|
| 116 |
".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}",
|
| 117 |
".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}",
|
| 118 |
".ar-ui input[type=\"range\"].rotY{position:absolute;opacity:0;pointer-events:none;width:0;height:0}",
|
| 119 |
-
".ar-ui .val{font-size:
|
| 120 |
|
| 121 |
/* iOS Quick Look button */
|
| 122 |
"#ios-quicklook-btn{position:fixed;left:50%;transform:translateX(-50%);bottom:16px;z-index:10003;display:inline-block;pointer-events:auto}",
|
|
@@ -137,14 +145,25 @@
|
|
| 137 |
}
|
| 138 |
var overlayRoot = ensureOverlayRoot();
|
| 139 |
|
| 140 |
-
|
|
|
|
| 141 |
var el = overlayRoot.querySelector(".pc-ar-msg");
|
| 142 |
if (!el) {
|
| 143 |
el = document.createElement("div");
|
| 144 |
el.className = "pc-ar-msg";
|
| 145 |
overlayRoot.appendChild(el);
|
| 146 |
}
|
| 147 |
-
el
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
}
|
| 149 |
|
| 150 |
// iOS Quick Look URL (empêche le pinch to scale)
|
|
@@ -174,7 +193,7 @@
|
|
| 174 |
img.alt = "Voir en AR";
|
| 175 |
img.src =
|
| 176 |
"data:image/svg+xml;charset=utf-8," +
|
| 177 |
-
encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="
|
| 178 |
|
| 179 |
anchor.appendChild(img);
|
| 180 |
document.body.appendChild(anchor);
|
|
@@ -230,6 +249,7 @@
|
|
| 230 |
return canvas;
|
| 231 |
}
|
| 232 |
|
|
|
|
| 233 |
function ensureSliderUI() {
|
| 234 |
var p = overlayRoot.querySelector(".ar-ui");
|
| 235 |
if (p) return p;
|
|
@@ -261,9 +281,9 @@
|
|
| 261 |
if (isIOS()) {
|
| 262 |
if (USDZ_URL) {
|
| 263 |
ensureQuickLookButton(USDZ_URL);
|
| 264 |
-
|
| 265 |
} else {
|
| 266 |
-
|
| 267 |
}
|
| 268 |
return;
|
| 269 |
}
|
|
@@ -272,7 +292,7 @@
|
|
| 272 |
await loadPlayCanvasRobust({ esmFirst: true, loadTimeoutMs: 15000 });
|
| 273 |
} catch (e) {
|
| 274 |
console.error("Chargement PlayCanvas échoué ->", e);
|
| 275 |
-
|
| 276 |
return;
|
| 277 |
}
|
| 278 |
initARApp(GLB_URL);
|
|
@@ -281,15 +301,18 @@
|
|
| 281 |
// ============ App Android/Desktop (WebXR) ============
|
| 282 |
function initARApp(GLB_URL) {
|
| 283 |
var pc = window.pc;
|
|
|
|
|
|
|
| 284 |
var canvas = ensureCanvas();
|
| 285 |
-
var
|
| 286 |
-
var
|
| 287 |
-
var
|
| 288 |
-
var
|
| 289 |
-
var
|
| 290 |
|
| 291 |
window.focus();
|
| 292 |
|
|
|
|
| 293 |
var app = new pc.Application(canvas, {
|
| 294 |
mouse: new pc.Mouse(canvas),
|
| 295 |
touch: new pc.TouchDevice(canvas),
|
|
@@ -305,23 +328,23 @@
|
|
| 305 |
app.on("destroy", function () { window.removeEventListener("resize", onResize); });
|
| 306 |
app.start();
|
| 307 |
|
| 308 |
-
// Rendu / PBR defaults
|
| 309 |
app.scene.gammaCorrection = pc.GAMMA_SRGB;
|
| 310 |
app.scene.toneMapping = pc.TONEMAP_ACES;
|
| 311 |
app.scene.exposure = 1;
|
| 312 |
app.scene.ambientLight = new pc.Color(1, 1, 1);
|
| 313 |
|
| 314 |
-
//
|
| 315 |
var camera = new pc.Entity("Camera");
|
| 316 |
camera.addComponent("camera", { clearColor: new pc.Color(0, 0, 0, 0), farClip: 10000 });
|
| 317 |
app.root.addChild(camera);
|
| 318 |
|
| 319 |
-
var
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
app.root.addChild(
|
| 323 |
|
| 324 |
-
// Réticule
|
| 325 |
var reticleMat = new pc.StandardMaterial();
|
| 326 |
reticleMat.diffuse = new pc.Color(0.2, 0.8, 1.0);
|
| 327 |
reticleMat.opacity = 0.85;
|
|
@@ -334,14 +357,15 @@
|
|
| 334 |
reticle.enabled = false;
|
| 335 |
app.root.addChild(reticle);
|
| 336 |
|
| 337 |
-
//
|
| 338 |
var modelRoot = new pc.Entity("ModelRoot");
|
| 339 |
modelRoot.enabled = false;
|
| 340 |
app.root.addChild(modelRoot);
|
|
|
|
| 341 |
var modelLoaded = false, placedOnce = false;
|
| 342 |
|
| 343 |
-
// =====
|
| 344 |
-
var
|
| 345 |
var BLOB_SIZE = 0.4;
|
| 346 |
var BLOB_OFFSET_Y = 0.005;
|
| 347 |
|
|
@@ -404,9 +428,9 @@
|
|
| 404 |
|
| 405 |
function updateKnobFromY(yDeg) {
|
| 406 |
var t = 1 - (yDeg / 360);
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
}
|
| 411 |
function applyRotationY(deg) {
|
| 412 |
var y = clamp360(deg);
|
|
@@ -416,14 +440,14 @@
|
|
| 416 |
}
|
| 417 |
|
| 418 |
function updateBlobPositionUnder(pos, rotLikePlane) {
|
| 419 |
-
if (!
|
| 420 |
-
|
| 421 |
-
if (rotLikePlane)
|
| 422 |
}
|
| 423 |
|
| 424 |
// Chargement GLB (depuis config.json)
|
| 425 |
app.assets.loadFromUrl(GLB_URL, "container", function (err, asset) {
|
| 426 |
-
if (err) { console.error(err);
|
| 427 |
var instance = asset.resource.instantiateRenderEntity({ castShadows: true, receiveShadows: false });
|
| 428 |
modelRoot.addChild(instance);
|
| 429 |
modelRoot.setLocalScale(1, 1, 1);
|
|
@@ -448,19 +472,21 @@
|
|
| 448 |
baseEulerX = initE.x; baseEulerZ = initE.z;
|
| 449 |
|
| 450 |
modelLoaded = true;
|
| 451 |
-
|
|
|
|
|
|
|
| 452 |
});
|
| 453 |
|
| 454 |
-
if (!app.xr.supported) {
|
| 455 |
|
| 456 |
-
// Slider fiable (pointer events en capture)
|
| 457 |
-
var
|
| 458 |
-
var
|
| 459 |
var activePointerId = null;
|
| 460 |
|
| 461 |
-
function
|
| 462 |
function degFromPointer(e) {
|
| 463 |
-
var rect =
|
| 464 |
var y = (e.clientY != null) ? e.clientY : ((e.touches && e.touches[0] && e.touches[0].clientY) || 0);
|
| 465 |
var ratio = (y - rect.top) / rect.height;
|
| 466 |
var t = Math.max(0, Math.min(1, ratio));
|
|
@@ -468,29 +494,29 @@
|
|
| 468 |
}
|
| 469 |
|
| 470 |
function onPointerDownCapture(e) {
|
| 471 |
-
if (!
|
| 472 |
-
|
| 473 |
-
|
| 474 |
activePointerId = (e.pointerId != null) ? e.pointerId : 1;
|
| 475 |
-
if (
|
| 476 |
-
try {
|
| 477 |
}
|
| 478 |
applyRotationY(degFromPointer(e));
|
| 479 |
e.preventDefault();
|
| 480 |
e.stopPropagation();
|
| 481 |
}
|
| 482 |
function onPointerMoveCapture(e) {
|
| 483 |
-
if (!
|
| 484 |
applyRotationY(degFromPointer(e));
|
| 485 |
e.preventDefault();
|
| 486 |
e.stopPropagation();
|
| 487 |
}
|
| 488 |
function endDrag(e) {
|
| 489 |
-
if (!
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
if (
|
| 493 |
-
try {
|
| 494 |
}
|
| 495 |
activePointerId = null;
|
| 496 |
e.preventDefault();
|
|
@@ -504,7 +530,7 @@
|
|
| 504 |
|
| 505 |
// --- Démarrage AR (Android/Desktop)
|
| 506 |
function activateAR() {
|
| 507 |
-
if (!app.xr.isAvailable(pc.XRTYPE_AR)) {
|
| 508 |
if (!app.xr.domOverlay) app.xr.domOverlay = {};
|
| 509 |
app.xr.domOverlay.root = document.getElementById("xr-overlay-root");
|
| 510 |
camera.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, {
|
|
@@ -513,15 +539,16 @@
|
|
| 513 |
callback: function (err) {
|
| 514 |
if (err) {
|
| 515 |
console.error("Échec du démarrage AR :", err);
|
| 516 |
-
|
| 517 |
}
|
| 518 |
}
|
| 519 |
});
|
| 520 |
}
|
| 521 |
-
|
|
|
|
| 522 |
if (app.touch) {
|
| 523 |
app.touch.on("touchend", function (evt) {
|
| 524 |
-
if (!app.xr.active && !
|
| 525 |
evt.event.preventDefault();
|
| 526 |
evt.event.stopPropagation();
|
| 527 |
});
|
|
@@ -541,7 +568,7 @@
|
|
| 541 |
app.xr.hitTest.start({
|
| 542 |
entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
|
| 543 |
callback: function (err, hitSource) {
|
| 544 |
-
if (err) {
|
| 545 |
hitSource.on("result", function (pos, rot) {
|
| 546 |
if (!isHorizontalUpFacing(rot)) return;
|
| 547 |
|
|
@@ -554,7 +581,7 @@
|
|
| 554 |
modelRoot.setPosition(pos);
|
| 555 |
|
| 556 |
// Ombre de contact
|
| 557 |
-
|
| 558 |
|
| 559 |
var e = new pc.Vec3();
|
| 560 |
rot.getEulerAngles(e);
|
|
@@ -562,8 +589,10 @@
|
|
| 562 |
applyRotationY(y0);
|
| 563 |
|
| 564 |
placedOnce = true;
|
| 565 |
-
|
| 566 |
-
|
|
|
|
|
|
|
| 567 |
}
|
| 568 |
});
|
| 569 |
}
|
|
@@ -571,80 +600,81 @@
|
|
| 571 |
});
|
| 572 |
|
| 573 |
// Déplacement XR (drag) — ignoré si UI active
|
| 574 |
-
var
|
| 575 |
app.xr.input.on("add", function (inputSource) {
|
| 576 |
inputSource.on("selectstart", function () {
|
| 577 |
-
if (
|
| 578 |
if (!placedOnce || !modelLoaded) return;
|
| 579 |
|
| 580 |
inputSource.hitTestStart({
|
| 581 |
entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
|
| 582 |
callback: function (err, transientSource) {
|
| 583 |
if (err) return;
|
| 584 |
-
|
| 585 |
|
| 586 |
transientSource.on("result", function (pos, rot) {
|
| 587 |
-
if (!
|
| 588 |
if (!isHorizontalUpFacing(rot)) return;
|
| 589 |
modelRoot.setPosition(pos);
|
| 590 |
updateBlobPositionUnder(pos, rot);
|
| 591 |
});
|
| 592 |
|
| 593 |
-
transientSource.once("remove", function () {
|
| 594 |
}
|
| 595 |
});
|
| 596 |
});
|
| 597 |
-
inputSource.on("selectend", function () {
|
| 598 |
});
|
| 599 |
|
| 600 |
// Desktop : rotation souris (ignore si UI)
|
| 601 |
-
var
|
| 602 |
var ROTATE_SENSITIVITY = 0.25;
|
| 603 |
app.mouse.on("mousedown", function (e) {
|
| 604 |
-
if (!app.xr.active || !placedOnce ||
|
| 605 |
if (e.button === 0 && !e.shiftKey) {
|
| 606 |
-
|
| 607 |
} else if (e.button === 2 || (e.button === 0 && e.shiftKey)) {
|
| 608 |
-
|
| 609 |
lastMouseX = e.x;
|
| 610 |
}
|
| 611 |
});
|
| 612 |
app.mouse.on("mousemove", function (e) {
|
| 613 |
-
if (!app.xr.active || !placedOnce ||
|
| 614 |
-
if (
|
| 615 |
if (reticle.enabled) {
|
| 616 |
var p = reticle.getPosition();
|
| 617 |
modelRoot.setPosition(p);
|
| 618 |
updateBlobPositionUnder(p, null);
|
| 619 |
}
|
| 620 |
-
} else if (
|
| 621 |
var dx = e.x - lastMouseX;
|
| 622 |
lastMouseX = e.x;
|
| 623 |
applyRotationY(rotationYDeg + dx * ROTATE_SENSITIVITY);
|
| 624 |
}
|
| 625 |
});
|
| 626 |
-
app.mouse.on("mouseup", function () {
|
| 627 |
window.addEventListener("contextmenu", function (e) { e.preventDefault(); });
|
| 628 |
|
| 629 |
// Slider (accessibilité clavier)
|
| 630 |
-
|
| 631 |
-
|
| 632 |
if (!modelRoot.enabled) return;
|
| 633 |
var v = parseFloat(e.target.value || "0");
|
| 634 |
applyRotationY(v);
|
| 635 |
}, { passive: true });
|
| 636 |
|
| 637 |
// AR events
|
| 638 |
-
app.xr.on("start", function () {
|
| 639 |
-
app.xr.on("end", function () {
|
| 640 |
app.xr.on("available:" + pc.XRTYPE_AR, function (a) {
|
| 641 |
-
if (!a)
|
| 642 |
-
else if (!app.xr.hitTest.supported)
|
| 643 |
-
else
|
| 644 |
});
|
| 645 |
|
| 646 |
-
if (!app.xr.isAvailable(pc.XRTYPE_AR))
|
| 647 |
-
else if (!app.xr.hitTest.supported)
|
| 648 |
-
else
|
| 649 |
}
|
| 650 |
})();
|
|
|
|
|
|
| 1 |
+
<script>
|
| 2 |
/* viewer_ar_ios.js — AR PlayCanvas + GLB/USDZ via config.json
|
| 3 |
- Lit config.json (data-config) => { "glb_url": "...", "usdz_url": "..." }
|
| 4 |
- iOS : AR Quick Look (USDZ) avec #allowsContentScaling=0 (pas de zoom)
|
|
|
|
| 107 |
}
|
| 108 |
|
| 109 |
// ============ UI / Overlay commun ============
|
| 110 |
+
// (On garde un style minimal intégré pour le message ; si tu utilises un CSS externe,
|
| 111 |
+
// tu peux déplacer ces règles dans ce fichier.)
|
| 112 |
var css = [
|
| 113 |
+
/* Toast (bas) */
|
| 114 |
+
".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}",
|
| 115 |
+
/* Variante centrée + plus grande pour le message “Modèle chargé …” */
|
| 116 |
+
".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)}",
|
| 117 |
+
|
| 118 |
"#xr-overlay-root{position:fixed;inset:0;z-index:10001;pointer-events:none}",
|
| 119 |
|
| 120 |
+
/* --- NE PAS MODIFIER : panneau Rotation --- */
|
| 121 |
".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}",
|
| 122 |
+
".ar-ui .label{font-size:12px;text-align:center;opacity:.95}",
|
| 123 |
".rotY-wrap{position:relative;width:48px;height:200px;display:flex;align-items:center;justify-content:center;touch-action:none;overflow:visible;pointer-events:auto}",
|
| 124 |
".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}",
|
| 125 |
".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}",
|
| 126 |
".ar-ui input[type=\"range\"].rotY{position:absolute;opacity:0;pointer-events:none;width:0;height:0}",
|
| 127 |
+
".ar-ui .val{font-size:12px;opacity:.95}",
|
| 128 |
|
| 129 |
/* iOS Quick Look button */
|
| 130 |
"#ios-quicklook-btn{position:fixed;left:50%;transform:translateX(-50%);bottom:16px;z-index:10003;display:inline-block;pointer-events:auto}",
|
|
|
|
| 145 |
}
|
| 146 |
var overlayRoot = ensureOverlayRoot();
|
| 147 |
|
| 148 |
+
// ------ Système de message : toast (bas) ou centré agrandi ------
|
| 149 |
+
function getMessageEl() {
|
| 150 |
var el = overlayRoot.querySelector(".pc-ar-msg");
|
| 151 |
if (!el) {
|
| 152 |
el = document.createElement("div");
|
| 153 |
el.className = "pc-ar-msg";
|
| 154 |
overlayRoot.appendChild(el);
|
| 155 |
}
|
| 156 |
+
return el;
|
| 157 |
+
}
|
| 158 |
+
function messageToast(text) {
|
| 159 |
+
var el = getMessageEl();
|
| 160 |
+
el.classList.remove("pc-ar-msg--centerBig");
|
| 161 |
+
el.textContent = text;
|
| 162 |
+
}
|
| 163 |
+
function messageCenterBig(text) {
|
| 164 |
+
var el = getMessageEl();
|
| 165 |
+
el.classList.add("pc-ar-msg--centerBig");
|
| 166 |
+
el.textContent = text;
|
| 167 |
}
|
| 168 |
|
| 169 |
// iOS Quick Look URL (empêche le pinch to scale)
|
|
|
|
| 193 |
img.alt = "Voir en AR";
|
| 194 |
img.src =
|
| 195 |
"data:image/svg+xml;charset=utf-8," +
|
| 196 |
+
encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="180" height="48"><rect rx="10" ry="10" width="180" height="48" fill="black"/><text x="90" y="30" font-size="16" text-anchor="middle" fill="white" font-family="system-ui, -apple-system, Segoe UI, Roboto">Voir en AR</text></svg>');
|
| 197 |
|
| 198 |
anchor.appendChild(img);
|
| 199 |
document.body.appendChild(anchor);
|
|
|
|
| 249 |
return canvas;
|
| 250 |
}
|
| 251 |
|
| 252 |
+
// ---------- Panneau “Rotation” (inchangé) ----------
|
| 253 |
function ensureSliderUI() {
|
| 254 |
var p = overlayRoot.querySelector(".ar-ui");
|
| 255 |
if (p) return p;
|
|
|
|
| 281 |
if (isIOS()) {
|
| 282 |
if (USDZ_URL) {
|
| 283 |
ensureQuickLookButton(USDZ_URL);
|
| 284 |
+
messageToast("iOS détecté : utilisez le bouton « Voir en AR » (AR Quick Look).");
|
| 285 |
} else {
|
| 286 |
+
messageToast("iOS détecté, mais aucun 'usdz_url' dans config.json.");
|
| 287 |
}
|
| 288 |
return;
|
| 289 |
}
|
|
|
|
| 292 |
await loadPlayCanvasRobust({ esmFirst: true, loadTimeoutMs: 15000 });
|
| 293 |
} catch (e) {
|
| 294 |
console.error("Chargement PlayCanvas échoué ->", e);
|
| 295 |
+
messageToast("Impossible de charger PlayCanvas (réseau/CDN). Réessaie plus tard.");
|
| 296 |
return;
|
| 297 |
}
|
| 298 |
initARApp(GLB_URL);
|
|
|
|
| 301 |
// ============ App Android/Desktop (WebXR) ============
|
| 302 |
function initARApp(GLB_URL) {
|
| 303 |
var pc = window.pc;
|
| 304 |
+
|
| 305 |
+
// Canvas + UI
|
| 306 |
var canvas = ensureCanvas();
|
| 307 |
+
var sliderPanel = ensureSliderUI(); // (anciennement 'ui', plus explicite)
|
| 308 |
+
var rotTrack = sliderPanel.querySelector("#ar-rotY-wrap"); // (anciennement rotWrap)
|
| 309 |
+
var rotThumb = sliderPanel.querySelector("#ar-rotY-knob"); // (anciennement rotKnob)
|
| 310 |
+
var rotRangeInput = sliderPanel.querySelector("#ar-rotY"); // (anciennement rotYInput)
|
| 311 |
+
var rotValueLabel = sliderPanel.querySelector("#ar-rotY-val"); // (anciennement rotYVal)
|
| 312 |
|
| 313 |
window.focus();
|
| 314 |
|
| 315 |
+
// --- Application PlayCanvas ---
|
| 316 |
var app = new pc.Application(canvas, {
|
| 317 |
mouse: new pc.Mouse(canvas),
|
| 318 |
touch: new pc.TouchDevice(canvas),
|
|
|
|
| 328 |
app.on("destroy", function () { window.removeEventListener("resize", onResize); });
|
| 329 |
app.start();
|
| 330 |
|
| 331 |
+
// --- Rendu / PBR defaults ---
|
| 332 |
app.scene.gammaCorrection = pc.GAMMA_SRGB;
|
| 333 |
app.scene.toneMapping = pc.TONEMAP_ACES;
|
| 334 |
app.scene.exposure = 1;
|
| 335 |
app.scene.ambientLight = new pc.Color(1, 1, 1);
|
| 336 |
|
| 337 |
+
// --- Caméra + lumière directionnelle ---
|
| 338 |
var camera = new pc.Entity("Camera");
|
| 339 |
camera.addComponent("camera", { clearColor: new pc.Color(0, 0, 0, 0), farClip: 10000 });
|
| 340 |
app.root.addChild(camera);
|
| 341 |
|
| 342 |
+
var mainLight = new pc.Entity("Light");
|
| 343 |
+
mainLight.addComponent("light", { type: "directional", intensity: 1.0, castShadows: true, color: new pc.Color(1, 1, 1) });
|
| 344 |
+
mainLight.setLocalEulerAngles(45, 30, 0);
|
| 345 |
+
app.root.addChild(mainLight);
|
| 346 |
|
| 347 |
+
// --- Réticule ---
|
| 348 |
var reticleMat = new pc.StandardMaterial();
|
| 349 |
reticleMat.diffuse = new pc.Color(0.2, 0.8, 1.0);
|
| 350 |
reticleMat.opacity = 0.85;
|
|
|
|
| 357 |
reticle.enabled = false;
|
| 358 |
app.root.addChild(reticle);
|
| 359 |
|
| 360 |
+
// --- Conteneur modèle ---
|
| 361 |
var modelRoot = new pc.Entity("ModelRoot");
|
| 362 |
modelRoot.enabled = false;
|
| 363 |
app.root.addChild(modelRoot);
|
| 364 |
+
|
| 365 |
var modelLoaded = false, placedOnce = false;
|
| 366 |
|
| 367 |
+
// ===== Ombre de contact “blob” =====
|
| 368 |
+
var blobShadowEntity = null;
|
| 369 |
var BLOB_SIZE = 0.4;
|
| 370 |
var BLOB_OFFSET_Y = 0.005;
|
| 371 |
|
|
|
|
| 428 |
|
| 429 |
function updateKnobFromY(yDeg) {
|
| 430 |
var t = 1 - (yDeg / 360);
|
| 431 |
+
rotThumb.style.top = String(t * 100) + "%";
|
| 432 |
+
rotRangeInput.value = String(Math.round(yDeg));
|
| 433 |
+
rotValueLabel.textContent = String(Math.round(yDeg)) + "°";
|
| 434 |
}
|
| 435 |
function applyRotationY(deg) {
|
| 436 |
var y = clamp360(deg);
|
|
|
|
| 440 |
}
|
| 441 |
|
| 442 |
function updateBlobPositionUnder(pos, rotLikePlane) {
|
| 443 |
+
if (!blobShadowEntity) return;
|
| 444 |
+
blobShadowEntity.setPosition(pos.x, pos.y + BLOB_OFFSET_Y, pos.z);
|
| 445 |
+
if (rotLikePlane) blobShadowEntity.setRotation(rotLikePlane);
|
| 446 |
}
|
| 447 |
|
| 448 |
// Chargement GLB (depuis config.json)
|
| 449 |
app.assets.loadFromUrl(GLB_URL, "container", function (err, asset) {
|
| 450 |
+
if (err) { console.error(err); messageToast("Échec du chargement du modèle GLB."); return; }
|
| 451 |
var instance = asset.resource.instantiateRenderEntity({ castShadows: true, receiveShadows: false });
|
| 452 |
modelRoot.addChild(instance);
|
| 453 |
modelRoot.setLocalScale(1, 1, 1);
|
|
|
|
| 472 |
baseEulerX = initE.x; baseEulerZ = initE.z;
|
| 473 |
|
| 474 |
modelLoaded = true;
|
| 475 |
+
|
| 476 |
+
// >>> ICI : message centré et plus grand <<<
|
| 477 |
+
messageCenterBig("Modèle chargé. Touchez l’écran pour démarrer l’AR.");
|
| 478 |
});
|
| 479 |
|
| 480 |
+
if (!app.xr.supported) { messageToast("WebXR n’est pas supporté sur cet appareil."); return; }
|
| 481 |
|
| 482 |
+
// ---------- Slider fiable (pointer events en capture) ----------
|
| 483 |
+
var isUIInteracting = false;
|
| 484 |
+
var isTrackDragging = false;
|
| 485 |
var activePointerId = null;
|
| 486 |
|
| 487 |
+
function insideTrack(target) { return rotTrack.contains(target); }
|
| 488 |
function degFromPointer(e) {
|
| 489 |
+
var rect = rotTrack.getBoundingClientRect();
|
| 490 |
var y = (e.clientY != null) ? e.clientY : ((e.touches && e.touches[0] && e.touches[0].clientY) || 0);
|
| 491 |
var ratio = (y - rect.top) / rect.height;
|
| 492 |
var t = Math.max(0, Math.min(1, ratio));
|
|
|
|
| 494 |
}
|
| 495 |
|
| 496 |
function onPointerDownCapture(e) {
|
| 497 |
+
if (!insideTrack(e.target)) return;
|
| 498 |
+
isUIInteracting = true;
|
| 499 |
+
isTrackDragging = true;
|
| 500 |
activePointerId = (e.pointerId != null) ? e.pointerId : 1;
|
| 501 |
+
if (rotTrack.setPointerCapture) {
|
| 502 |
+
try { rotTrack.setPointerCapture(activePointerId); } catch (er) {}
|
| 503 |
}
|
| 504 |
applyRotationY(degFromPointer(e));
|
| 505 |
e.preventDefault();
|
| 506 |
e.stopPropagation();
|
| 507 |
}
|
| 508 |
function onPointerMoveCapture(e) {
|
| 509 |
+
if (!isTrackDragging || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
|
| 510 |
applyRotationY(degFromPointer(e));
|
| 511 |
e.preventDefault();
|
| 512 |
e.stopPropagation();
|
| 513 |
}
|
| 514 |
function endDrag(e) {
|
| 515 |
+
if (!isTrackDragging || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
|
| 516 |
+
isTrackDragging = false;
|
| 517 |
+
isUIInteracting = false;
|
| 518 |
+
if (rotTrack.releasePointerCapture) {
|
| 519 |
+
try { rotTrack.releasePointerCapture(activePointerId); } catch (er) {}
|
| 520 |
}
|
| 521 |
activePointerId = null;
|
| 522 |
e.preventDefault();
|
|
|
|
| 530 |
|
| 531 |
// --- Démarrage AR (Android/Desktop)
|
| 532 |
function activateAR() {
|
| 533 |
+
if (!app.xr.isAvailable(pc.XRTYPE_AR)) { messageToast("AR immersive indisponible sur cet appareil."); return; }
|
| 534 |
if (!app.xr.domOverlay) app.xr.domOverlay = {};
|
| 535 |
app.xr.domOverlay.root = document.getElementById("xr-overlay-root");
|
| 536 |
camera.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, {
|
|
|
|
| 539 |
callback: function (err) {
|
| 540 |
if (err) {
|
| 541 |
console.error("Échec du démarrage AR :", err);
|
| 542 |
+
messageToast("Échec du démarrage AR : " + (err.message || err));
|
| 543 |
}
|
| 544 |
}
|
| 545 |
});
|
| 546 |
}
|
| 547 |
+
// Tap écran → démarre l'AR (si pas sur le slider)
|
| 548 |
+
app.mouse.on("mousedown", function () { if (!app.xr.active && !isUIInteracting) activateAR(); });
|
| 549 |
if (app.touch) {
|
| 550 |
app.touch.on("touchend", function (evt) {
|
| 551 |
+
if (!app.xr.active && !isUIInteracting) activateAR();
|
| 552 |
evt.event.preventDefault();
|
| 553 |
evt.event.stopPropagation();
|
| 554 |
});
|
|
|
|
| 568 |
app.xr.hitTest.start({
|
| 569 |
entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
|
| 570 |
callback: function (err, hitSource) {
|
| 571 |
+
if (err) { messageToast("Le AR hit test n’a pas pu démarrer."); return; }
|
| 572 |
hitSource.on("result", function (pos, rot) {
|
| 573 |
if (!isHorizontalUpFacing(rot)) return;
|
| 574 |
|
|
|
|
| 581 |
modelRoot.setPosition(pos);
|
| 582 |
|
| 583 |
// Ombre de contact
|
| 584 |
+
blobShadowEntity = createBlobShadowAt(pos, rot);
|
| 585 |
|
| 586 |
var e = new pc.Vec3();
|
| 587 |
rot.getEulerAngles(e);
|
|
|
|
| 589 |
applyRotationY(y0);
|
| 590 |
|
| 591 |
placedOnce = true;
|
| 592 |
+
rotRangeInput.disabled = false;
|
| 593 |
+
|
| 594 |
+
// Après placement, on repasse aux toasts bas classiques
|
| 595 |
+
messageToast("Objet placé. Glissez pour déplacer, tournez-le avec le slider →");
|
| 596 |
}
|
| 597 |
});
|
| 598 |
}
|
|
|
|
| 600 |
});
|
| 601 |
|
| 602 |
// Déplacement XR (drag) — ignoré si UI active
|
| 603 |
+
var isModelDragging = false;
|
| 604 |
app.xr.input.on("add", function (inputSource) {
|
| 605 |
inputSource.on("selectstart", function () {
|
| 606 |
+
if (isUIInteracting) return;
|
| 607 |
if (!placedOnce || !modelLoaded) return;
|
| 608 |
|
| 609 |
inputSource.hitTestStart({
|
| 610 |
entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
|
| 611 |
callback: function (err, transientSource) {
|
| 612 |
if (err) return;
|
| 613 |
+
isModelDragging = true;
|
| 614 |
|
| 615 |
transientSource.on("result", function (pos, rot) {
|
| 616 |
+
if (!isModelDragging) return;
|
| 617 |
if (!isHorizontalUpFacing(rot)) return;
|
| 618 |
modelRoot.setPosition(pos);
|
| 619 |
updateBlobPositionUnder(pos, rot);
|
| 620 |
});
|
| 621 |
|
| 622 |
+
transientSource.once("remove", function () { isModelDragging = false; });
|
| 623 |
}
|
| 624 |
});
|
| 625 |
});
|
| 626 |
+
inputSource.on("selectend", function () { isModelDragging = false; });
|
| 627 |
});
|
| 628 |
|
| 629 |
// Desktop : rotation souris (ignore si UI)
|
| 630 |
+
var isRotateMode = false, lastMouseX = 0;
|
| 631 |
var ROTATE_SENSITIVITY = 0.25;
|
| 632 |
app.mouse.on("mousedown", function (e) {
|
| 633 |
+
if (!app.xr.active || !placedOnce || isUIInteracting) return;
|
| 634 |
if (e.button === 0 && !e.shiftKey) {
|
| 635 |
+
isModelDragging = true;
|
| 636 |
} else if (e.button === 2 || (e.button === 0 && e.shiftKey)) {
|
| 637 |
+
isRotateMode = true;
|
| 638 |
lastMouseX = e.x;
|
| 639 |
}
|
| 640 |
});
|
| 641 |
app.mouse.on("mousemove", function (e) {
|
| 642 |
+
if (!app.xr.active || !placedOnce || isUIInteracting) return;
|
| 643 |
+
if (isModelDragging) {
|
| 644 |
if (reticle.enabled) {
|
| 645 |
var p = reticle.getPosition();
|
| 646 |
modelRoot.setPosition(p);
|
| 647 |
updateBlobPositionUnder(p, null);
|
| 648 |
}
|
| 649 |
+
} else if (isRotateMode && modelRoot.enabled) {
|
| 650 |
var dx = e.x - lastMouseX;
|
| 651 |
lastMouseX = e.x;
|
| 652 |
applyRotationY(rotationYDeg + dx * ROTATE_SENSITIVITY);
|
| 653 |
}
|
| 654 |
});
|
| 655 |
+
app.mouse.on("mouseup", function () { isModelDragging = false; isRotateMode = false; });
|
| 656 |
window.addEventListener("contextmenu", function (e) { e.preventDefault(); });
|
| 657 |
|
| 658 |
// Slider (accessibilité clavier)
|
| 659 |
+
rotRangeInput.disabled = true;
|
| 660 |
+
rotRangeInput.addEventListener("input", function (e) {
|
| 661 |
if (!modelRoot.enabled) return;
|
| 662 |
var v = parseFloat(e.target.value || "0");
|
| 663 |
applyRotationY(v);
|
| 664 |
}, { passive: true });
|
| 665 |
|
| 666 |
// AR events
|
| 667 |
+
app.xr.on("start", function () { messageToast("Session AR démarrée. Visez le sol pour détecter un plan…"); reticle.enabled = true; });
|
| 668 |
+
app.xr.on("end", function () { messageToast("Session AR terminée."); reticle.enabled = false; isModelDragging = false; isRotateMode = false; rotRangeInput.disabled = true; });
|
| 669 |
app.xr.on("available:" + pc.XRTYPE_AR, function (a) {
|
| 670 |
+
if (!a) messageToast("AR immersive indisponible.");
|
| 671 |
+
else if (!app.xr.hitTest.supported) messageToast("AR Hit Test non supporté.");
|
| 672 |
+
else messageToast(modelLoaded ? "Touchez l’écran pour démarrer l’AR." : "Chargement du modèle…");
|
| 673 |
});
|
| 674 |
|
| 675 |
+
if (!app.xr.isAvailable(pc.XRTYPE_AR)) messageToast("AR immersive indisponible.");
|
| 676 |
+
else if (!app.xr.hitTest.supported) messageToast("AR Hit Test non supporté.");
|
| 677 |
+
else messageToast("Chargement du modèle…");
|
| 678 |
}
|
| 679 |
})();
|
| 680 |
+
</script>
|