MikaFil commited on
Commit
237e5e4
·
verified ·
1 Parent(s): 8a23f53

Update viewer_ar.js

Browse files
Files changed (1) hide show
  1. viewer_ar.js +44 -38
viewer_ar.js CHANGED
@@ -1,13 +1,12 @@
1
  /* script_ar.js — AR PlayCanvas + GLB HuggingFace
2
- - Version PC verrouillée
3
- - WebXR AR Hit Test (HORIZONTAL UNIQUEMENT), placement auto, drag
4
- - Rotation Y via SLIDER custom (knob centré, rail plein, 0→360 sans wrap)
5
  */
6
 
7
  (() => {
8
  const GLB_URL = "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/AR/tests/fraisier.glb";
9
 
10
- // ===== PlayCanvas chargé en version fixe =====
11
  const PC_VERSION = "2.11.7";
12
  const PC_URLS = {
13
  esm: [`https://cdn.jsdelivr.net/npm/playcanvas@${PC_VERSION}/build/playcanvas.min.mjs`],
@@ -22,7 +21,6 @@
22
  try{ return esmFirst ? await tryESM() : await tryUMD(); }catch{ return esmFirst ? await tryUMD() : await tryESM(); }
23
  }
24
 
25
- // ===== UI / Overlay =====
26
  const css = `
27
  .pc-ar-msg{position:fixed;left:50%;transform:translateX(-50%);bottom:16px;z-index:2;padding:10px 14px;background:rgba(0,0,0,.65);color:#fff;border-radius:12px;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;font-size:14px;line-height:1.3;text-align:center;max-width:min(90vw,640px);box-shadow:0 6px 20px rgba(0,0,0,.25);backdrop-filter:blur(4px);pointer-events:none}
28
  #xr-overlay-root{position:fixed;inset:0;z-index:9999;pointer-events:none}
@@ -31,16 +29,16 @@
31
 
32
  .ar-ui .label{font-size:12px;text-align:center;opacity:.95}
33
 
34
- /* Grande zone tactile */
35
- .rotY-wrap{position:relative;width:48px;height:200px;display:flex;align-items:center;justify-content:center;touch-action:none;overflow:visible}
36
 
37
  /* Rail PLEIN (de haut en bas), centré */
38
  .rotY-rail{position:absolute;left:50%;transform:translateX(-50%);width:4px;height:100%;background:rgba(255,255,255,.35);border-radius:2px}
39
 
40
  /* Knob custom bien centré */
41
- .rotY-knob{position:absolute;left:50%;width:22px;height:22px;border-radius:50%;background:#fff;box-shadow:0 2px 8px rgba(0,0,0,.35);transform:translate(-50%,-50%);top:50%;will-change:top}
42
 
43
- /* Input range caché (on s'en sert seulement pour valeur/keyboard) */
44
  .ar-ui input[type="range"].rotY{position:absolute;opacity:0;pointer-events:none;width:0;height:0}
45
  .ar-ui .val{font-size:12px;opacity:.95}
46
  `;
@@ -68,10 +66,8 @@
68
  return p;
69
  }
70
 
71
- // ===== Boot =====
72
  (async()=>{ try{ await loadPlayCanvasRobust({esmFirst:true,loadTimeoutMs:15000}); }catch(e){ console.error("Chargement PlayCanvas échoué ->",e); message("Impossible de charger PlayCanvas (réseau/CDN). Réessaie plus tard."); return; } initARApp(); })();
73
 
74
- // ===== App =====
75
  function initARApp(){
76
  const pc = window.pc;
77
  const canvas = ensureCanvas();
@@ -112,14 +108,14 @@
112
  // Euler de base (évite les inversions)
113
  let baseEulerX=0, baseEulerZ=0;
114
 
115
- // Rotation Y (clamp 0..360)
116
  let rotationYDeg = 0;
117
  const clamp360 = d => Math.max(0, Math.min(360, d));
118
 
119
- // met à jour modèle + UI (knob plein rail)
120
  function updateKnobFromY(yDeg){
121
- const t = yDeg / 360; // 0 en haut () 1 en bas (360°)
122
- rotKnob.style.top = `${t*100}%`; // knob va de haut en bas
123
  rotYInput.value = String(Math.round(yDeg));
124
  rotYVal.textContent = `${Math.round(yDeg)}°`;
125
  }
@@ -144,28 +140,42 @@
144
  message("Modèle chargé. Touchez l’écran pour démarrer l’AR.");
145
  });
146
 
147
- // WebXR dispo ?
148
  if (!app.xr.supported){ message("WebXR n’est pas supporté sur cet appareil."); return; }
149
 
150
- // ——— UI: empêcher les événements d'aller à la scène
151
  let uiInteracting=false, draggingWrap=false;
152
  const stop = e => { e.stopPropagation(); e.preventDefault(); };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  ["pointerdown","pointermove","pointerup","pointercancel","touchstart","touchmove","touchend","touchcancel","mousedown","mousemove","mouseup","wheel","click"].forEach(evt=>{
154
  ui.addEventListener(evt, stop, {passive:false});
155
  rotYInput.addEventListener(evt, stop, {passive:false});
156
  rotKnob.addEventListener(evt, stop, {passive:false});
157
- rotWrap.addEventListener(evt, (evt==="pointerdown"||evt==="touchstart"||evt==="mousedown")?(()=>{}):stop, {passive:false});
158
  });
159
 
160
- // Drag custom sur le wrapper vertical
161
  function valueFromWrapEvent(ev){
162
  const rect = rotWrap.getBoundingClientRect();
163
  const clientY = (ev.touches && ev.touches[0]) ? ev.touches[0].clientY : ev.clientY;
164
- const ratio = (clientY - rect.top) / rect.height; // 0 en haut → 1 en bas
165
  const t = Math.max(0, Math.min(1, ratio));
166
- return t * 360; // 0..360
167
  }
168
- function beginWrapDrag(e){ uiInteracting=true; draggingWrap=true; stop(e); rotWrap.setPointerCapture?.(e.pointerId||1); applyRotationY(valueFromWrapEvent(e)); }
169
  function moveWrapDrag(e){ if(!draggingWrap) return; stop(e); applyRotationY(valueFromWrapEvent(e)); }
170
  function endWrapDrag(e){ if(!draggingWrap) return; stop(e); draggingWrap=false; uiInteracting=false; try{ rotWrap.releasePointerCapture?.(e.pointerId||1);}catch{} }
171
 
@@ -178,7 +188,7 @@
178
  rotWrap.addEventListener("touchend", endWrapDrag, {passive:false});
179
  rotWrap.addEventListener("touchcancel", endWrapDrag, {passive:false});
180
 
181
- // ——— Démarrage AR
182
  const activateAR = () => {
183
  if (!app.xr.isAvailable(pc.XRTYPE_AR)){ message("AR immersive indisponible sur cet appareil."); return; }
184
  app.xr.domOverlay.root = document.getElementById("xr-overlay-root");
@@ -195,25 +205,21 @@
195
  app.keyboard.on("keydown", (evt)=>{ if (evt.key===pc.KEY_ESCAPE && app.xr.active) app.xr.end(); });
196
 
197
  // ====== Filtre HORIZONTAL uniquement ======
198
- const V_UP = new pc.Vec3(0,1,0);
199
  const TMP_IN = new pc.Vec3(0,1,0);
200
  const TMP_OUT = new pc.Vec3();
201
-
202
- function isHorizontalUpFacing(rot, minDot = 0.75){ // ~> <= ~41°
203
- rot.transformVector(TMP_IN, TMP_OUT); // normale du plan (Y local → monde)
204
- // on veut une normale orientée vers +Y (sol/table), pas murs/plafond
205
  return TMP_OUT.y >= minDot;
206
  }
207
 
208
- // --- Hit Test global (réticule + 1er placement)
209
  app.xr.hitTest.on("available", () => {
210
  app.xr.hitTest.start({
211
  entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
212
  callback: (err, hitSource) => {
213
  if (err){ message("Le AR hit test n’a pas pu démarrer."); return; }
214
  hitSource.on("result", (pos, rot) => {
215
- // filtre: uniquement horizontaux up-facing
216
- if (!isHorizontalUpFacing(rot)) return;
217
 
218
  reticle.enabled = true;
219
  reticle.setPosition(pos);
@@ -223,9 +229,10 @@
223
  modelRoot.enabled = true;
224
  modelRoot.setPosition(pos);
225
 
226
- // Yaw initiale normalisée (sans wrap) + maj UI
227
  const e = new pc.Vec3(); rot.getEulerAngles(e);
228
- applyRotationY(clamp360((e.y % 360 + 360) % 360));
 
 
229
 
230
  placedOnce = true;
231
  rotYInput.disabled = false;
@@ -236,7 +243,7 @@
236
  });
237
  });
238
 
239
- // --- Déplacement XR (drag) — ignore si UI
240
  let isDragging=false;
241
  app.xr.input.on("add", (inputSource) => {
242
  inputSource.on("selectstart", () => {
@@ -251,7 +258,7 @@
251
 
252
  transientSource.on("result", (pos, rot) => {
253
  if (!isDragging) return;
254
- if (!isHorizontalUpFacing(rot)) return; // <- rejet vertical
255
  modelRoot.setPosition(pos);
256
  });
257
 
@@ -262,14 +269,14 @@
262
  inputSource.on("selectend", () => { isDragging = false; });
263
  });
264
 
265
- // --- Desktop : rotation souris conservée
266
  let rotateMode=false, lastMouseX=0; const ROTATE_SENSITIVITY=0.25;
267
  app.mouse.on("mousedown",(e)=>{ if(!app.xr.active||!placedOnce||uiInteracting) return; if(e.button===0 && !e.shiftKey) isDragging=true; else if(e.button===2 || (e.button===0 && e.shiftKey)){ rotateMode=true; lastMouseX=e.x; }});
268
  app.mouse.on("mousemove",(e)=>{ if(!app.xr.active||!placedOnce||uiInteracting) return; if(isDragging){ if(reticle.enabled) modelRoot.setPosition(reticle.getPosition()); } else if(rotateMode && modelRoot.enabled){ const dx=e.x-lastMouseX; lastMouseX=e.x; applyRotationY(rotationYDeg + dx*ROTATE_SENSITIVITY); }});
269
  app.mouse.on("mouseup",()=>{ isDragging=false; rotateMode=false; });
270
  window.addEventListener("contextmenu",(e)=>e.preventDefault());
271
 
272
- // --- Slider (si changé via clavier)
273
  rotYInput.disabled = true;
274
  rotYInput.addEventListener("input",(e)=>{ if(!modelRoot.enabled) return; applyRotationY(parseFloat(e.target.value||"0")); },{passive:true});
275
 
@@ -278,7 +285,6 @@
278
  app.xr.on("end",()=>{ message("Session AR terminée."); reticle.enabled=false; isDragging=false; rotateMode=false; rotYInput.disabled=true; });
279
  app.xr.on(`available:${pc.XRTYPE_AR}`,(a)=>{ if(!a) message("AR immersive indisponible."); else if(!app.xr.hitTest.supported) message("AR Hit Test non supporté."); else message(modelLoaded?"Touchez l’écran pour démarrer l’AR.":"Chargement du modèle…"); });
280
 
281
- // Message initial
282
  if(!app.xr.isAvailable(pc.XRTYPE_AR)) message("AR immersive indisponible.");
283
  else if(!app.xr.hitTest.supported) message("AR Hit Test non supporté.");
284
  else message("Chargement du modèle…");
 
1
  /* script_ar.js — AR PlayCanvas + GLB HuggingFace
2
+ - Hit-test AR (horizontaux uniquement) + placement + drag
3
+ - Slider custom (360° en haut → 0° en bas), knob centré, rail plein
4
+ - Bloque toute interaction scène quand on tape la box grise du slider
5
  */
6
 
7
  (() => {
8
  const GLB_URL = "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/AR/tests/fraisier.glb";
9
 
 
10
  const PC_VERSION = "2.11.7";
11
  const PC_URLS = {
12
  esm: [`https://cdn.jsdelivr.net/npm/playcanvas@${PC_VERSION}/build/playcanvas.min.mjs`],
 
21
  try{ return esmFirst ? await tryESM() : await tryUMD(); }catch{ return esmFirst ? await tryUMD() : await tryESM(); }
22
  }
23
 
 
24
  const css = `
25
  .pc-ar-msg{position:fixed;left:50%;transform:translateX(-50%);bottom:16px;z-index:2;padding:10px 14px;background:rgba(0,0,0,.65);color:#fff;border-radius:12px;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;font-size:14px;line-height:1.3;text-align:center;max-width:min(90vw,640px);box-shadow:0 6px 20px rgba(0,0,0,.25);backdrop-filter:blur(4px);pointer-events:none}
26
  #xr-overlay-root{position:fixed;inset:0;z-index:9999;pointer-events:none}
 
29
 
30
  .ar-ui .label{font-size:12px;text-align:center;opacity:.95}
31
 
32
+ /* Grande zone tactile (box grise) */
33
+ .rotY-wrap{position:relative;width:48px;height:200px;display:flex;align-items:center;justify-content:center;touch-action:none;overflow:visible;pointer-events:auto}
34
 
35
  /* Rail PLEIN (de haut en bas), centré */
36
  .rotY-rail{position:absolute;left:50%;transform:translateX(-50%);width:4px;height:100%;background:rgba(255,255,255,.35);border-radius:2px}
37
 
38
  /* Knob custom bien centré */
39
+ .rotY-knob{position:absolute;left:50%;width:22px;height:22px;border-radius:50%;background:#fff;box-shadow:0 2px 8px rgba(0,0,0,.35);transform:translate(-50%,-50%);top:50%;will-change:top;touch-action:none}
40
 
41
+ /* Input range caché (pour accessibilité/valeur) */
42
  .ar-ui input[type="range"].rotY{position:absolute;opacity:0;pointer-events:none;width:0;height:0}
43
  .ar-ui .val{font-size:12px;opacity:.95}
44
  `;
 
66
  return p;
67
  }
68
 
 
69
  (async()=>{ try{ await loadPlayCanvasRobust({esmFirst:true,loadTimeoutMs:15000}); }catch(e){ console.error("Chargement PlayCanvas échoué ->",e); message("Impossible de charger PlayCanvas (réseau/CDN). Réessaie plus tard."); return; } initARApp(); })();
70
 
 
71
  function initARApp(){
72
  const pc = window.pc;
73
  const canvas = ensureCanvas();
 
108
  // Euler de base (évite les inversions)
109
  let baseEulerX=0, baseEulerZ=0;
110
 
111
+ // Rotation Y (clamp 0..360) — 360 en haut, 0 en bas
112
  let rotationYDeg = 0;
113
  const clamp360 = d => Math.max(0, Math.min(360, d));
114
 
115
+ // Met à jour modèle + UI ( inversion : 360 en haut)
116
  function updateKnobFromY(yDeg){
117
+ const t = 1 - (yDeg / 360); // y=360 -> t=0 (haut), y=0 -> t=1 (bas)
118
+ rotKnob.style.top = `${t*100}%`;
119
  rotYInput.value = String(Math.round(yDeg));
120
  rotYVal.textContent = `${Math.round(yDeg)}°`;
121
  }
 
140
  message("Modèle chargé. Touchez l’écran pour démarrer l’AR.");
141
  });
142
 
 
143
  if (!app.xr.supported){ message("WebXR n’est pas supporté sur cet appareil."); return; }
144
 
145
+ // ===== Empêcher les “fuites” : activer uiInteracting en CAPTURE sur la box grise =====
146
  let uiInteracting=false, draggingWrap=false;
147
  const stop = e => { e.stopPropagation(); e.preventDefault(); };
148
+
149
+ // Capture globale : si on clique/touche la box grise, on active uiInteracting IMMÉDIATEMENT
150
+ const isInsideWrap = (target) => rotWrap.contains(target);
151
+ const onGlobalPointerDownCapture = (e) => { if (isInsideWrap(e.target)) uiInteracting = true; };
152
+ const onGlobalTouchStartCapture = (e) => { if (isInsideWrap(e.target)) uiInteracting = true; };
153
+
154
+ document.addEventListener("pointerdown", onGlobalPointerDownCapture, true);
155
+ document.addEventListener("touchstart", onGlobalTouchStartCapture, true);
156
+
157
+ // Et on nettoie à la fin du geste
158
+ const onGlobalPointerUpCapture = () => { if (!draggingWrap) uiInteracting = false; };
159
+ const onGlobalTouchEndCapture = () => { if (!draggingWrap) uiInteracting = false; };
160
+ document.addEventListener("pointerup", onGlobalPointerUpCapture, true);
161
+ document.addEventListener("touchend", onGlobalTouchEndCapture, true);
162
+
163
+ // Bloque la propagation normale depuis le panneau / knob / input
164
  ["pointerdown","pointermove","pointerup","pointercancel","touchstart","touchmove","touchend","touchcancel","mousedown","mousemove","mouseup","wheel","click"].forEach(evt=>{
165
  ui.addEventListener(evt, stop, {passive:false});
166
  rotYInput.addEventListener(evt, stop, {passive:false});
167
  rotKnob.addEventListener(evt, stop, {passive:false});
 
168
  });
169
 
170
+ // Drag custom sur le wrapper vertical — 360 en haut, 0 en bas
171
  function valueFromWrapEvent(ev){
172
  const rect = rotWrap.getBoundingClientRect();
173
  const clientY = (ev.touches && ev.touches[0]) ? ev.touches[0].clientY : ev.clientY;
174
+ const ratio = (clientY - rect.top) / rect.height; // 0 en haut → 1 en bas
175
  const t = Math.max(0, Math.min(1, ratio));
176
+ return (1 - t) * 360; // inversion : haut=360, bas=0
177
  }
178
+ function beginWrapDrag(e){ draggingWrap=true; stop(e); rotWrap.setPointerCapture?.(e.pointerId||1); applyRotationY(valueFromWrapEvent(e)); }
179
  function moveWrapDrag(e){ if(!draggingWrap) return; stop(e); applyRotationY(valueFromWrapEvent(e)); }
180
  function endWrapDrag(e){ if(!draggingWrap) return; stop(e); draggingWrap=false; uiInteracting=false; try{ rotWrap.releasePointerCapture?.(e.pointerId||1);}catch{} }
181
 
 
188
  rotWrap.addEventListener("touchend", endWrapDrag, {passive:false});
189
  rotWrap.addEventListener("touchcancel", endWrapDrag, {passive:false});
190
 
191
+ // --- Démarrage AR
192
  const activateAR = () => {
193
  if (!app.xr.isAvailable(pc.XRTYPE_AR)){ message("AR immersive indisponible sur cet appareil."); return; }
194
  app.xr.domOverlay.root = document.getElementById("xr-overlay-root");
 
205
  app.keyboard.on("keydown", (evt)=>{ if (evt.key===pc.KEY_ESCAPE && app.xr.active) app.xr.end(); });
206
 
207
  // ====== Filtre HORIZONTAL uniquement ======
 
208
  const TMP_IN = new pc.Vec3(0,1,0);
209
  const TMP_OUT = new pc.Vec3();
210
+ function isHorizontalUpFacing(rot, minDot = 0.75){
211
+ rot.transformVector(TMP_IN, TMP_OUT);
 
 
212
  return TMP_OUT.y >= minDot;
213
  }
214
 
215
+ // Hit Test global
216
  app.xr.hitTest.on("available", () => {
217
  app.xr.hitTest.start({
218
  entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
219
  callback: (err, hitSource) => {
220
  if (err){ message("Le AR hit test n’a pas pu démarrer."); return; }
221
  hitSource.on("result", (pos, rot) => {
222
+ if (!isHorizontalUpFacing(rot)) return; // horizontaux uniquement
 
223
 
224
  reticle.enabled = true;
225
  reticle.setPosition(pos);
 
229
  modelRoot.enabled = true;
230
  modelRoot.setPosition(pos);
231
 
 
232
  const e = new pc.Vec3(); rot.getEulerAngles(e);
233
+ // normalise 0..360 (puis UI inversée affichera 360 en haut)
234
+ const y0 = ((e.y % 360) + 360) % 360;
235
+ applyRotationY(y0);
236
 
237
  placedOnce = true;
238
  rotYInput.disabled = false;
 
243
  });
244
  });
245
 
246
+ // Déplacement XR (drag) — ignoré si on interagit avec la box grise
247
  let isDragging=false;
248
  app.xr.input.on("add", (inputSource) => {
249
  inputSource.on("selectstart", () => {
 
258
 
259
  transientSource.on("result", (pos, rot) => {
260
  if (!isDragging) return;
261
+ if (!isHorizontalUpFacing(rot)) return;
262
  modelRoot.setPosition(pos);
263
  });
264
 
 
269
  inputSource.on("selectend", () => { isDragging = false; });
270
  });
271
 
272
+ // Desktop : rotation souris conservée
273
  let rotateMode=false, lastMouseX=0; const ROTATE_SENSITIVITY=0.25;
274
  app.mouse.on("mousedown",(e)=>{ if(!app.xr.active||!placedOnce||uiInteracting) return; if(e.button===0 && !e.shiftKey) isDragging=true; else if(e.button===2 || (e.button===0 && e.shiftKey)){ rotateMode=true; lastMouseX=e.x; }});
275
  app.mouse.on("mousemove",(e)=>{ if(!app.xr.active||!placedOnce||uiInteracting) return; if(isDragging){ if(reticle.enabled) modelRoot.setPosition(reticle.getPosition()); } else if(rotateMode && modelRoot.enabled){ const dx=e.x-lastMouseX; lastMouseX=e.x; applyRotationY(rotationYDeg + dx*ROTATE_SENSITIVITY); }});
276
  app.mouse.on("mouseup",()=>{ isDragging=false; rotateMode=false; });
277
  window.addEventListener("contextmenu",(e)=>e.preventDefault());
278
 
279
+ // Slider (au cas via clavier)
280
  rotYInput.disabled = true;
281
  rotYInput.addEventListener("input",(e)=>{ if(!modelRoot.enabled) return; applyRotationY(parseFloat(e.target.value||"0")); },{passive:true});
282
 
 
285
  app.xr.on("end",()=>{ message("Session AR terminée."); reticle.enabled=false; isDragging=false; rotateMode=false; rotYInput.disabled=true; });
286
  app.xr.on(`available:${pc.XRTYPE_AR}`,(a)=>{ if(!a) message("AR immersive indisponible."); else if(!app.xr.hitTest.supported) message("AR Hit Test non supporté."); else message(modelLoaded?"Touchez l’écran pour démarrer l’AR.":"Chargement du modèle…"); });
287
 
 
288
  if(!app.xr.isAvailable(pc.XRTYPE_AR)) message("AR immersive indisponible.");
289
  else if(!app.xr.hitTest.supported) message("AR Hit Test non supporté.");
290
  else message("Chargement du modèle…");