MikaFil commited on
Commit
50241ae
·
verified ·
1 Parent(s): 84e939d

Update viewer_ar.js

Browse files
Files changed (1) hide show
  1. viewer_ar.js +59 -143
viewer_ar.js CHANGED
@@ -1,6 +1,7 @@
1
  /* script_ar.js — AR PlayCanvas + GLB HuggingFace
2
- - Chargeur PlayCanvas robuste (ESM -> UMD avec fallbacks + timeout)
3
- - WebXR AR Hit Test, placement auto, drag + rotation Y via SLIDER (DOM Overlay)
 
4
  - Aucune dépendance externe autre que PlayCanvas et le GLB
5
  */
6
 
@@ -11,18 +12,12 @@
11
  const GLB_URL = "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/AR/tests/fraisier.glb";
12
 
13
  // =========================
14
- // 2) Chargeur PlayCanvas robuste
15
  // =========================
 
16
  const PC_URLS = {
17
- esm: [
18
- "https://cdn.jsdelivr.net/npm/playcanvas@latest/build/playcanvas.min.mjs",
19
- "https://cdn.jsdelivr.net/npm/playcanvas@2/build/playcanvas.min.mjs"
20
- ],
21
- umd: [
22
- "https://cdnjs.cloudflare.com/ajax/libs/playcanvas/2.11.7/playcanvas.min.js",
23
- "https://cdn.jsdelivr.net/npm/playcanvas@latest/build/playcanvas.min.js",
24
- "https://cdn.jsdelivr.net/npm/playcanvas@2/build/playcanvas.min.js"
25
- ]
26
  };
27
 
28
  function timeout(ms) {
@@ -41,7 +36,7 @@
41
  if (!window.pc) window.pc = namespace;
42
  return window.pc;
43
  }
44
- } catch (_) { /* continue */ }
45
  }
46
  throw new Error("ESM failed");
47
  };
@@ -61,77 +56,50 @@
61
  timeout(loadTimeoutMs)
62
  ]);
63
  if (window.pc && typeof window.pc.Application === "function") return window.pc;
64
- } catch (_) { /* continue */ }
65
  }
66
  throw new Error("UMD failed");
67
  };
68
 
69
  if (esmFirst) {
70
- try { return await tryESM(); } catch (_) { /* fallback */ }
71
  return await tryUMD();
72
  } else {
73
- try { return await tryUMD(); } catch (_) { /* fallback */ }
74
  return await tryESM();
75
  }
76
  }
77
 
78
  // =========================
79
- // 3) UI & utilitaires
80
  // =========================
81
  const css = `
82
- /* Messages in-overlay */
83
  .pc-ar-msg {
84
  position: fixed; left: 50%; transform: translateX(-50%);
85
  bottom: 16px; z-index: 2; padding: 10px 14px;
86
  background: rgba(0,0,0,.65); color: #fff; border-radius: 8px;
87
  font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
88
  font-size: 14px; line-height: 1.3; text-align: center;
89
- max-width: min(90vw, 640px);
90
- box-shadow: 0 6px 20px rgba(0,0,0,.25);
91
- backdrop-filter: blur(4px);
92
- user-select: none;
93
- pointer-events: none;
94
  }
95
- /* Racine DOM Overlay : tout ce qui doit rester visible en AR doit être descendant d’ici */
96
  #xr-overlay-root {
97
- position: fixed;
98
- inset: 0;
99
- z-index: 9999; /* au-dessus du canvas XR */
100
- pointer-events: none; /* les enfants réactivent si besoin */
101
  }
102
- /* --- UI Slider (DOM overlay) --- */
103
  .ar-ui {
104
- position: absolute;
105
- right: 12px;
106
- top: 50%;
107
- transform: translateY(-50%);
108
- background: rgba(0,0,0,0.55);
109
- color: #fff;
110
- padding: 12px 10px;
111
- border-radius: 12px;
112
  font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
113
- pointer-events: auto; /* capter les interactions pendant AR */
114
- width: 56px;
115
- display: flex; flex-direction: column; align-items: center; gap: 8px;
116
- box-shadow: 0 6px 18px rgba(0,0,0,.25);
117
- backdrop-filter: blur(4px);
118
- }
119
- .ar-ui .label {
120
- font-size: 12px;
121
- text-align: center;
122
- opacity: 0.9;
123
  }
 
124
  .ar-ui input[type="range"].rotY {
125
- -webkit-appearance: none;
126
- width: 220px; height: 28px;
127
- transform: rotate(-90deg);
128
- outline: none;
129
- background: transparent;
130
- touch-action: none;
131
  }
132
  .ar-ui input[type="range"].rotY::-webkit-slider-thumb {
133
- -webkit-appearance: none; appearance: none;
134
- width: 20px; height: 20px; border-radius: 50%;
135
  background: #fff; border: none; box-shadow: 0 2px 8px rgba(0,0,0,.35);
136
  }
137
  .ar-ui input[type="range"].rotY::-webkit-slider-runnable-track {
@@ -143,7 +111,6 @@
143
  styleTag.textContent = css;
144
  document.head.appendChild(styleTag);
145
 
146
- // Racine d'overlay (toujours présente)
147
  function ensureOverlayRoot() {
148
  let root = document.getElementById("xr-overlay-root");
149
  if (!root) {
@@ -155,7 +122,6 @@
155
  }
156
  const overlayRoot = ensureOverlayRoot();
157
 
158
- // Message -> dans l'overlay (pour rester visible en AR)
159
  function message(msg) {
160
  let el = overlayRoot.querySelector(".pc-ar-msg");
161
  if (!el) {
@@ -178,7 +144,6 @@
178
  return canvas;
179
  }
180
 
181
- // Crée le panneau slider (DANS la racine overlay)
182
  function ensureSliderUI() {
183
  let panel = overlayRoot.querySelector(".ar-ui");
184
  if (panel) return panel;
@@ -211,7 +176,7 @@
211
  // 5) App AR PlayCanvas
212
  // =========================
213
  function initARApp() {
214
- const pc = window.pc; // alias
215
  const canvas = ensureCanvas();
216
  const uiPanel = ensureSliderUI();
217
  const rotYInput = uiPanel.querySelector("#ar-rotY");
@@ -275,18 +240,13 @@
275
  let modelLoaded = false;
276
  let placedOnce = false;
277
 
278
- // État rotation Y (en degrés, 0..360)
279
  let rotationYDeg = 0;
280
- const norm360 = (deg) => {
281
- let d = deg % 360;
282
- if (d < 0) d += 360;
283
- return d;
284
- };
285
  const applyRotationY = (deg) => {
286
  rotationYDeg = norm360(deg);
287
  const eul = modelRoot.getEulerAngles();
288
  modelRoot.setEulerAngles(eul.x, rotationYDeg, eul.z);
289
- // sync UI
290
  rotYInput.value = String(Math.round(rotationYDeg));
291
  rotYVal.textContent = `${Math.round(rotationYDeg)}°`;
292
  };
@@ -303,8 +263,6 @@
303
  receiveShadows: false
304
  });
305
  modelRoot.addChild(instance);
306
-
307
- // Ajuste si besoin selon ton modèle
308
  modelRoot.setLocalScale(0.2, 0.2, 0.2);
309
 
310
  modelLoaded = true;
@@ -317,28 +275,31 @@
317
  return;
318
  }
319
 
320
- // --- Démarrage AR avec DOM Overlay correctement enregistré ---
321
  const activateAR = () => {
322
  if (!app.xr.isAvailable(pc.XRTYPE_AR)) {
323
  message("AR immersive indisponible sur cet appareil.");
324
  return;
325
  }
326
 
327
- // 1) Enregistrer la racine DOM overlay AVANT le start
328
  app.xr.domOverlay.root = document.getElementById("xr-overlay-root");
329
 
330
- // 2) Démarrer via l’API PlayCanvas, en passant les features (dont dom-overlay)
331
- app.xr.start(camera, pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, {
332
  requiredFeatures: ["hit-test", "dom-overlay"],
333
- optionalFeatures: ["plane-detection"],
334
- domOverlay: { root: app.xr.domOverlay.root }
 
 
 
 
 
335
  });
336
  };
337
 
338
  // Premier tap / clic → démarrer AR
339
- app.mouse.on("mousedown", () => {
340
- if (!app.xr.active) activateAR();
341
- });
342
  if (app.touch) {
343
  app.touch.on("touchend", (evt) => {
344
  if (!app.xr.active) activateAR();
@@ -348,19 +309,14 @@
348
  }
349
 
350
  // ESC → quitter
351
- app.keyboard.on("keydown", (evt) => {
352
- if (evt.key === pc.KEY_ESCAPE && app.xr.active) app.xr.end();
353
- });
354
 
355
  // --- Hit Test global (réglage réticule + 1er placement) ---
356
  app.xr.hitTest.on("available", () => {
357
  app.xr.hitTest.start({
358
  entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
359
  callback: (err, hitSource) => {
360
- if (err) {
361
- message("Le AR hit test n’a pas pu démarrer.");
362
- return;
363
- }
364
  hitSource.on("result", (pos, rot) => {
365
  reticle.enabled = true;
366
  reticle.setPosition(pos);
@@ -369,13 +325,11 @@
369
  if (modelLoaded && !placedOnce) {
370
  modelRoot.enabled = true;
371
  modelRoot.setPosition(pos);
372
- // Conserver yaw initiale détectée
373
  const euler = new pc.Vec3();
374
  rot.getEulerAngles(euler);
375
  rotationYDeg = norm360(euler.y);
376
  applyRotationY(rotationYDeg);
377
  placedOnce = true;
378
- // activer le slider
379
  rotYInput.disabled = false;
380
  message("Objet placé. Glissez pour déplacer, tournez-le avec le slider →");
381
  }
@@ -384,79 +338,50 @@
384
  });
385
  });
386
 
387
- // --- Interactions (déplacement & rotation souris seulement) ---
388
  let isDragging = false;
389
  let rotateMode = false;
390
  let lastMouseX = 0;
391
- const ROTATE_SENSITIVITY = 0.25; // degrés / pixel approx.
392
 
393
  // Déplacement (input XR : maintien/drag)
394
  app.xr.input.on("add", (inputSource) => {
395
  inputSource.on("selectstart", () => {
396
  if (!placedOnce || !modelLoaded) return;
397
-
398
  inputSource.hitTestStart({
399
  entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
400
  callback: (err, transientSource) => {
401
  if (err) return;
402
  isDragging = true;
403
-
404
- transientSource.on("result", (pos /*, rot */) => {
405
- if (!isDragging) return;
406
- modelRoot.setPosition(pos);
407
- });
408
-
409
- transientSource.once("remove", () => {
410
- isDragging = false;
411
- });
412
  }
413
  });
414
  });
415
-
416
- inputSource.on("selectend", () => {
417
- isDragging = false;
418
- });
419
  });
420
 
421
- // Rotation tactile 2 doigts SUPPRIMÉE
422
-
423
- // Souris : déplacement (clic gauche) & rotation (clic droit ou Shift+clic gauche) — conservée
424
  app.mouse.on("mousedown", (e) => {
425
  if (!app.xr.active || !placedOnce) return;
426
-
427
- if (e.button === 0 && !e.shiftKey) {
428
- isDragging = true;
429
- } else if (e.button === 2 || (e.button === 0 && e.shiftKey)) {
430
- rotateMode = true;
431
- lastMouseX = e.x;
432
- }
433
  });
434
 
435
  app.mouse.on("mousemove", (e) => {
436
  if (!app.xr.active || !placedOnce) return;
437
-
438
- if (isDragging) {
439
- if (reticle.enabled) {
440
- const pos = reticle.getPosition();
441
- modelRoot.setPosition(pos);
442
- }
443
- } else if (rotateMode && modelRoot.enabled) {
444
- const dx = e.x - lastMouseX;
445
- lastMouseX = e.x;
446
  applyRotationY(rotationYDeg + dx * ROTATE_SENSITIVITY);
447
  }
448
  });
449
 
450
- app.mouse.on("mouseup", () => {
451
- isDragging = false;
452
- rotateMode = false;
453
- });
454
-
455
- // Empêcher menu contextuel (utile pour la rotation au clic droit)
456
  window.addEventListener("contextmenu", (e) => e.preventDefault());
457
 
458
  // --- Slider rotation (DOM overlay) ---
459
- rotYInput.disabled = true; // activé au 1er placement
460
  rotYInput.addEventListener("input", (e) => {
461
  if (!modelRoot.enabled) return;
462
  const deg = parseFloat(e.target.value || "0");
@@ -465,7 +390,6 @@
465
 
466
  // --- Événements AR globaux ---
467
  app.xr.on("start", () => {
468
- // overlay présent pendant la session ?
469
  console.log("[XR] start — domOverlay.supported:", app.xr.domOverlay.supported, "available:", app.xr.domOverlay.available);
470
  message("Session AR démarrée. Visez le sol pour détecter un plan…");
471
  reticle.enabled = true;
@@ -480,22 +404,14 @@
480
  });
481
 
482
  app.xr.on(`available:${pc.XRTYPE_AR}`, (available) => {
483
- if (!available) {
484
- message("AR immersive indisponible.");
485
- } else if (!app.xr.hitTest.supported) {
486
- message("AR Hit Test non supporté.");
487
- } else {
488
- message(modelLoaded ? "Touchez l’écran pour démarrer l’AR." : "Chargement du modèle…");
489
- }
490
  });
491
 
492
  // Message initial
493
- if (!app.xr.isAvailable(pc.XRTYPE_AR)) {
494
- message("AR immersive indisponible.");
495
- } else if (!app.xr.hitTest.supported) {
496
- message("AR Hit Test non supporté.");
497
- } else {
498
- message("Chargement du modèle…");
499
- }
500
  }
501
  })();
 
1
  /* script_ar.js — AR PlayCanvas + GLB HuggingFace
2
+ - Version PC verrouillée
3
+ - WebXR AR Hit Test, placement auto, drag
4
+ - Rotation Y via SLIDER (DOM Overlay)
5
  - Aucune dépendance externe autre que PlayCanvas et le GLB
6
  */
7
 
 
12
  const GLB_URL = "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/AR/tests/fraisier.glb";
13
 
14
  // =========================
15
+ // 2) Chargeur PlayCanvas ROBUSTE (version verrouillée)
16
  // =========================
17
+ const PC_VERSION = "2.11.7"; // même version pour ESM et UMD
18
  const PC_URLS = {
19
+ esm: [`https://cdn.jsdelivr.net/npm/playcanvas@${PC_VERSION}/build/playcanvas.min.mjs`],
20
+ umd: [`https://cdn.jsdelivr.net/npm/playcanvas@${PC_VERSION}/build/playcanvas.min.js`]
 
 
 
 
 
 
 
21
  };
22
 
23
  function timeout(ms) {
 
36
  if (!window.pc) window.pc = namespace;
37
  return window.pc;
38
  }
39
+ } catch (_) {}
40
  }
41
  throw new Error("ESM failed");
42
  };
 
56
  timeout(loadTimeoutMs)
57
  ]);
58
  if (window.pc && typeof window.pc.Application === "function") return window.pc;
59
+ } catch (_) {}
60
  }
61
  throw new Error("UMD failed");
62
  };
63
 
64
  if (esmFirst) {
65
+ try { return await tryESM(); } catch (_) {}
66
  return await tryUMD();
67
  } else {
68
+ try { return await tryUMD(); } catch (_) {}
69
  return await tryESM();
70
  }
71
  }
72
 
73
  // =========================
74
+ // 3) UI & utilitaires (DOM Overlay)
75
  // =========================
76
  const css = `
 
77
  .pc-ar-msg {
78
  position: fixed; left: 50%; transform: translateX(-50%);
79
  bottom: 16px; z-index: 2; padding: 10px 14px;
80
  background: rgba(0,0,0,.65); color: #fff; border-radius: 8px;
81
  font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
82
  font-size: 14px; line-height: 1.3; text-align: center;
83
+ max-width: min(90vw, 640px); box-shadow: 0 6px 20px rgba(0,0,0,.25);
84
+ backdrop-filter: blur(4px); user-select: none; pointer-events: none;
 
 
 
85
  }
 
86
  #xr-overlay-root {
87
+ position: fixed; inset: 0; z-index: 9999; pointer-events: none;
 
 
 
88
  }
 
89
  .ar-ui {
90
+ position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
91
+ background: rgba(0,0,0,0.55); color: #fff; padding: 12px 10px; border-radius: 12px;
 
 
 
 
 
 
92
  font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
93
+ pointer-events: auto; width: 56px; display: flex; flex-direction: column; align-items: center; gap: 8px;
94
+ box-shadow: 0 6px 18px rgba(0,0,0,.25); backdrop-filter: blur(4px);
 
 
 
 
 
 
 
 
95
  }
96
+ .ar-ui .label { font-size: 12px; text-align: center; opacity: 0.9; }
97
  .ar-ui input[type="range"].rotY {
98
+ -webkit-appearance: none; width: 220px; height: 28px; transform: rotate(-90deg);
99
+ outline: none; background: transparent; touch-action: none;
 
 
 
 
100
  }
101
  .ar-ui input[type="range"].rotY::-webkit-slider-thumb {
102
+ -webkit-appearance: none; appearance: none; width: 20px; height: 20px; border-radius: 50%;
 
103
  background: #fff; border: none; box-shadow: 0 2px 8px rgba(0,0,0,.35);
104
  }
105
  .ar-ui input[type="range"].rotY::-webkit-slider-runnable-track {
 
111
  styleTag.textContent = css;
112
  document.head.appendChild(styleTag);
113
 
 
114
  function ensureOverlayRoot() {
115
  let root = document.getElementById("xr-overlay-root");
116
  if (!root) {
 
122
  }
123
  const overlayRoot = ensureOverlayRoot();
124
 
 
125
  function message(msg) {
126
  let el = overlayRoot.querySelector(".pc-ar-msg");
127
  if (!el) {
 
144
  return canvas;
145
  }
146
 
 
147
  function ensureSliderUI() {
148
  let panel = overlayRoot.querySelector(".ar-ui");
149
  if (panel) return panel;
 
176
  // 5) App AR PlayCanvas
177
  // =========================
178
  function initARApp() {
179
+ const pc = window.pc;
180
  const canvas = ensureCanvas();
181
  const uiPanel = ensureSliderUI();
182
  const rotYInput = uiPanel.querySelector("#ar-rotY");
 
240
  let modelLoaded = false;
241
  let placedOnce = false;
242
 
243
+ // Rotation Y (0..360)
244
  let rotationYDeg = 0;
245
+ const norm360 = (deg) => { let d = deg % 360; return d < 0 ? d + 360 : d; };
 
 
 
 
246
  const applyRotationY = (deg) => {
247
  rotationYDeg = norm360(deg);
248
  const eul = modelRoot.getEulerAngles();
249
  modelRoot.setEulerAngles(eul.x, rotationYDeg, eul.z);
 
250
  rotYInput.value = String(Math.round(rotationYDeg));
251
  rotYVal.textContent = `${Math.round(rotationYDeg)}°`;
252
  };
 
263
  receiveShadows: false
264
  });
265
  modelRoot.addChild(instance);
 
 
266
  modelRoot.setLocalScale(0.2, 0.2, 0.2);
267
 
268
  modelLoaded = true;
 
275
  return;
276
  }
277
 
278
+ // --- Démarrage AR (CameraComponent.startXr + DOM Overlay) ---
279
  const activateAR = () => {
280
  if (!app.xr.isAvailable(pc.XRTYPE_AR)) {
281
  message("AR immersive indisponible sur cet appareil.");
282
  return;
283
  }
284
 
285
+ // DOM overlay root (notre conteneur avec slider + messages)
286
  app.xr.domOverlay.root = document.getElementById("xr-overlay-root");
287
 
288
+ // Démarre via la Camera Component (plus compatible selon versions moteur)
289
+ camera.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, {
290
  requiredFeatures: ["hit-test", "dom-overlay"],
291
+ domOverlay: { root: app.xr.domOverlay.root },
292
+ callback: (err) => {
293
+ if (err) {
294
+ console.error("Échec du démarrage AR :", err);
295
+ message(`Échec du démarrage AR : ${err.message || err}`);
296
+ }
297
+ }
298
  });
299
  };
300
 
301
  // Premier tap / clic → démarrer AR
302
+ app.mouse.on("mousedown", () => { if (!app.xr.active) activateAR(); });
 
 
303
  if (app.touch) {
304
  app.touch.on("touchend", (evt) => {
305
  if (!app.xr.active) activateAR();
 
309
  }
310
 
311
  // ESC → quitter
312
+ app.keyboard.on("keydown", (evt) => { if (evt.key === pc.KEY_ESCAPE && app.xr.active) app.xr.end(); });
 
 
313
 
314
  // --- Hit Test global (réglage réticule + 1er placement) ---
315
  app.xr.hitTest.on("available", () => {
316
  app.xr.hitTest.start({
317
  entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
318
  callback: (err, hitSource) => {
319
+ if (err) { message("Le AR hit test n’a pas pu démarrer."); return; }
 
 
 
320
  hitSource.on("result", (pos, rot) => {
321
  reticle.enabled = true;
322
  reticle.setPosition(pos);
 
325
  if (modelLoaded && !placedOnce) {
326
  modelRoot.enabled = true;
327
  modelRoot.setPosition(pos);
 
328
  const euler = new pc.Vec3();
329
  rot.getEulerAngles(euler);
330
  rotationYDeg = norm360(euler.y);
331
  applyRotationY(rotationYDeg);
332
  placedOnce = true;
 
333
  rotYInput.disabled = false;
334
  message("Objet placé. Glissez pour déplacer, tournez-le avec le slider →");
335
  }
 
338
  });
339
  });
340
 
341
+ // --- Interactions (déplacement & rotation souris) ---
342
  let isDragging = false;
343
  let rotateMode = false;
344
  let lastMouseX = 0;
345
+ const ROTATE_SENSITIVITY = 0.25;
346
 
347
  // Déplacement (input XR : maintien/drag)
348
  app.xr.input.on("add", (inputSource) => {
349
  inputSource.on("selectstart", () => {
350
  if (!placedOnce || !modelLoaded) return;
 
351
  inputSource.hitTestStart({
352
  entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
353
  callback: (err, transientSource) => {
354
  if (err) return;
355
  isDragging = true;
356
+ transientSource.on("result", (pos) => { if (isDragging) modelRoot.setPosition(pos); });
357
+ transientSource.once("remove", () => { isDragging = false; });
 
 
 
 
 
 
 
358
  }
359
  });
360
  });
361
+ inputSource.on("selectend", () => { isDragging = false; });
 
 
 
362
  });
363
 
364
+ // Souris : déplacement (gauche) & rotation (droit ou Shift+gauche)
 
 
365
  app.mouse.on("mousedown", (e) => {
366
  if (!app.xr.active || !placedOnce) return;
367
+ if (e.button === 0 && !e.shiftKey) isDragging = true;
368
+ else if (e.button === 2 || (e.button === 0 && e.shiftKey)) { rotateMode = true; lastMouseX = e.x; }
 
 
 
 
 
369
  });
370
 
371
  app.mouse.on("mousemove", (e) => {
372
  if (!app.xr.active || !placedOnce) return;
373
+ if (isDragging) { if (reticle.enabled) modelRoot.setPosition(reticle.getPosition()); }
374
+ else if (rotateMode && modelRoot.enabled) {
375
+ const dx = e.x - lastMouseX; lastMouseX = e.x;
 
 
 
 
 
 
376
  applyRotationY(rotationYDeg + dx * ROTATE_SENSITIVITY);
377
  }
378
  });
379
 
380
+ app.mouse.on("mouseup", () => { isDragging = false; rotateMode = false; });
 
 
 
 
 
381
  window.addEventListener("contextmenu", (e) => e.preventDefault());
382
 
383
  // --- Slider rotation (DOM overlay) ---
384
+ rotYInput.disabled = true;
385
  rotYInput.addEventListener("input", (e) => {
386
  if (!modelRoot.enabled) return;
387
  const deg = parseFloat(e.target.value || "0");
 
390
 
391
  // --- Événements AR globaux ---
392
  app.xr.on("start", () => {
 
393
  console.log("[XR] start — domOverlay.supported:", app.xr.domOverlay.supported, "available:", app.xr.domOverlay.available);
394
  message("Session AR démarrée. Visez le sol pour détecter un plan…");
395
  reticle.enabled = true;
 
404
  });
405
 
406
  app.xr.on(`available:${pc.XRTYPE_AR}`, (available) => {
407
+ if (!available) message("AR immersive indisponible.");
408
+ else if (!app.xr.hitTest.supported) message("AR Hit Test non supporté.");
409
+ else message(modelLoaded ? "Touchez l’écran pour démarrer l’AR." : "Chargement du modèle…");
 
 
 
 
410
  });
411
 
412
  // Message initial
413
+ if (!app.xr.isAvailable(pc.XRTYPE_AR)) message("AR immersive indisponible.");
414
+ else if (!app.xr.hitTest.supported) message("AR Hit Test non supporté.");
415
+ else message("Chargement du modèle…");
 
 
 
 
416
  }
417
  })();