MikaFil commited on
Commit
2d27d75
·
verified ·
1 Parent(s): 1daadf3

Update viewer_ar.js

Browse files
Files changed (1) hide show
  1. viewer_ar.js +133 -114
viewer_ar.js CHANGED
@@ -1,7 +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 (2 doigts)
4
- - DOM Overlay activé pour capter les touches en AR
5
  - Aucune dépendance externe autre que PlayCanvas et le GLB
6
  */
7
 
@@ -92,6 +92,57 @@
92
  user-select: none;
93
  pointer-events: none;
94
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  `;
96
  const styleTag = document.createElement("style");
97
  styleTag.textContent = css;
@@ -119,6 +170,22 @@
119
  return canvas;
120
  }
121
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  // =========================
123
  // 4) Lancement
124
  // =========================
@@ -139,6 +206,10 @@
139
  function initARApp() {
140
  const pc = window.pc;
141
  const canvas = ensureCanvas();
 
 
 
 
142
  window.focus();
143
 
144
  const app = new pc.Application(canvas, {
@@ -197,6 +268,26 @@
197
  let modelLoaded = false;
198
  let placedOnce = false;
199
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  // --- Chargement GLB ---
201
  app.assets.loadFromUrl(GLB_URL, "container", (err, asset) => {
202
  if (err) {
@@ -209,8 +300,6 @@
209
  receiveShadows: false
210
  });
211
  modelRoot.addChild(instance);
212
-
213
- // Ajuste si besoin selon ton modèle
214
  modelRoot.setLocalScale(0.2, 0.2, 0.2);
215
 
216
  modelLoaded = true;
@@ -223,17 +312,16 @@
223
  return;
224
  }
225
 
226
- // --- Démarrage AR (MODIF: activer DOM Overlay & features) ---
227
  const activateAR = () => {
228
  if (!app.xr.isAvailable(pc.XRTYPE_AR)) {
229
  message("AR immersive indisponible sur cet appareil.");
230
  return;
231
  }
232
  camera.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, {
233
- // IMPORTANT: activer l'overlay DOM pour recevoir les events tactiles en AR
234
- domOverlay: { root: document.body }, // <-- MODIF
235
- requiredFeatures: ["hit-test", "dom-overlay"], // <-- MODIF (hit-test déjà géré mais ok)
236
- optionalFeatures: ["plane-detection"], // <-- MODIF (facultatif)
237
  callback: (err) => {
238
  if (err) message(`Échec du démarrage AR : ${err.message}`);
239
  }
@@ -272,11 +360,13 @@
272
  if (modelLoaded && !placedOnce) {
273
  modelRoot.enabled = true;
274
  modelRoot.setPosition(pos);
 
275
  const euler = new pc.Vec3();
276
  rot.getEulerAngles(euler);
277
- modelRoot.setEulerAngles(0, euler.y, 0);
 
278
  placedOnce = true;
279
- message("Objet placé. Glissez pour déplacer, 2 doigts / clic droit pour tourner.");
280
  }
281
  });
282
  }
@@ -284,27 +374,19 @@
284
  });
285
 
286
  // =========================
287
- // --- Interactions
288
  // =========================
289
- let isDragging = false; // déplacement du modèle
290
- let rotateMode = false; // rotation 2 doigts (tactile) ou souris
291
- let multiTouchActive = false; // >= 2 doigts
292
- let lastMouseX = 0;
293
-
294
- // Hit-tests transitoires à annuler quand on passe à 2 doigts
295
  const activeTransientSources = new Set();
296
  function cancelAllTransientHitTests() {
297
- activeTransientSources.forEach(src => {
298
- try { src.remove && src.remove(); } catch(_) {}
299
- });
300
  activeTransientSources.clear();
301
  isDragging = false;
302
  }
303
 
304
- // Déplacement (input XR) — ne démarre que si pas de 2 doigts
305
  app.xr.input.on("add", (inputSource) => {
306
  inputSource.on("selectstart", () => {
307
- if (!placedOnce || !modelLoaded || multiTouchActive) return;
308
  inputSource.hitTestStart({
309
  entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
310
  callback: (err, transientSource) => {
@@ -313,7 +395,7 @@
313
  activeTransientSources.add(transientSource);
314
 
315
  transientSource.on("result", (pos /*, rot */) => {
316
- if (!isDragging || rotateMode || multiTouchActive) return;
317
  modelRoot.setPosition(pos);
318
  });
319
 
@@ -331,104 +413,51 @@
331
  });
332
 
333
  // =========================
334
- // --- Gestes tactiles NATIFS via DOM Overlay (MODIF)
335
  // =========================
336
- // On n'utilise PAS app.touch ici : on écoute le canvas en DOM Overlay XR
337
- const RAD2DEG = 180 / Math.PI;
338
- let lastTwoFingerAngle = null;
339
- let rotationAlertShown = false;
340
-
341
- function getAngleTwoTouches(touches) {
342
- if (touches.length < 2) return null;
343
- const t0 = touches[0], t1 = touches[1];
344
- return Math.atan2(t1.clientY - t0.clientY, t1.clientX - t0.clientX);
345
- }
346
-
347
- // Démarre un geste : si 2 doigts, activer rotation et couper la translation
348
- const onTouchStart = (e) => {
349
- if (!app.xr.active) return; // on s'intéresse seulement au mode AR
350
- if (e.touches.length >= 2) {
351
- multiTouchActive = true;
352
- rotateMode = true;
353
- cancelAllTransientHitTests();
354
- lastTwoFingerAngle = getAngleTwoTouches(e.touches);
355
-
356
- if (!rotationAlertShown) {
357
- rotationAlertShown = true;
358
- console.log("[AR] Rotation 2 doigts détectée — touches:", e.touches.length);
359
- try { alert("Rotation à 2 doigts détectée"); } catch(_) {}
360
- }
361
- }
362
  };
 
 
363
 
364
- const onTouchMove = (e) => {
365
- if (!app.xr.active) return;
366
- if (rotateMode && multiTouchActive && modelRoot.enabled && e.touches.length >= 2) {
367
- const ang = getAngleTwoTouches(e.touches);
368
- if (ang !== null && lastTwoFingerAngle !== null) {
369
- let delta = ang - lastTwoFingerAngle;
370
- if (delta > Math.PI) delta -= 2 * Math.PI;
371
- if (delta < -Math.PI) delta += 2 * Math.PI;
372
-
373
- const eul = modelRoot.getEulerAngles();
374
- modelRoot.setEulerAngles(eul.x, eul.y + (delta * RAD2DEG), eul.z);
375
-
376
- lastTwoFingerAngle = ang;
377
- }
378
- // empêcher le scroll/zoom navigateur & gestes par défaut
379
- e.preventDefault();
380
- }
381
- };
382
-
383
- const onTouchEnd = (e) => {
384
- if (!app.xr.active) return;
385
- if (e.touches.length < 2) {
386
- rotateMode = false;
387
- multiTouchActive = false;
388
- lastTwoFingerAngle = null;
389
- rotationAlertShown = false;
390
- }
391
- };
392
-
393
- // IMPORTANT : listeners non-passifs pour pouvoir appeler preventDefault
394
- canvas.addEventListener("touchstart", onTouchStart, { passive: false });
395
- canvas.addEventListener("touchmove", onTouchMove, { passive: false });
396
- canvas.addEventListener("touchend", onTouchEnd, { passive: false });
397
- canvas.addEventListener("touchcancel", onTouchEnd, { passive: false });
398
 
399
  // =========================
400
- // Souris (pour desktop)
 
401
  // =========================
402
  app.mouse.on("mousedown", (e) => {
403
  if (!app.xr.active || !placedOnce) return;
404
-
405
- if (e.button === 0 && !e.shiftKey) {
406
- if (!multiTouchActive) isDragging = true;
407
- } else if (e.button === 2 || (e.button === 0 && e.shiftKey)) {
408
- rotateMode = true;
409
- lastMouseX = e.x;
410
  }
411
  });
412
 
413
  app.mouse.on("mousemove", (e) => {
414
  if (!app.xr.active || !placedOnce) return;
415
-
416
- if (isDragging && !rotateMode && !multiTouchActive) {
417
- if (reticle.enabled) {
418
- const pos = reticle.getPosition();
419
- modelRoot.setPosition(pos);
420
- }
421
- } else if (rotateMode && modelRoot.enabled) {
422
- const dx = e.x - lastMouseX;
423
- lastMouseX = e.x;
424
- const euler = modelRoot.getEulerAngles();
425
- modelRoot.setEulerAngles(euler.x, euler.y + dx * 0.25, euler.z);
426
  }
427
  });
428
 
429
  app.mouse.on("mouseup", () => {
430
  isDragging = false;
431
- rotateMode = false;
432
  });
433
 
434
  window.addEventListener("contextmenu", (e) => e.preventDefault());
@@ -443,17 +472,7 @@
443
  message("Session AR terminée.");
444
  reticle.enabled = false;
445
  isDragging = false;
446
- rotateMode = false;
447
- multiTouchActive = false;
448
- lastTwoFingerAngle = null;
449
- rotationAlertShown = false;
450
  cancelAllTransientHitTests();
451
-
452
- // Nettoyage des listeners DOM (au cas où)
453
- canvas.removeEventListener("touchstart", onTouchStart);
454
- canvas.removeEventListener("touchmove", onTouchMove);
455
- canvas.removeEventListener("touchend", onTouchEnd);
456
- canvas.removeEventListener("touchcancel", onTouchEnd);
457
  });
458
 
459
  app.xr.on(`available:${pc.XRTYPE_AR}`, (available) => {
 
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 (déplacement) + rotation Y via SLIDER
4
+ - DOM Overlay activé pour utiliser le slider en AR
5
  - Aucune dépendance externe autre que PlayCanvas et le GLB
6
  */
7
 
 
92
  user-select: none;
93
  pointer-events: none;
94
  }
95
+ /* Panneau slider (DOM overlay) */
96
+ .ar-ui {
97
+ position: fixed;
98
+ right: 12px;
99
+ top: 50%;
100
+ transform: translateY(-50%);
101
+ z-index: 10000;
102
+ background: rgba(0,0,0,0.55);
103
+ color: #fff;
104
+ padding: 12px 10px;
105
+ border-radius: 12px;
106
+ font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
107
+ pointer-events: auto; /* important: capter les interactions en AR */
108
+ width: 56px;
109
+ display: flex; flex-direction: column; align-items: center; gap: 8px;
110
+ box-shadow: 0 6px 18px rgba(0,0,0,.25);
111
+ backdrop-filter: blur(4px);
112
+ }
113
+ .ar-ui .label {
114
+ font-size: 12px;
115
+ text-align: center;
116
+ opacity: 0.9;
117
+ }
118
+ /* Slider vertical via rotation (meilleure compatibilité) */
119
+ .ar-ui input[type="range"].rotY {
120
+ -webkit-appearance: none;
121
+ width: 220px; /* longueur avant rotation */
122
+ height: 28px;
123
+ transform: rotate(-90deg);
124
+ outline: none;
125
+ background: transparent;
126
+ touch-action: none;
127
+ }
128
+ .ar-ui input[type="range"].rotY::-webkit-slider-thumb {
129
+ -webkit-appearance: none;
130
+ appearance: none;
131
+ width: 20px; height: 20px;
132
+ border-radius: 50%;
133
+ background: #fff;
134
+ border: none;
135
+ box-shadow: 0 2px 8px rgba(0,0,0,.35);
136
+ }
137
+ .ar-ui input[type="range"].rotY::-webkit-slider-runnable-track {
138
+ height: 6px;
139
+ background: rgba(255,255,255,.6);
140
+ border-radius: 4px;
141
+ }
142
+ .ar-ui .val {
143
+ font-size: 11px;
144
+ opacity: 0.85;
145
+ }
146
  `;
147
  const styleTag = document.createElement("style");
148
  styleTag.textContent = css;
 
170
  return canvas;
171
  }
172
 
173
+ // Crée le panneau slider (DOM overlay)
174
+ function ensureSliderUI() {
175
+ let panel = document.querySelector(".ar-ui");
176
+ if (panel) return panel;
177
+
178
+ panel = document.createElement("div");
179
+ panel.className = "ar-ui";
180
+ panel.innerHTML = `
181
+ <div class="label">Rotation</div>
182
+ <input class="rotY" id="ar-rotY" type="range" min="0" max="360" step="1" value="0" />
183
+ <div class="val" id="ar-rotY-val">0°</div>
184
+ `;
185
+ document.body.appendChild(panel);
186
+ return panel;
187
+ }
188
+
189
  // =========================
190
  // 4) Lancement
191
  // =========================
 
206
  function initARApp() {
207
  const pc = window.pc;
208
  const canvas = ensureCanvas();
209
+ const uiPanel = ensureSliderUI();
210
+ const rotYInput = uiPanel.querySelector("#ar-rotY");
211
+ const rotYVal = uiPanel.querySelector("#ar-rotY-val");
212
+
213
  window.focus();
214
 
215
  const app = new pc.Application(canvas, {
 
268
  let modelLoaded = false;
269
  let placedOnce = false;
270
 
271
+ // État rotation Y (en degrés, 0..360)
272
+ let rotationYDeg = 0;
273
+
274
+ // Helpers
275
+ const norm360 = (deg) => {
276
+ let d = deg % 360;
277
+ if (d < 0) d += 360;
278
+ return d;
279
+ };
280
+ const applyRotationY = (deg) => {
281
+ rotationYDeg = norm360(deg);
282
+ const eul = modelRoot.getEulerAngles();
283
+ modelRoot.setEulerAngles(eul.x, rotationYDeg, eul.z);
284
+ // Synchronise l’UI si besoin
285
+ if (rotYInput && rotYVal) {
286
+ rotYInput.value = String(Math.round(rotationYDeg));
287
+ rotYVal.textContent = `${Math.round(rotationYDeg)}°`;
288
+ }
289
+ };
290
+
291
  // --- Chargement GLB ---
292
  app.assets.loadFromUrl(GLB_URL, "container", (err, asset) => {
293
  if (err) {
 
300
  receiveShadows: false
301
  });
302
  modelRoot.addChild(instance);
 
 
303
  modelRoot.setLocalScale(0.2, 0.2, 0.2);
304
 
305
  modelLoaded = true;
 
312
  return;
313
  }
314
 
315
+ // --- Démarrage AR (DOM Overlay pour slider) ---
316
  const activateAR = () => {
317
  if (!app.xr.isAvailable(pc.XRTYPE_AR)) {
318
  message("AR immersive indisponible sur cet appareil.");
319
  return;
320
  }
321
  camera.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, {
322
+ domOverlay: { root: document.body }, // permet d’utiliser le slider en AR
323
+ requiredFeatures: ["hit-test", "dom-overlay"],
324
+ optionalFeatures: ["plane-detection"],
 
325
  callback: (err) => {
326
  if (err) message(`Échec du démarrage AR : ${err.message}`);
327
  }
 
360
  if (modelLoaded && !placedOnce) {
361
  modelRoot.enabled = true;
362
  modelRoot.setPosition(pos);
363
+ // Initialise l'orientation Y à celle du plan détecté
364
  const euler = new pc.Vec3();
365
  rot.getEulerAngles(euler);
366
+ rotationYDeg = norm360(euler.y);
367
+ applyRotationY(rotationYDeg);
368
  placedOnce = true;
369
+ message("Objet placé. Déplacez-le en glissant, tournez-le avec le slider ");
370
  }
371
  });
372
  }
 
374
  });
375
 
376
  // =========================
377
+ // --- Déplacement (drag) via input XR
378
  // =========================
379
+ let isDragging = false;
 
 
 
 
 
380
  const activeTransientSources = new Set();
381
  function cancelAllTransientHitTests() {
382
+ activeTransientSources.forEach(src => { try { src.remove && src.remove(); } catch(_) {} });
 
 
383
  activeTransientSources.clear();
384
  isDragging = false;
385
  }
386
 
 
387
  app.xr.input.on("add", (inputSource) => {
388
  inputSource.on("selectstart", () => {
389
+ if (!placedOnce || !modelLoaded) return;
390
  inputSource.hitTestStart({
391
  entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
392
  callback: (err, transientSource) => {
 
395
  activeTransientSources.add(transientSource);
396
 
397
  transientSource.on("result", (pos /*, rot */) => {
398
+ if (!isDragging) return;
399
  modelRoot.setPosition(pos);
400
  });
401
 
 
413
  });
414
 
415
  // =========================
416
+ // --- Slider rotation (UI)
417
  // =========================
418
+ // Désactivé tant que l’objet n’est pas placé
419
+ rotYInput.disabled = true;
420
+
421
+ rotYInput.addEventListener("input", (e) => {
422
+ if (!modelRoot.enabled) return;
423
+ const deg = parseFloat(e.target.value || "0");
424
+ applyRotationY(deg);
425
+ }, { passive: true });
426
+
427
+ // Active le slider après le 1er placement
428
+ const enableSliderIfReady = () => {
429
+ rotYInput.disabled = !(modelLoaded && placedOnce);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
  };
431
+ const observer = new MutationObserver(enableSliderIfReady);
432
+ observer.observe(uiPanel, { attributes: true, childList: true, subtree: true });
433
 
434
+ // Assure l’état après placement
435
+ const checkInterval = setInterval(() => {
436
+ enableSliderIfReady();
437
+ if (modelLoaded && placedOnce) clearInterval(checkInterval);
438
+ }, 200);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
439
 
440
  // =========================
441
+ // Souris (rotation via slider uniquement → on ne capture plus le clic droit)
442
+ // On garde seulement le drag (clic gauche) comme fallback desktop
443
  // =========================
444
  app.mouse.on("mousedown", (e) => {
445
  if (!app.xr.active || !placedOnce) return;
446
+ if (e.button === 0) {
447
+ isDragging = true;
 
 
 
 
448
  }
449
  });
450
 
451
  app.mouse.on("mousemove", (e) => {
452
  if (!app.xr.active || !placedOnce) return;
453
+ if (isDragging && reticle.enabled) {
454
+ const pos = reticle.getPosition();
455
+ modelRoot.setPosition(pos);
 
 
 
 
 
 
 
 
456
  }
457
  });
458
 
459
  app.mouse.on("mouseup", () => {
460
  isDragging = false;
 
461
  });
462
 
463
  window.addEventListener("contextmenu", (e) => e.preventDefault());
 
472
  message("Session AR terminée.");
473
  reticle.enabled = false;
474
  isDragging = false;
 
 
 
 
475
  cancelAllTransientHitTests();
 
 
 
 
 
 
476
  });
477
 
478
  app.xr.on(`available:${pc.XRTYPE_AR}`, (available) => {