MikaFil commited on
Commit
bc24a3e
·
verified ·
1 Parent(s): 0e13190

Update viewer_ar.js

Browse files
Files changed (1) hide show
  1. viewer_ar.js +101 -262
viewer_ar.js CHANGED
@@ -25,9 +25,7 @@
25
  ]
26
  };
27
 
28
- function timeout(ms) {
29
- return new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), ms));
30
- }
31
 
32
  async function loadPlayCanvasRobust({ esmFirst = true, loadTimeoutMs = 15000 } = {}) {
33
  if (window.pc && typeof window.pc.Application === "function") return window.pc;
@@ -37,11 +35,8 @@
37
  try {
38
  const mod = await Promise.race([import(/* @vite-ignore */ url), timeout(loadTimeoutMs)]);
39
  const namespace = mod?.pc || mod?.default || mod;
40
- if (namespace?.Application) {
41
- if (!window.pc) window.pc = namespace;
42
- return window.pc;
43
- }
44
- } catch (_) { /* continue */ }
45
  }
46
  throw new Error("ESM failed");
47
  };
@@ -52,27 +47,20 @@
52
  await Promise.race([
53
  new Promise((res, rej) => {
54
  const s = document.createElement("script");
55
- s.src = url;
56
- s.async = true;
57
- s.onload = () => res();
58
- s.onerror = () => rej(new Error("script error"));
59
  document.head.appendChild(s);
60
  }),
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
  // =========================
@@ -85,100 +73,55 @@
85
  background: rgba(0,0,0,.65); color: #fff; border-radius: 8px;
86
  font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
87
  font-size: 14px; line-height: 1.3; text-align: center;
88
- max-width: min(90vw, 640px);
89
- box-shadow: 0 6px 20px rgba(0,0,0,.25);
90
- backdrop-filter: blur(4px);
91
- user-select: none;
92
- pointer-events: none;
93
  }
94
- /* Racine DOM Overlay (tout ce qui doit rester visible en AR doit être descendant) */
95
  #xr-overlay-root {
96
- position: fixed;
97
- inset: 0;
98
- z-index: 10000; /* au-dessus du canvas XR */
99
- pointer-events: none; /* par défaut, mais les enfants peuvent réactiver */
100
  }
101
- /* --- UI Slider (DOM overlay) --- */
102
  .ar-ui {
103
- position: absolute;
104
- right: 12px;
105
- top: 50%;
106
- transform: translateY(-50%);
107
- background: rgba(0,0,0,0.55);
108
- color: #fff;
109
- padding: 12px 10px;
110
- border-radius: 12px;
111
  font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
112
- pointer-events: auto; /* capter interactions pendant AR */
113
- width: 56px;
114
- display: flex; flex-direction: column; align-items: center; gap: 8px;
115
- box-shadow: 0 6px 18px rgba(0,0,0,.25);
116
- backdrop-filter: blur(4px);
117
  }
118
  .ar-ui .label { font-size: 12px; text-align: center; opacity: 0.9; }
119
- /* Slider vertical (on le tourne) */
120
  .ar-ui input[type="range"].rotY {
121
- -webkit-appearance: none;
122
- width: 220px; /* longueur avant rotation */
123
- height: 28px;
124
- transform: rotate(-90deg);
125
- outline: none;
126
- background: transparent;
127
- touch-action: none;
128
  }
129
  .ar-ui input[type="range"].rotY::-webkit-slider-thumb {
130
- -webkit-appearance: none;
131
- appearance: none;
132
- width: 20px; height: 20px;
133
- border-radius: 50%;
134
- background: #fff;
135
- border: none;
136
- box-shadow: 0 2px 8px rgba(0,0,0,.35);
137
  }
138
  .ar-ui input[type="range"].rotY::-webkit-slider-runnable-track {
139
- height: 6px;
140
- background: rgba(255,255,255,.6);
141
- border-radius: 4px;
142
  }
143
  .ar-ui .val { font-size: 11px; opacity: 0.85; }
144
  `;
145
- const styleTag = document.createElement("style");
146
- styleTag.textContent = css;
147
- document.head.appendChild(styleTag);
148
 
149
  function message(msg) {
150
  let el = document.querySelector(".pc-ar-msg");
151
- if (!el) {
152
- el = document.createElement("div");
153
- el.className = "pc-ar-msg";
154
- document.body.appendChild(el);
155
- }
156
  el.textContent = msg;
157
  }
158
 
159
  function ensureCanvas() {
160
  let canvas = document.getElementById("application-canvas");
161
- if (!canvas) {
162
- canvas = document.createElement("canvas");
163
- canvas.id = "application-canvas";
164
- canvas.style.width = "100%";
165
- canvas.style.height = "100%";
166
- document.body.appendChild(canvas);
167
- }
168
  return canvas;
169
  }
170
 
171
  function ensureOverlayRoot() {
172
  let root = document.getElementById("xr-overlay-root");
173
- if (!root) {
174
- root = document.createElement("div");
175
- root.id = "xr-overlay-root";
176
- document.body.appendChild(root);
177
- }
178
  return root;
179
  }
180
 
181
- // Crée le panneau slider DANS la racine d’overlay
182
  function ensureSliderUI(overlayRoot) {
183
  let panel = overlayRoot.querySelector(".ar-ui");
184
  if (panel) return panel;
@@ -197,13 +140,8 @@
197
  // 4) Lancement
198
  // =========================
199
  (async () => {
200
- try {
201
- await loadPlayCanvasRobust({ esmFirst: true, loadTimeoutMs: 15000 });
202
- } catch (e) {
203
- console.error("Chargement PlayCanvas échoué ->", e);
204
- message("Impossible de charger PlayCanvas (réseau/CDN). Réessaie plus tard.");
205
- return;
206
- }
207
  initARApp();
208
  })();
209
 
@@ -213,7 +151,7 @@
213
  function initARApp() {
214
  const pc = window.pc;
215
  const canvas = ensureCanvas();
216
- const overlayRoot = ensureOverlayRoot(); // <-- racine DOM overlay
217
  const uiPanel = ensureSliderUI(overlayRoot);
218
  const rotYInput = uiPanel.querySelector("#ar-rotY");
219
  const rotYVal = uiPanel.querySelector("#ar-rotY-val");
@@ -234,55 +172,43 @@
234
  const onResize = () => app.resizeCanvas();
235
  window.addEventListener("resize", onResize);
236
  app.on("destroy", () => window.removeEventListener("resize", onResize));
237
-
238
  app.start();
239
 
240
  // --- Camera & light ---
241
  const camera = new pc.Entity("Camera");
242
- camera.addComponent("camera", {
243
- clearColor: new pc.Color(0, 0, 0, 0),
244
- farClip: 10000
245
- });
246
  app.root.addChild(camera);
247
 
248
  const light = new pc.Entity("Light");
249
- light.addComponent("light", {
250
- type: "spot",
251
- range: 30,
252
- intensity: 1.1,
253
- castShadows: false
254
- });
255
  light.setLocalPosition(0, 10, 0);
256
  app.root.addChild(light);
257
 
258
- // --- Reticle (anneau) ---
259
  const reticleMat = new pc.StandardMaterial();
260
- reticleMat.diffuse = new pc.Color(0.2, 0.8, 1.0);
261
- reticleMat.opacity = 0.85;
262
- reticleMat.blendType = pc.BLEND_NORMAL;
263
- reticleMat.update();
264
-
265
  const reticle = new pc.Entity("Reticle");
266
  reticle.addComponent("render", { type: "torus", material: reticleMat });
267
- reticle.setLocalScale(0.12, 0.005, 0.12);
268
- reticle.enabled = false;
269
- app.root.addChild(reticle);
270
 
271
- // --- Conteneur modèle GLB ---
272
  const modelRoot = new pc.Entity("ModelRoot");
273
- modelRoot.enabled = false;
274
- app.root.addChild(modelRoot);
275
 
276
- let modelLoaded = false;
277
- let placedOnce = false;
278
 
279
- // État rotation Y (en degrés, 0..360)
 
 
 
 
 
 
 
 
 
280
  let rotationYDeg = 0;
281
- const norm360 = (deg) => {
282
- let d = deg % 360;
283
- if (d < 0) d += 360;
284
- return d;
285
- };
286
  const applyRotationY = (deg) => {
287
  rotationYDeg = norm360(deg);
288
  const eul = modelRoot.getEulerAngles();
@@ -291,87 +217,47 @@
291
  rotYVal.textContent = `${Math.round(rotationYDeg)}°`;
292
  };
293
 
294
- // --- Chargement GLB ---
295
- app.assets.loadFromUrl(GLB_URL, "container", (err, asset) => {
296
- if (err) {
297
- console.error(err);
298
- message("Échec du chargement du modèle GLB.");
299
- return;
300
- }
301
- const instance = asset.resource.instantiateRenderEntity({
302
- castShadows: false,
303
- receiveShadows: false
304
- });
305
- modelRoot.addChild(instance);
306
- modelRoot.setLocalScale(0.2, 0.2, 0.2);
307
 
308
- modelLoaded = true;
309
- message("Modèle chargé. Touchez l’écran pour démarrer l’AR.");
310
- });
311
 
312
- // --- Vérifs WebXR ---
313
- if (!app.xr.supported) {
314
- message("WebXR nest pas supporté sur cet appareil.");
315
- return;
316
- }
317
-
318
- // --- Démarrage AR (DOM Overlay assuré) ---
319
- const activateAR = () => {
320
- if (!app.xr.isAvailable(pc.XRTYPE_AR)) {
321
- message("AR immersive indisponible sur cet appareil.");
322
- return;
323
  }
324
- camera.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, {
325
- // IMPORTANT : tout élément DOM que tu veux voir en AR doit être descendant de 'overlayRoot'
326
- domOverlay: { root: overlayRoot },
327
- requiredFeatures: ["hit-test", "dom-overlay"], // on exige le DOM overlay
328
- optionalFeatures: ["plane-detection"],
329
- callback: (err) => {
330
- if (err) message(`Échec du démarrage AR : ${err.message}`);
331
- }
332
- });
333
- };
334
 
335
- // Premier tap / clic démarrer AR
336
- app.mouse.on("mousedown", () => {
337
- if (!app.xr.active) activateAR();
338
- });
 
 
339
  if (app.touch) {
340
  app.touch.on("touchend", (evt) => {
341
  if (!app.xr.active) activateAR();
342
- evt.event.preventDefault();
343
- evt.event.stopPropagation();
344
  });
345
  }
346
 
347
  // ESC → quitter
348
- app.keyboard.on("keydown", (evt) => {
349
- if (evt.key === pc.KEY_ESCAPE && app.xr.active) app.xr.end();
350
- });
351
 
352
- // --- Hit Test global (réglage réticule + 1er placement) ---
353
  app.xr.hitTest.on("available", () => {
354
  app.xr.hitTest.start({
355
  entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
356
  callback: (err, hitSource) => {
357
- if (err) {
358
- message("Le AR hit test n’a pas pu démarrer.");
359
- return;
360
- }
361
  hitSource.on("result", (pos, rot) => {
362
- reticle.enabled = true;
363
- reticle.setPosition(pos);
364
- reticle.setRotation(rot);
365
-
366
  if (modelLoaded && !placedOnce) {
367
- modelRoot.enabled = true;
368
- modelRoot.setPosition(pos);
369
- const euler = new pc.Vec3();
370
- rot.getEulerAngles(euler);
371
- rotationYDeg = norm360(euler.y);
372
- applyRotationY(rotationYDeg);
373
- placedOnce = true;
374
- rotYInput.disabled = false;
375
  message("Objet placé. Glissez pour déplacer, tournez-le avec le slider →");
376
  }
377
  });
@@ -379,13 +265,12 @@
379
  });
380
  });
381
 
382
- // --- Déplacement (drag) via input XR ---
383
  let isDragging = false;
384
  const activeTransientSources = new Set();
385
  function cancelAllTransientHitTests() {
386
  activeTransientSources.forEach(src => { try { src.remove && src.remove(); } catch(_) {} });
387
- activeTransientSources.clear();
388
- isDragging = false;
389
  }
390
 
391
  app.xr.input.on("add", (inputSource) => {
@@ -394,110 +279,64 @@
394
  inputSource.hitTestStart({
395
  entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
396
  callback: (err, transientSource) => {
397
- if (err) return;
398
- isDragging = true;
399
- activeTransientSources.add(transientSource);
400
-
401
- transientSource.on("result", (pos /*, rot */) => {
402
- if (!isDragging) return;
403
- modelRoot.setPosition(pos);
404
- });
405
-
406
- transientSource.once("remove", () => {
407
- activeTransientSources.delete(transientSource);
408
- if (activeTransientSources.size === 0) isDragging = false;
409
- });
410
  }
411
  });
412
  });
413
-
414
- inputSource.on("selectend", () => {
415
- cancelAllTransientHitTests();
416
- });
417
  });
418
 
419
- // ❌ Rotation tactile 2 doigts — SUPPRIMÉE
420
-
421
  // Souris : déplacement (clic gauche) & rotation (clic droit ou Shift+clic gauche)
422
- let rotateMode = false;
423
- let lastMouseX = 0;
424
- const ROTATE_SENSITIVITY = 0.25; // degrés / pixel approx.
425
 
426
  app.mouse.on("mousedown", (e) => {
427
  if (!app.xr.active || !placedOnce) return;
428
-
429
- if (e.button === 0 && !e.shiftKey) {
430
- isDragging = true;
431
- } else if (e.button === 2 || (e.button === 0 && e.shiftKey)) {
432
- rotateMode = true;
433
- lastMouseX = e.x;
434
- }
435
  });
436
 
437
  app.mouse.on("mousemove", (e) => {
438
  if (!app.xr.active || !placedOnce) return;
439
-
440
- if (isDragging) {
441
- if (reticle.enabled) {
442
- const pos = reticle.getPosition();
443
- modelRoot.setPosition(pos);
444
- }
445
- } else if (rotateMode && modelRoot.enabled) {
446
- const dx = e.x - lastMouseX;
447
- lastMouseX = e.x;
448
- applyRotationY(rotationYDeg + dx * ROTATE_SENSITIVITY);
449
- }
450
- });
451
-
452
- app.mouse.on("mouseup", () => {
453
- isDragging = false;
454
- rotateMode = false;
455
  });
456
 
 
457
  window.addEventListener("contextmenu", (e) => e.preventDefault());
458
 
459
- // --- Slider rotation (DOM overlay) ---
460
- rotYInput.disabled = true; // activé au 1er placement
461
- rotYInput.addEventListener("input", (e) => {
462
- if (!modelRoot.enabled) return;
463
- const deg = parseFloat(e.target.value || "0");
464
- applyRotationY(deg);
465
- }, { passive: true });
466
 
467
- // --- Événements AR globaux ---
468
  app.xr.on("start", () => {
469
- // s'assurer que l'overlay (et donc le slider) reste visible en AR
470
- overlayRoot.style.display = "block";
 
 
 
 
471
  message("Session AR démarrée. Visez le sol pour détecter un plan…");
472
  reticle.enabled = true;
473
  });
474
 
475
  app.xr.on("end", () => {
476
  message("Session AR terminée.");
477
- reticle.enabled = false;
478
- isDragging = false;
479
- rotateMode = false;
480
- rotYInput.disabled = !placedOnce;
481
- // on garde l'overlay affiché hors AR aussi
482
  });
483
 
484
  app.xr.on(`available:${pc.XRTYPE_AR}`, (available) => {
485
- if (!available) {
486
- message("AR immersive indisponible.");
487
- } else if (!app.xr.hitTest.supported) {
488
- message("AR Hit Test non supporté.");
489
- } else {
490
- message(modelLoaded ? "Touchez l’écran pour démarrer l’AR." : "Chargement du modèle…");
491
- }
492
  });
493
 
494
  // Message initial
495
- if (!app.xr.isAvailable(pc.XRTYPE_AR)) {
496
- message("AR immersive indisponible.");
497
- } else if (!app.xr.hitTest.supported) {
498
- message("AR Hit Test non supporté.");
499
- } else {
500
- message("Chargement du modèle…");
501
- }
502
  }
503
  })();
 
25
  ]
26
  };
27
 
28
+ function timeout(ms) { return new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), ms)); }
 
 
29
 
30
  async function loadPlayCanvasRobust({ esmFirst = true, loadTimeoutMs = 15000 } = {}) {
31
  if (window.pc && typeof window.pc.Application === "function") return window.pc;
 
35
  try {
36
  const mod = await Promise.race([import(/* @vite-ignore */ url), timeout(loadTimeoutMs)]);
37
  const namespace = mod?.pc || mod?.default || mod;
38
+ if (namespace?.Application) { if (!window.pc) window.pc = namespace; return window.pc; }
39
+ } catch (_) {}
 
 
 
40
  }
41
  throw new Error("ESM failed");
42
  };
 
47
  await Promise.race([
48
  new Promise((res, rej) => {
49
  const s = document.createElement("script");
50
+ s.src = url; s.async = true;
51
+ s.onload = () => res(); s.onerror = () => rej(new Error("script error"));
 
 
52
  document.head.appendChild(s);
53
  }),
54
  timeout(loadTimeoutMs)
55
  ]);
56
  if (window.pc && typeof window.pc.Application === "function") return window.pc;
57
+ } catch (_) {}
58
  }
59
  throw new Error("UMD failed");
60
  };
61
 
62
+ if (esmFirst) { try { return await tryESM(); } catch (_) {} return await tryUMD(); }
63
+ else { try { return await tryUMD(); } catch (_) {} return await tryESM(); }
 
 
 
 
 
64
  }
65
 
66
  // =========================
 
73
  background: rgba(0,0,0,.65); color: #fff; border-radius: 8px;
74
  font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
75
  font-size: 14px; line-height: 1.3; text-align: center;
76
+ max-width: min(90vw, 640px); box-shadow: 0 6px 20px rgba(0,0,0,.25);
77
+ backdrop-filter: blur(4px); user-select: none; pointer-events: none;
 
 
 
78
  }
79
+ /* Racine DOM Overlay : tout ce qui doit rester visible en AR doit être descendant */
80
  #xr-overlay-root {
81
+ position: fixed; inset: 0; z-index: 10000; pointer-events: none;
 
 
 
82
  }
83
+ /* --- UI Slider --- */
84
  .ar-ui {
85
+ position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
86
+ background: rgba(0,0,0,0.55); color: #fff; padding: 12px 10px; border-radius: 12px;
 
 
 
 
 
 
87
  font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
88
+ pointer-events: auto; width: 56px; display: flex; flex-direction: column;
89
+ align-items: center; gap: 8px; box-shadow: 0 6px 18px rgba(0,0,0,.25); backdrop-filter: blur(4px);
 
 
 
90
  }
91
  .ar-ui .label { font-size: 12px; text-align: center; opacity: 0.9; }
 
92
  .ar-ui input[type="range"].rotY {
93
+ -webkit-appearance: none; width: 220px; height: 28px; transform: rotate(-90deg);
94
+ outline: none; background: transparent; touch-action: none;
 
 
 
 
 
95
  }
96
  .ar-ui input[type="range"].rotY::-webkit-slider-thumb {
97
+ -webkit-appearance: none; appearance: none; width: 20px; height: 20px; border-radius: 50%;
98
+ background: #fff; border: none; box-shadow: 0 2px 8px rgba(0,0,0,.35);
 
 
 
 
 
99
  }
100
  .ar-ui input[type="range"].rotY::-webkit-slider-runnable-track {
101
+ height: 6px; background: rgba(255,255,255,.6); border-radius: 4px;
 
 
102
  }
103
  .ar-ui .val { font-size: 11px; opacity: 0.85; }
104
  `;
105
+ const styleTag = document.createElement("style"); styleTag.textContent = css; document.head.appendChild(styleTag);
 
 
106
 
107
  function message(msg) {
108
  let el = document.querySelector(".pc-ar-msg");
109
+ if (!el) { el = document.createElement("div"); el.className = "pc-ar-msg"; document.body.appendChild(el); }
 
 
 
 
110
  el.textContent = msg;
111
  }
112
 
113
  function ensureCanvas() {
114
  let canvas = document.getElementById("application-canvas");
115
+ if (!canvas) { canvas = document.createElement("canvas"); canvas.id = "application-canvas"; canvas.style.width = "100%"; canvas.style.height = "100%"; document.body.appendChild(canvas); }
 
 
 
 
 
 
116
  return canvas;
117
  }
118
 
119
  function ensureOverlayRoot() {
120
  let root = document.getElementById("xr-overlay-root");
121
+ if (!root) { root = document.createElement("div"); root.id = "xr-overlay-root"; document.body.appendChild(root); }
 
 
 
 
122
  return root;
123
  }
124
 
 
125
  function ensureSliderUI(overlayRoot) {
126
  let panel = overlayRoot.querySelector(".ar-ui");
127
  if (panel) return panel;
 
140
  // 4) Lancement
141
  // =========================
142
  (async () => {
143
+ try { await loadPlayCanvasRobust({ esmFirst: true, loadTimeoutMs: 15000 }); }
144
+ catch (e) { console.error("Chargement PlayCanvas échoué ->", e); message("Impossible de charger PlayCanvas (réseau/CDN). Réessaie plus tard."); return; }
 
 
 
 
 
145
  initARApp();
146
  })();
147
 
 
151
  function initARApp() {
152
  const pc = window.pc;
153
  const canvas = ensureCanvas();
154
+ const overlayRoot = ensureOverlayRoot(); // racine DOM overlay
155
  const uiPanel = ensureSliderUI(overlayRoot);
156
  const rotYInput = uiPanel.querySelector("#ar-rotY");
157
  const rotYVal = uiPanel.querySelector("#ar-rotY-val");
 
172
  const onResize = () => app.resizeCanvas();
173
  window.addEventListener("resize", onResize);
174
  app.on("destroy", () => window.removeEventListener("resize", onResize));
 
175
  app.start();
176
 
177
  // --- Camera & light ---
178
  const camera = new pc.Entity("Camera");
179
+ camera.addComponent("camera", { clearColor: new pc.Color(0, 0, 0, 0), farClip: 10000 });
 
 
 
180
  app.root.addChild(camera);
181
 
182
  const light = new pc.Entity("Light");
183
+ light.addComponent("light", { type: "spot", range: 30, intensity: 1.1, castShadows: false });
 
 
 
 
 
184
  light.setLocalPosition(0, 10, 0);
185
  app.root.addChild(light);
186
 
187
+ // --- Reticle ---
188
  const reticleMat = new pc.StandardMaterial();
189
+ reticleMat.diffuse = new pc.Color(0.2, 0.8, 1.0); reticleMat.opacity = 0.85; reticleMat.blendType = pc.BLEND_NORMAL; reticleMat.update();
 
 
 
 
190
  const reticle = new pc.Entity("Reticle");
191
  reticle.addComponent("render", { type: "torus", material: reticleMat });
192
+ reticle.setLocalScale(0.12, 0.005, 0.12); reticle.enabled = false; app.root.addChild(reticle);
 
 
193
 
194
+ // --- Modèle GLB ---
195
  const modelRoot = new pc.Entity("ModelRoot");
196
+ modelRoot.enabled = false; app.root.addChild(modelRoot);
 
197
 
198
+ let modelLoaded = false, placedOnce = false;
 
199
 
200
+ app.assets.loadFromUrl(GLB_URL, "container", (err, asset) => {
201
+ if (err) { console.error(err); message("Échec du chargement du modèle GLB."); return; }
202
+ const instance = asset.resource.instantiateRenderEntity({ castShadows: false, receiveShadows: false });
203
+ modelRoot.addChild(instance);
204
+ modelRoot.setLocalScale(0.2, 0.2, 0.2);
205
+ modelLoaded = true;
206
+ message("Modèle chargé. Touchez l’écran pour démarrer l’AR.");
207
+ });
208
+
209
+ // === Rotation Y via slider ===
210
  let rotationYDeg = 0;
211
+ const norm360 = (deg) => { let d = deg % 360; if (d < 0) d += 360; return d; };
 
 
 
 
212
  const applyRotationY = (deg) => {
213
  rotationYDeg = norm360(deg);
214
  const eul = modelRoot.getEulerAngles();
 
217
  rotYVal.textContent = `${Math.round(rotationYDeg)}°`;
218
  };
219
 
220
+ // === Démarrage AR — CORRIGÉ ===
221
+ function activateAR() {
222
+ if (!app.xr.isAvailable(pc.XRTYPE_AR)) { message("AR immersive indisponible sur cet appareil."); return; }
 
 
 
 
 
 
 
 
 
 
223
 
224
+ // 1) déclarer la racine DOM overlay AVANT de démarrer la session
225
+ app.xr.domOverlay.root = overlayRoot; // <-- clé : c’est ainsi que PlayCanvas enregistre l’overlay
 
226
 
227
+ // (optionnel) feedback support
228
+ if (!app.xr.domOverlay.supported) {
229
+ console.warn("DOM Overlay non supporté par lUA — le slider peut ne pas apparaître en AR.");
 
 
 
 
 
 
 
 
230
  }
 
 
 
 
 
 
 
 
 
 
231
 
232
+ // 2) démarrer l’AR via l’API PlayCanvas (et non camera.camera.startXr)
233
+ app.xr.start(camera, pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR);
234
+ }
235
+
236
+ // Tap / clic → start AR
237
+ app.mouse.on("mousedown", () => { if (!app.xr.active) activateAR(); });
238
  if (app.touch) {
239
  app.touch.on("touchend", (evt) => {
240
  if (!app.xr.active) activateAR();
241
+ evt.event.preventDefault(); evt.event.stopPropagation();
 
242
  });
243
  }
244
 
245
  // ESC → quitter
246
+ app.keyboard.on("keydown", (evt) => { if (evt.key === pc.KEY_ESCAPE && app.xr.active) app.xr.end(); });
 
 
247
 
248
+ // Hit Test global (réglage réticule + 1er placement)
249
  app.xr.hitTest.on("available", () => {
250
  app.xr.hitTest.start({
251
  entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
252
  callback: (err, hitSource) => {
253
+ if (err) { message("Le AR hit test n’a pas pu démarrer."); return; }
 
 
 
254
  hitSource.on("result", (pos, rot) => {
255
+ reticle.enabled = true; reticle.setPosition(pos); reticle.setRotation(rot);
 
 
 
256
  if (modelLoaded && !placedOnce) {
257
+ modelRoot.enabled = true; modelRoot.setPosition(pos);
258
+ const euler = new pc.Vec3(); rot.getEulerAngles(euler);
259
+ rotationYDeg = norm360(euler.y); applyRotationY(rotationYDeg);
260
+ placedOnce = true; rotYInput.disabled = false;
 
 
 
 
261
  message("Objet placé. Glissez pour déplacer, tournez-le avec le slider →");
262
  }
263
  });
 
265
  });
266
  });
267
 
268
+ // Déplacement (drag) via input XR
269
  let isDragging = false;
270
  const activeTransientSources = new Set();
271
  function cancelAllTransientHitTests() {
272
  activeTransientSources.forEach(src => { try { src.remove && src.remove(); } catch(_) {} });
273
+ activeTransientSources.clear(); isDragging = false;
 
274
  }
275
 
276
  app.xr.input.on("add", (inputSource) => {
 
279
  inputSource.hitTestStart({
280
  entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
281
  callback: (err, transientSource) => {
282
+ if (err) return; isDragging = true; activeTransientSources.add(transientSource);
283
+ transientSource.on("result", (pos) => { if (isDragging) modelRoot.setPosition(pos); });
284
+ transientSource.once("remove", () => { activeTransientSources.delete(transientSource); if (activeTransientSources.size === 0) isDragging = false; });
 
 
 
 
 
 
 
 
 
 
285
  }
286
  });
287
  });
288
+ inputSource.on("selectend", () => { cancelAllTransientHitTests(); });
 
 
 
289
  });
290
 
 
 
291
  // Souris : déplacement (clic gauche) & rotation (clic droit ou Shift+clic gauche)
292
+ let rotateMode = false, lastMouseX = 0;
293
+ const ROTATE_SENSITIVITY = 0.25;
 
294
 
295
  app.mouse.on("mousedown", (e) => {
296
  if (!app.xr.active || !placedOnce) return;
297
+ if (e.button === 0 && !e.shiftKey) isDragging = true;
298
+ else if (e.button === 2 || (e.button === 0 && e.shiftKey)) { rotateMode = true; lastMouseX = e.x; }
 
 
 
 
 
299
  });
300
 
301
  app.mouse.on("mousemove", (e) => {
302
  if (!app.xr.active || !placedOnce) return;
303
+ if (isDragging) { if (reticle.enabled) modelRoot.setPosition(reticle.getPosition()); }
304
+ else if (rotateMode && modelRoot.enabled) { const dx = e.x - lastMouseX; lastMouseX = e.x; applyRotationY(rotationYDeg + dx * ROTATE_SENSITIVITY); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  });
306
 
307
+ app.mouse.on("mouseup", () => { isDragging = false; rotateMode = false; });
308
  window.addEventListener("contextmenu", (e) => e.preventDefault());
309
 
310
+ // Slider rotation
311
+ rotYInput.disabled = true;
312
+ rotYInput.addEventListener("input", (e) => { if (modelRoot.enabled) applyRotationY(parseFloat(e.target.value || "0")); }, { passive: true });
 
 
 
 
313
 
314
+ // Événements AR
315
  app.xr.on("start", () => {
316
+ // overlay disponible ?
317
+ if (app.xr.domOverlay.available) {
318
+ // rien à faire : overlayRoot est déjà l’UI parent (slider visible)
319
+ } else {
320
+ console.warn("DOM Overlay non disponible pendant la session.");
321
+ }
322
  message("Session AR démarrée. Visez le sol pour détecter un plan…");
323
  reticle.enabled = true;
324
  });
325
 
326
  app.xr.on("end", () => {
327
  message("Session AR terminée.");
328
+ reticle.enabled = false; isDragging = false; rotateMode = false; rotYInput.disabled = !placedOnce;
 
 
 
 
329
  });
330
 
331
  app.xr.on(`available:${pc.XRTYPE_AR}`, (available) => {
332
+ if (!available) message("AR immersive indisponible.");
333
+ else if (!app.xr.hitTest.supported) message("AR Hit Test non supporté.");
334
+ else message(modelLoaded ? "Touchez l’écran pour démarrer l’AR." : "Chargement du modèle…");
 
 
 
 
335
  });
336
 
337
  // Message initial
338
+ if (!app.xr.isAvailable(pc.XRTYPE_AR)) message("AR immersive indisponible.");
339
+ else if (!app.xr.hitTest.supported) message("AR Hit Test non supporté.");
340
+ else message("Chargement du modèle…");
 
 
 
 
341
  }
342
  })();