MikaFil commited on
Commit
085d305
·
verified ·
1 Parent(s): 084ad0a

Update viewer_ar_ios.js

Browse files
Files changed (1) hide show
  1. 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
- ".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:24px;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}",
 
 
 
 
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:22px;text-align:center;opacity:.95}",
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:22px;opacity:.95}",
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
- function message(msg) {
 
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.textContent = msg;
 
 
 
 
 
 
 
 
 
 
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="160" height="44"><rect rx="8" ry="8" width="160" height="44" fill="black"/><text x="80" y="28" font-size="24" text-anchor="middle" fill="white" font-family="system-ui, -apple-system, Segoe UI, Roboto">Voir en AR</text></svg>');
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
- message("iOS détecté : utilisez le bouton « Voir en AR » (AR Quick Look).");
265
  } else {
266
- message("iOS détecté, mais aucun 'usdz_url' dans config.json.");
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
- message("Impossible de charger PlayCanvas (réseau/CDN). Réessaie plus tard.");
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 ui = ensureSliderUI();
286
- var rotWrap = ui.querySelector("#ar-rotY-wrap");
287
- var rotKnob = ui.querySelector("#ar-rotY-knob");
288
- var rotYInput = ui.querySelector("#ar-rotY");
289
- var rotYVal = ui.querySelector("#ar-rotY-val");
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
- // Camera + lumière
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 light = new pc.Entity("Light");
320
- light.addComponent("light", { type: "directional", intensity: 1.0, castShadows: true, color: new pc.Color(1, 1, 1) });
321
- light.setLocalEulerAngles(45, 30, 0);
322
- app.root.addChild(light);
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
- // Modèle
338
  var modelRoot = new pc.Entity("ModelRoot");
339
  modelRoot.enabled = false;
340
  app.root.addChild(modelRoot);
 
341
  var modelLoaded = false, placedOnce = false;
342
 
343
- // ===== Blob Shadow =====
344
- var blob = null;
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
- rotKnob.style.top = String(t * 100) + "%";
408
- rotYInput.value = String(Math.round(yDeg));
409
- rotYVal.textContent = String(Math.round(yDeg)) + "°";
410
  }
411
  function applyRotationY(deg) {
412
  var y = clamp360(deg);
@@ -416,14 +440,14 @@
416
  }
417
 
418
  function updateBlobPositionUnder(pos, rotLikePlane) {
419
- if (!blob) return;
420
- blob.setPosition(pos.x, pos.y + BLOB_OFFSET_Y, pos.z);
421
- if (rotLikePlane) blob.setRotation(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); message("Échec du chargement du modèle GLB."); return; }
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
- message("Modèle chargé. Touchez l’écran pour démarrer l’AR.");
 
 
452
  });
453
 
454
- if (!app.xr.supported) { message("WebXR n’est pas supporté sur cet appareil."); return; }
455
 
456
- // Slider fiable (pointer events en capture)
457
- var uiInteracting = false;
458
- var draggingWrap = false;
459
  var activePointerId = null;
460
 
461
- function insideWrap(target) { return rotWrap.contains(target); }
462
  function degFromPointer(e) {
463
- var rect = rotWrap.getBoundingClientRect();
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 (!insideWrap(e.target)) return;
472
- uiInteracting = true;
473
- draggingWrap = true;
474
  activePointerId = (e.pointerId != null) ? e.pointerId : 1;
475
- if (rotWrap.setPointerCapture) {
476
- try { rotWrap.setPointerCapture(activePointerId); } catch (er) {}
477
  }
478
  applyRotationY(degFromPointer(e));
479
  e.preventDefault();
480
  e.stopPropagation();
481
  }
482
  function onPointerMoveCapture(e) {
483
- if (!draggingWrap || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
484
  applyRotationY(degFromPointer(e));
485
  e.preventDefault();
486
  e.stopPropagation();
487
  }
488
  function endDrag(e) {
489
- if (!draggingWrap || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
490
- draggingWrap = false;
491
- uiInteracting = false;
492
- if (rotWrap.releasePointerCapture) {
493
- try { rotWrap.releasePointerCapture(activePointerId); } catch (er) {}
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)) { message("AR immersive indisponible sur cet appareil."); return; }
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
- message("Échec du démarrage AR : " + (err.message || err));
517
  }
518
  }
519
  });
520
  }
521
- app.mouse.on("mousedown", function () { if (!app.xr.active && !uiInteracting) activateAR(); });
 
522
  if (app.touch) {
523
  app.touch.on("touchend", function (evt) {
524
- if (!app.xr.active && !uiInteracting) activateAR();
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) { message("Le AR hit test n’a pas pu démarrer."); return; }
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
- blob = createBlobShadowAt(pos, rot);
558
 
559
  var e = new pc.Vec3();
560
  rot.getEulerAngles(e);
@@ -562,8 +589,10 @@
562
  applyRotationY(y0);
563
 
564
  placedOnce = true;
565
- rotYInput.disabled = false;
566
- message("Objet placé. Glissez pour déplacer, tournez-le avec le slider →");
 
 
567
  }
568
  });
569
  }
@@ -571,80 +600,81 @@
571
  });
572
 
573
  // Déplacement XR (drag) — ignoré si UI active
574
- var isDragging = false;
575
  app.xr.input.on("add", function (inputSource) {
576
  inputSource.on("selectstart", function () {
577
- if (uiInteracting) return;
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
- isDragging = true;
585
 
586
  transientSource.on("result", function (pos, rot) {
587
- if (!isDragging) return;
588
  if (!isHorizontalUpFacing(rot)) return;
589
  modelRoot.setPosition(pos);
590
  updateBlobPositionUnder(pos, rot);
591
  });
592
 
593
- transientSource.once("remove", function () { isDragging = false; });
594
  }
595
  });
596
  });
597
- inputSource.on("selectend", function () { isDragging = false; });
598
  });
599
 
600
  // Desktop : rotation souris (ignore si UI)
601
- var rotateMode = false, lastMouseX = 0;
602
  var ROTATE_SENSITIVITY = 0.25;
603
  app.mouse.on("mousedown", function (e) {
604
- if (!app.xr.active || !placedOnce || uiInteracting) return;
605
  if (e.button === 0 && !e.shiftKey) {
606
- isDragging = true;
607
  } else if (e.button === 2 || (e.button === 0 && e.shiftKey)) {
608
- rotateMode = true;
609
  lastMouseX = e.x;
610
  }
611
  });
612
  app.mouse.on("mousemove", function (e) {
613
- if (!app.xr.active || !placedOnce || uiInteracting) return;
614
- if (isDragging) {
615
  if (reticle.enabled) {
616
  var p = reticle.getPosition();
617
  modelRoot.setPosition(p);
618
  updateBlobPositionUnder(p, null);
619
  }
620
- } else if (rotateMode && modelRoot.enabled) {
621
  var dx = e.x - lastMouseX;
622
  lastMouseX = e.x;
623
  applyRotationY(rotationYDeg + dx * ROTATE_SENSITIVITY);
624
  }
625
  });
626
- app.mouse.on("mouseup", function () { isDragging = false; rotateMode = false; });
627
  window.addEventListener("contextmenu", function (e) { e.preventDefault(); });
628
 
629
  // Slider (accessibilité clavier)
630
- rotYInput.disabled = true;
631
- rotYInput.addEventListener("input", function (e) {
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 () { message("Session AR démarrée. Visez le sol pour détecter un plan…"); reticle.enabled = true; });
639
- app.xr.on("end", function () { message("Session AR terminée."); reticle.enabled = false; isDragging = false; rotateMode = false; rotYInput.disabled = true; });
640
  app.xr.on("available:" + pc.XRTYPE_AR, function (a) {
641
- if (!a) message("AR immersive indisponible.");
642
- else if (!app.xr.hitTest.supported) message("AR Hit Test non supporté.");
643
- else message(modelLoaded ? "Touchez l’écran pour démarrer l’AR." : "Chargement du modèle…");
644
  });
645
 
646
- if (!app.xr.isAvailable(pc.XRTYPE_AR)) message("AR immersive indisponible.");
647
- else if (!app.xr.hitTest.supported) message("AR Hit Test non supporté.");
648
- else message("Chargement du modèle…");
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>