MikaFil commited on
Commit
678cb07
·
verified ·
1 Parent(s): bc24a3e

Update viewer_ar.js

Browse files
Files changed (1) hide show
  1. viewer_ar.js +262 -101
viewer_ar.js CHANGED
@@ -25,7 +25,9 @@
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,8 +37,11 @@
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,20 +52,27 @@
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,55 +85,100 @@
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,8 +197,13 @@
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,7 +213,7 @@
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,43 +234,55 @@
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,47 +291,87 @@
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 l’UA — 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,12 +379,13 @@
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,64 +394,110 @@
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
  })();
 
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
  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
  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
  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
  // 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
  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
  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
  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 n’est 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
  });
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
  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
  })();