MikaFil commited on
Commit
a017d0c
·
verified ·
1 Parent(s): f339265

Update viewer_ar.js

Browse files
Files changed (1) hide show
  1. viewer_ar.js +88 -46
viewer_ar.js CHANGED
@@ -1,7 +1,7 @@
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
 
@@ -32,10 +32,7 @@
32
  try {
33
  const mod = await Promise.race([import(/* @vite-ignore */ url), timeout(loadTimeoutMs)]);
34
  const namespace = mod?.pc || mod?.default || mod;
35
- if (namespace?.Application) {
36
- if (!window.pc) window.pc = namespace;
37
- return window.pc;
38
- }
39
  } catch (_) {}
40
  }
41
  throw new Error("ESM failed");
@@ -47,10 +44,8 @@
47
  await Promise.race([
48
  new Promise((res, rej) => {
49
  const s = document.createElement("script");
50
- s.src = url;
51
- s.async = true;
52
- s.onload = () => res();
53
- s.onerror = () => rej(new Error("script error"));
54
  document.head.appendChild(s);
55
  }),
56
  timeout(loadTimeoutMs)
@@ -85,23 +80,44 @@
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; align-items: center; gap: 8px;
89
  box-shadow: 0 6px 18px rgba(0,0,0,.25); backdrop-filter: blur(4px);
90
- touch-action: none; /* important pour éviter le scroll/zoom natif */
91
  }
92
  .ar-ui .label { font-size: 12px; text-align: center; opacity: 0.9; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  .ar-ui input[type="range"].rotY {
94
- -webkit-appearance: none; width: 220px; height: 28px; transform: rotate(-90deg);
95
- outline: none; background: transparent; touch-action: none; /* bloque gestures tactiles */
 
 
 
 
96
  }
97
  .ar-ui input[type="range"].rotY::-webkit-slider-thumb {
98
- -webkit-appearance: none; appearance: none; width: 20px; height: 20px; border-radius: 50%;
99
  background: #fff; border: none; box-shadow: 0 2px 8px rgba(0,0,0,.35);
100
  }
101
  .ar-ui input[type="range"].rotY::-webkit-slider-runnable-track {
102
  height: 6px; background: rgba(255,255,255,.6); border-radius: 4px;
103
  }
104
- .ar-ui .val { font-size: 11px; opacity: 0.85; }
 
105
  `;
106
  const styleTag = document.createElement("style");
107
  styleTag.textContent = css;
@@ -148,7 +164,10 @@
148
  panel.className = "ar-ui";
149
  panel.innerHTML = `
150
  <div class="label">Rotation</div>
151
- <input class="rotY" id="ar-rotY" type="range" min="0" max="360" step="1" value="0" />
 
 
 
152
  <div class="val" id="ar-rotY-val">0°</div>
153
  `;
154
  overlayRoot.appendChild(panel);
@@ -176,8 +195,10 @@
176
  const pc = window.pc;
177
  const canvas = ensureCanvas();
178
  const uiPanel = ensureSliderUI();
 
179
  const rotYInput = uiPanel.querySelector("#ar-rotY");
180
- const rotYVal = uiPanel.querySelector("#ar-rotY-val");
 
181
 
182
  window.focus();
183
 
@@ -261,29 +282,55 @@
261
  // --- Vérifs WebXR ---
262
  if (!app.xr.supported) { message("WebXR n’est pas supporté sur cet appareil."); return; }
263
 
264
- // ====== NOUVEAU : gestion anti-fuite d'événements UI ======
265
  let uiInteracting = false;
266
- const startUIHold = (e) => { uiInteracting = true; e.stopPropagation(); e.preventDefault(); };
267
- const endUIHold = (e) => { uiInteracting = false; e.stopPropagation(); e.preventDefault(); };
268
- const swallow = (e) => { e.stopPropagation(); e.preventDefault(); };
269
-
270
- // Piégeage complet sur le panneau ET le slider
271
- ["pointerdown","pointermove","pointerup","pointercancel",
272
- "touchstart","touchmove","touchend","touchcancel",
273
- "mousedown","mousemove","mouseup","wheel","click"].forEach(evt => {
274
- uiPanel.addEventListener(evt, evt === "pointerdown" || evt === "touchstart" || evt === "mousedown" ? startUIHold :
275
- (evt === "pointerup" || evt === "pointercancel" || evt === "touchend" || evt === "touchcancel" || evt === "mouseup" ? endUIHold : swallow),
276
- { passive: false });
277
- rotYInput.addEventListener(evt, evt === "pointerdown" || evt === "touchstart" || evt === "mousedown" ? startUIHold :
278
- (evt === "pointerup" || evt === "pointercancel" || evt === "touchend" || evt === "touchcancel" || evt === "mouseup" ? endUIHold : swallow),
279
- { passive: false });
280
  });
281
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  // --- Démarrage AR (CameraComponent.startXr + DOM Overlay) ---
283
  const activateAR = () => {
284
  if (!app.xr.isAvailable(pc.XRTYPE_AR)) { message("AR immersive indisponible sur cet appareil."); return; }
285
-
286
- // DOM overlay root (notre conteneur avec slider + messages)
287
  app.xr.domOverlay.root = document.getElementById("xr-overlay-root");
288
 
289
  camera.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, {
@@ -295,8 +342,8 @@
295
  });
296
  };
297
 
298
- // Premier tap / clic → démarrer AR (ignorer si on tape sur l'UI)
299
- app.mouse.on("mousedown", (e) => { if (!app.xr.active && !uiInteracting) activateAR(); });
300
  if (app.touch) {
301
  app.touch.on("touchend", (evt) => {
302
  if (!app.xr.active && !uiInteracting) activateAR();
@@ -322,7 +369,8 @@
322
  modelRoot.enabled = true;
323
  modelRoot.setPosition(pos);
324
  const euler = new pc.Vec3(); rot.getEulerAngles(euler);
325
- rotationYDeg = norm360(euler.y); applyRotationY(rotationYDeg);
 
326
  placedOnce = true; rotYInput.disabled = false;
327
  message("Objet placé. Glissez pour déplacer, tournez-le avec le slider →");
328
  }
@@ -340,7 +388,7 @@
340
  // Déplacement (input XR : maintien/drag) — IGNORE si UI en cours
341
  app.xr.input.on("add", (inputSource) => {
342
  inputSource.on("selectstart", () => {
343
- if (uiInteracting) return; // <<< bloque le drag si doigt sur le slider
344
  if (!placedOnce || !modelLoaded) return;
345
 
346
  inputSource.hitTestStart({
@@ -348,12 +396,7 @@
348
  callback: (err, transientSource) => {
349
  if (err) return;
350
  isDragging = true;
351
-
352
- transientSource.on("result", (pos) => {
353
- if (!isDragging) return;
354
- modelRoot.setPosition(pos);
355
- });
356
-
357
  transientSource.once("remove", () => { isDragging = false; });
358
  }
359
  });
@@ -385,8 +428,7 @@
385
  rotYInput.disabled = true; // activé au 1er placement
386
  rotYInput.addEventListener("input", (e) => {
387
  if (!modelRoot.enabled) return;
388
- const deg = parseFloat(e.target.value || "0");
389
- applyRotationY(deg);
390
  }, { passive: true });
391
 
392
  // --- Événements AR globaux ---
 
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) avec large zone tactile
5
  - Aucune dépendance externe autre que PlayCanvas et le GLB
6
  */
7
 
 
32
  try {
33
  const mod = await Promise.race([import(/* @vite-ignore */ url), timeout(loadTimeoutMs)]);
34
  const namespace = mod?.pc || mod?.default || mod;
35
+ if (namespace?.Application) { if (!window.pc) window.pc = namespace; return window.pc; }
 
 
 
36
  } catch (_) {}
37
  }
38
  throw new Error("ESM failed");
 
44
  await Promise.race([
45
  new Promise((res, rej) => {
46
  const s = document.createElement("script");
47
+ s.src = url; s.async = true;
48
+ s.onload = () => res(); s.onerror = () => rej(new Error("script error"));
 
 
49
  document.head.appendChild(s);
50
  }),
51
  timeout(loadTimeoutMs)
 
80
  position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
81
  background: rgba(0,0,0,0.55); color: #fff; padding: 12px 10px; border-radius: 12px;
82
  font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
83
+ pointer-events: auto; width: 72px; display: flex; flex-direction: column; align-items: center; gap: 10px;
84
  box-shadow: 0 6px 18px rgba(0,0,0,.25); backdrop-filter: blur(4px);
85
+ touch-action: none;
86
  }
87
  .ar-ui .label { font-size: 12px; text-align: center; opacity: 0.9; }
88
+
89
+ /* GRAND WRAPPER TACTILE (hitbox) */
90
+ .rotY-wrap {
91
+ position: relative;
92
+ width: 60px; /* largeur tactile confortable */
93
+ height: 260px; /* hauteur tactile confortable */
94
+ display: flex; align-items: center; justify-content: center;
95
+ touch-action: none;
96
+ }
97
+ /* Rail visuel (optionnel) */
98
+ .rotY-rail {
99
+ position: absolute; width: 6px; height: 80%;
100
+ background: rgba(255,255,255,.35); border-radius: 3px;
101
+ }
102
+
103
+ /* Slider réel, tourné, mais on ne dépend PAS de son hitbox */
104
  .ar-ui input[type="range"].rotY {
105
+ -webkit-appearance: none;
106
+ position: relative;
107
+ width: 220px; height: 32px;
108
+ transform: rotate(-90deg);
109
+ outline: none; background: transparent; touch-action: none;
110
+ pointer-events: none; /* <- on lit sa valeur mais on ne compte pas sur sa hitbox */
111
  }
112
  .ar-ui input[type="range"].rotY::-webkit-slider-thumb {
113
+ -webkit-appearance: none; appearance: none; width: 26px; height: 26px; border-radius: 50%;
114
  background: #fff; border: none; box-shadow: 0 2px 8px rgba(0,0,0,.35);
115
  }
116
  .ar-ui input[type="range"].rotY::-webkit-slider-runnable-track {
117
  height: 6px; background: rgba(255,255,255,.6); border-radius: 4px;
118
  }
119
+
120
+ .ar-ui .val { font-size: 12px; opacity: 0.9; }
121
  `;
122
  const styleTag = document.createElement("style");
123
  styleTag.textContent = css;
 
164
  panel.className = "ar-ui";
165
  panel.innerHTML = `
166
  <div class="label">Rotation</div>
167
+ <div class="rotY-wrap" id="ar-rotY-wrap">
168
+ <div class="rotY-rail"></div>
169
+ <input class="rotY" id="ar-rotY" type="range" min="0" max="360" step="1" value="0" />
170
+ </div>
171
  <div class="val" id="ar-rotY-val">0°</div>
172
  `;
173
  overlayRoot.appendChild(panel);
 
195
  const pc = window.pc;
196
  const canvas = ensureCanvas();
197
  const uiPanel = ensureSliderUI();
198
+
199
  const rotYInput = uiPanel.querySelector("#ar-rotY");
200
+ const rotYVal = uiPanel.querySelector("#ar-rotY-val");
201
+ const rotWrap = uiPanel.querySelector("#ar-rotY-wrap");
202
 
203
  window.focus();
204
 
 
282
  // --- Vérifs WebXR ---
283
  if (!app.xr.supported) { message("WebXR n’est pas supporté sur cet appareil."); return; }
284
 
285
+ // ====== Anti-fuite d'événements UI + grande hitbox ======
286
  let uiInteracting = false;
287
+ let draggingWrap = false;
288
+
289
+ const stop = (e) => { e.stopPropagation(); e.preventDefault(); };
290
+
291
+ // 1) Empêcher toute propagation depuis le panneau
292
+ ["pointerdown","pointermove","pointerup","pointercancel","touchstart","touchmove","touchend","touchcancel","mousedown","mousemove","mouseup","wheel","click"].forEach(evt => {
293
+ uiPanel.addEventListener(evt, stop, { passive: false });
294
+ rotYInput.addEventListener(evt, stop, { passive: false });
 
 
 
 
 
 
295
  });
296
 
297
+ // 2) Drag custom sur le GRAND WRAPPER vertical
298
+ function valueFromWrapEvent(ev) {
299
+ const rect = rotWrap.getBoundingClientRect();
300
+ const clientY = (ev.touches && ev.touches[0]) ? ev.touches[0].clientY : ev.clientY;
301
+ const ratio = 1 - ((clientY - rect.top) / rect.height); // 1 en haut, 0 en bas
302
+ const clamped = Math.max(0, Math.min(1, ratio));
303
+ return clamped * 360;
304
+ }
305
+
306
+ function beginWrapDrag(e) {
307
+ uiInteracting = true; draggingWrap = true; stop(e);
308
+ rotWrap.setPointerCapture?.(e.pointerId || 1);
309
+ applyRotationY(valueFromWrapEvent(e));
310
+ }
311
+ function moveWrapDrag(e) {
312
+ if (!draggingWrap) return; stop(e);
313
+ applyRotationY(valueFromWrapEvent(e));
314
+ }
315
+ function endWrapDrag(e) {
316
+ if (!draggingWrap) return; stop(e);
317
+ draggingWrap = false; uiInteracting = false;
318
+ try { rotWrap.releasePointerCapture?.(e.pointerId || 1); } catch(_) {}
319
+ }
320
+
321
+ rotWrap.addEventListener("pointerdown", beginWrapDrag, { passive: false });
322
+ rotWrap.addEventListener("pointermove", moveWrapDrag, { passive: false });
323
+ rotWrap.addEventListener("pointerup", endWrapDrag, { passive: false });
324
+ rotWrap.addEventListener("pointercancel", endWrapDrag, { passive: false });
325
+
326
+ rotWrap.addEventListener("touchstart", beginWrapDrag, { passive: false });
327
+ rotWrap.addEventListener("touchmove", moveWrapDrag, { passive: false });
328
+ rotWrap.addEventListener("touchend", endWrapDrag, { passive: false });
329
+ rotWrap.addEventListener("touchcancel",endWrapDrag, { passive: false });
330
+
331
  // --- Démarrage AR (CameraComponent.startXr + DOM Overlay) ---
332
  const activateAR = () => {
333
  if (!app.xr.isAvailable(pc.XRTYPE_AR)) { message("AR immersive indisponible sur cet appareil."); return; }
 
 
334
  app.xr.domOverlay.root = document.getElementById("xr-overlay-root");
335
 
336
  camera.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, {
 
342
  });
343
  };
344
 
345
+ // Premier tap / clic → démarrer AR (ignorer si UI en cours)
346
+ app.mouse.on("mousedown", () => { if (!app.xr.active && !uiInteracting) activateAR(); });
347
  if (app.touch) {
348
  app.touch.on("touchend", (evt) => {
349
  if (!app.xr.active && !uiInteracting) activateAR();
 
369
  modelRoot.enabled = true;
370
  modelRoot.setPosition(pos);
371
  const euler = new pc.Vec3(); rot.getEulerAngles(euler);
372
+ rotationYDeg = (euler.y % 360 + 360) % 360;
373
+ applyRotationY(rotationYDeg);
374
  placedOnce = true; rotYInput.disabled = false;
375
  message("Objet placé. Glissez pour déplacer, tournez-le avec le slider →");
376
  }
 
388
  // Déplacement (input XR : maintien/drag) — IGNORE si UI en cours
389
  app.xr.input.on("add", (inputSource) => {
390
  inputSource.on("selectstart", () => {
391
+ if (uiInteracting) return;
392
  if (!placedOnce || !modelLoaded) return;
393
 
394
  inputSource.hitTestStart({
 
396
  callback: (err, transientSource) => {
397
  if (err) return;
398
  isDragging = true;
399
+ transientSource.on("result", (pos) => { if (isDragging) modelRoot.setPosition(pos); });
 
 
 
 
 
400
  transientSource.once("remove", () => { isDragging = false; });
401
  }
402
  });
 
428
  rotYInput.disabled = true; // activé au 1er placement
429
  rotYInput.addEventListener("input", (e) => {
430
  if (!modelRoot.enabled) return;
431
+ applyRotationY(parseFloat(e.target.value || "0"));
 
432
  }, { passive: true });
433
 
434
  // --- Événements AR globaux ---