MikaFil commited on
Commit
7096247
·
verified ·
1 Parent(s): 28721f6

Update viewer_ar.js

Browse files
Files changed (1) hide show
  1. viewer_ar.js +98 -70
viewer_ar.js CHANGED
@@ -1,13 +1,13 @@
1
  /* script_ar.js — AR PlayCanvas + GLB HuggingFace
2
  - Hit-test AR (horizontaux uniquement) + placement + drag
3
- - Slider custom robuste (Pointer Events only) — 360° en haut, 0° en bas
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
- // ===== PlayCanvas (version verrouillée) =====
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`],
@@ -31,16 +31,16 @@
31
 
32
  .ar-ui .label{font-size:12px;text-align:center;opacity:.95}
33
 
34
- /* Box grise cliquable (grande hitbox) */
35
  .rotY-wrap{position:relative;width:48px;height:200px;display:flex;align-items:center;justify-content:center;touch-action:none;overflow:visible;pointer-events:auto}
36
 
37
- /* Rail plein, 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 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;touch-action:none}
42
 
43
- /* Input range caché (valeur/accès clavier) */
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
  `;
@@ -107,7 +107,7 @@
107
  const modelRoot = new pc.Entity("ModelRoot"); modelRoot.enabled = false; app.root.addChild(modelRoot);
108
  let modelLoaded=false, placedOnce=false;
109
 
110
- // Euler de base
111
  let baseEulerX=0, baseEulerZ=0;
112
 
113
  // Rotation Y (clamp 0..360) — 360 en haut, 0 en bas
@@ -115,7 +115,7 @@
115
  const clamp360 = d => Math.max(0, Math.min(360, d));
116
 
117
  function updateKnobFromY(yDeg){
118
- const t = 1 - (yDeg / 360); // y=360 -> haut ; y=0 -> bas
119
  rotKnob.style.top = `${t*100}%`;
120
  rotYInput.value = String(Math.round(yDeg));
121
  rotYVal.textContent = `${Math.round(yDeg)}°`;
@@ -143,64 +143,54 @@
143
 
144
  if (!app.xr.supported){ message("WebXR n’est pas supporté sur cet appareil."); return; }
145
 
146
- // ===== Slider robuste : Pointer Events only =====
147
- let uiInteracting=false, draggingWrap=false, activePointerId=null;
 
 
148
 
149
- // Marquer TOUT DE SUITE si on tape dans la box grise (phase capture)
150
- const onGlobalPointerDownCapture = (e) => {
151
- if (rotWrap.contains(e.target)) uiInteracting = true;
152
- };
153
- document.addEventListener("pointerdown", onGlobalPointerDownCapture, true);
154
-
155
- const onGlobalPointerUpCapture = () => {
156
- if (!draggingWrap) uiInteracting = false;
157
- };
158
- document.addEventListener("pointerup", onGlobalPointerUpCapture, true);
159
-
160
- // Bloque la propagation depuis le panneau/knob/input (bubbling)
161
- const stop = e => { e.stopPropagation(); e.preventDefault(); };
162
- ["pointerdown","pointermove","pointerup","pointercancel","mousedown","mousemove","mouseup","wheel","click"].forEach(evt=>{
163
- ui.addEventListener(evt, stop, {passive:false});
164
- rotYInput.addEventListener(evt, stop, {passive:false});
165
- rotKnob.addEventListener(evt, stop, {passive:false});
166
- });
167
 
168
- // Drag sur la box grise
169
- function valueFromWrapEvent(ev){
170
  const rect = rotWrap.getBoundingClientRect();
171
- const y = ev.clientY;
172
  const ratio = (y - rect.top) / rect.height; // 0 en haut -> 1 en bas
173
  const t = Math.max(0, Math.min(1, ratio));
174
- return (1 - t) * 360; // 360 en haut -> 0 en bas
175
- }
176
 
177
- function beginWrapDrag(e){
 
 
178
  uiInteracting = true;
179
  draggingWrap = true;
180
- activePointerId = e.pointerId;
181
- stop(e);
182
- try { rotWrap.setPointerCapture(e.pointerId); } catch(_) {}
183
- applyRotationY(valueFromWrapEvent(e));
184
- }
185
- function moveWrapDrag(e){
186
- if (!draggingWrap || e.pointerId !== activePointerId) return;
187
- stop(e);
188
- applyRotationY(valueFromWrapEvent(e));
189
- }
190
- function endWrapDrag(e){
191
- if (e.pointerId !== activePointerId) return;
192
- stop(e);
 
 
 
193
  draggingWrap = false;
194
- activePointerId = null;
195
  uiInteracting = false;
196
- try { rotWrap.releasePointerCapture(e.pointerId); } catch(_) {}
197
- }
 
 
 
198
 
199
- // Pointer events uniquement (fiable Android/Chrome)
200
- rotWrap.addEventListener("pointerdown", beginWrapDrag, {passive:false});
201
- rotWrap.addEventListener("pointermove", moveWrapDrag, {passive:false});
202
- rotWrap.addEventListener("pointerup", endWrapDrag, {passive:false});
203
- rotWrap.addEventListener("pointercancel",endWrapDrag, {passive:false});
204
 
205
  // --- Démarrage AR
206
  const activateAR = () => {
@@ -219,21 +209,17 @@
219
  app.keyboard.on("keydown", (evt)=>{ if (evt.key===pc.KEY_ESCAPE && app.xr.active) app.xr.end(); });
220
 
221
  // ====== Filtre HORIZONTAL uniquement ======
222
- const TMP_IN = new pc.Vec3(0,1,0);
223
- const TMP_OUT = new pc.Vec3();
224
- function isHorizontalUpFacing(rot, minDot = 0.75){
225
- rot.transformVector(TMP_IN, TMP_OUT);
226
- return TMP_OUT.y >= minDot;
227
- }
228
 
229
- // Hit Test global
230
  app.xr.hitTest.on("available", () => {
231
  app.xr.hitTest.start({
232
  entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
233
  callback: (err, hitSource) => {
234
  if (err){ message("Le AR hit test n’a pas pu démarrer."); return; }
235
  hitSource.on("result", (pos, rot) => {
236
- if (!isHorizontalUpFacing(rot)) return; // horizontaux uniquement
237
 
238
  reticle.enabled = true;
239
  reticle.setPosition(pos);
@@ -244,8 +230,7 @@
244
  modelRoot.setPosition(pos);
245
 
246
  const e = new pc.Vec3(); rot.getEulerAngles(e);
247
- const y0 = ((e.y % 360) + 360) % 360;
248
- applyRotationY(y0);
249
 
250
  placedOnce = true;
251
  rotYInput.disabled = false;
@@ -256,7 +241,50 @@
256
  });
257
  });
258
 
259
- // Déplacement XR — ignoré si on touche le slider
260
  let isDragging=false;
261
  app.xr.input.on("add", (inputSource) => {
262
- inputSource.on("selectstart", () =>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ - Tap sur la box grise capturé en phase CAPTURE => l’objet ne se déplace jamais
5
  */
6
 
7
  (() => {
8
  const GLB_URL = "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/AR/tests/fraisier.glb";
9
 
10
+ // ===== PlayCanvas version fixée =====
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`],
 
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;pointer-events:auto}
36
 
37
+ /* Rail PLEIN (haut->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;pointer-events:none}
39
 
40
+ /* Knob custom 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;touch-action:none;pointer-events:none}
42
 
43
+ /* Input range caché (pour accessibilité/valeur) */
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
  `;
 
107
  const modelRoot = new pc.Entity("ModelRoot"); modelRoot.enabled = false; app.root.addChild(modelRoot);
108
  let modelLoaded=false, placedOnce=false;
109
 
110
+ // Euler de base (évite inversions)
111
  let baseEulerX=0, baseEulerZ=0;
112
 
113
  // Rotation Y (clamp 0..360) — 360 en haut, 0 en bas
 
115
  const clamp360 = d => Math.max(0, Math.min(360, d));
116
 
117
  function updateKnobFromY(yDeg){
118
+ const t = 1 - (yDeg / 360); // 360 -> top(0%), 0 -> bottom(100%)
119
  rotKnob.style.top = `${t*100}%`;
120
  rotYInput.value = String(Math.round(yDeg));
121
  rotYVal.textContent = `${Math.round(yDeg)}°`;
 
143
 
144
  if (!app.xr.supported){ message("WebXR n’est pas supporté sur cet appareil."); return; }
145
 
146
+ // ===== Gestion fiable du slider : Pointer Events en CAPTURE =====
147
+ let uiInteracting=false;
148
+ let draggingWrap=false;
149
+ let activePointerId=null;
150
 
151
+ const insideWrap = (target) => rotWrap.contains(target);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
+ const getDegFromPointer = (e) => {
 
154
  const rect = rotWrap.getBoundingClientRect();
155
+ const y = e.clientY ?? (e.touches && e.touches[0] && e.touches[0].clientY) ?? 0;
156
  const ratio = (y - rect.top) / rect.height; // 0 en haut -> 1 en bas
157
  const t = Math.max(0, Math.min(1, ratio));
158
+ return (1 - t) * 360; // inversion : haut=360, bas=0
159
+ };
160
 
161
+ // pointerdown CAPTURE → on “prend la main” AVANT PlayCanvas
162
+ const onPointerDownCapture = (e) => {
163
+ if (!insideWrap(e.target)) return;
164
  uiInteracting = true;
165
  draggingWrap = true;
166
+ activePointerId = e.pointerId ?? 1;
167
+ try { rotWrap.setPointerCapture?.(activePointerId); } catch {}
168
+ applyRotationY(getDegFromPointer(e));
169
+ e.preventDefault();
170
+ e.stopPropagation();
171
+ };
172
+
173
+ const onPointerMoveCapture = (e) => {
174
+ if (!draggingWrap || (e.pointerId ?? 1) !== activePointerId) return;
175
+ applyRotationY(getDegFromPointer(e));
176
+ e.preventDefault();
177
+ e.stopPropagation();
178
+ };
179
+
180
+ const endDrag = (e) => {
181
+ if (!draggingWrap || (e.pointerId ?? 1) !== activePointerId) return;
182
  draggingWrap = false;
 
183
  uiInteracting = false;
184
+ try { rotWrap.releasePointerCapture?.(activePointerId); } catch {}
185
+ activePointerId = null;
186
+ e.preventDefault();
187
+ e.stopPropagation();
188
+ };
189
 
190
+ document.addEventListener("pointerdown", onPointerDownCapture, { capture: true, passive: false });
191
+ document.addEventListener("pointermove", onPointerMoveCapture, { capture: true, passive: false });
192
+ document.addEventListener("pointerup", endDrag, { capture: true, passive: false });
193
+ document.addEventListener("pointercancel", endDrag, { capture: true, passive: false });
 
194
 
195
  // --- Démarrage AR
196
  const activateAR = () => {
 
209
  app.keyboard.on("keydown", (evt)=>{ if (evt.key===pc.KEY_ESCAPE && app.xr.active) app.xr.end(); });
210
 
211
  // ====== Filtre HORIZONTAL uniquement ======
212
+ const TMP_IN = new pc.Vec3(0,1,0), TMP_OUT = new pc.Vec3();
213
+ function isHorizontalUpFacing(rot, minDot = 0.75){ rot.transformVector(TMP_IN, TMP_OUT); return TMP_OUT.y >= minDot; }
 
 
 
 
214
 
215
+ // Hit Test
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;
223
 
224
  reticle.enabled = true;
225
  reticle.setPosition(pos);
 
230
  modelRoot.setPosition(pos);
231
 
232
  const e = new pc.Vec3(); rot.getEulerAngles(e);
233
+ applyRotationY(((e.y % 360)+360)%360);
 
234
 
235
  placedOnce = true;
236
  rotYInput.disabled = false;
 
241
  });
242
  });
243
 
244
+ // Déplacement XR (drag) — ignoré si UI active
245
  let isDragging=false;
246
  app.xr.input.on("add", (inputSource) => {
247
+ inputSource.on("selectstart", () => {
248
+ if (uiInteracting) return;
249
+ if (!placedOnce || !modelLoaded) return;
250
+
251
+ inputSource.hitTestStart({
252
+ entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
253
+ callback: (err, transientSource) => {
254
+ if (err) return;
255
+ isDragging = true;
256
+
257
+ transientSource.on("result", (pos, rot) => {
258
+ if (!isDragging) return;
259
+ if (!isHorizontalUpFacing(rot)) return;
260
+ modelRoot.setPosition(pos);
261
+ });
262
+
263
+ transientSource.once("remove", () => { isDragging = false; });
264
+ }
265
+ });
266
+ });
267
+ inputSource.on("selectend", () => { isDragging = false; });
268
+ });
269
+
270
+ // Desktop : rotation souris conservée (ignore si UI)
271
+ let rotateMode=false, lastMouseX=0; const ROTATE_SENSITIVITY=0.25;
272
+ 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; }});
273
+ 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); }});
274
+ app.mouse.on("mouseup",()=>{ isDragging=false; rotateMode=false; });
275
+ window.addEventListener("contextmenu",(e)=>e.preventDefault());
276
+
277
+ // Slider (accessibilité clavier)
278
+ rotYInput.disabled = true;
279
+ rotYInput.addEventListener("input",(e)=>{ if(!modelRoot.enabled) return; applyRotationY(parseFloat(e.target.value||"0")); },{passive:true});
280
+
281
+ // AR events
282
+ app.xr.on("start",()=>{ message("Session AR démarrée. Visez le sol pour détecter un plan…"); reticle.enabled=true; });
283
+ app.xr.on("end",()=>{ message("Session AR terminée."); reticle.enabled=false; isDragging=false; rotateMode=false; rotYInput.disabled=true; });
284
+ 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…"); });
285
+
286
+ if(!app.xr.isAvailable(pc.XRTYPE_AR)) message("AR immersive indisponible.");
287
+ else if(!app.xr.hitTest.supported) message("AR Hit Test non supporté.");
288
+ else message("Chargement du modèle…");
289
+ }
290
+ })();