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

Update viewer_ar.js

Browse files
Files changed (1) hide show
  1. viewer_ar.js +84 -149
viewer_ar.js CHANGED
@@ -1,7 +1,6 @@
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,57 +91,6 @@
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;
@@ -163,6 +111,7 @@
163
  if (!canvas) {
164
  canvas = document.createElement("canvas");
165
  canvas.id = "application-canvas";
 
166
  canvas.style.width = "100%";
167
  canvas.style.height = "100%";
168
  document.body.appendChild(canvas);
@@ -170,22 +119,6 @@
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
  // =========================
@@ -197,6 +130,7 @@
197
  message("Impossible de charger PlayCanvas (réseau/CDN). Réessaie plus tard.");
198
  return;
199
  }
 
200
  initARApp();
201
  })();
202
 
@@ -204,12 +138,8 @@
204
  // 5) App AR PlayCanvas
205
  // =========================
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,26 +198,6 @@
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,6 +210,8 @@
300
  receiveShadows: false
301
  });
302
  modelRoot.addChild(instance);
 
 
303
  modelRoot.setLocalScale(0.2, 0.2, 0.2);
304
 
305
  modelLoaded = true;
@@ -312,22 +224,20 @@
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
  }
328
  });
329
  };
330
 
 
331
  app.mouse.on("mousedown", () => {
332
  if (!app.xr.active) activateAR();
333
  });
@@ -339,6 +249,7 @@
339
  });
340
  }
341
 
 
342
  app.keyboard.on("keydown", (evt) => {
343
  if (evt.key === pc.KEY_ESCAPE && app.xr.active) app.xr.end();
344
  });
@@ -360,39 +271,35 @@
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
  }
373
  });
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) => {
393
  if (err) return;
394
  isDragging = true;
395
- activeTransientSources.add(transientSource);
396
 
397
  transientSource.on("result", (pos /*, rot */) => {
398
  if (!isDragging) return;
@@ -400,66 +307,94 @@
400
  });
401
 
402
  transientSource.once("remove", () => {
403
- activeTransientSources.delete(transientSource);
404
- if (activeTransientSources.size === 0) isDragging = false;
405
  });
406
  }
407
  });
408
  });
409
 
410
  inputSource.on("selectend", () => {
411
- cancelAllTransientHitTests();
412
  });
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());
464
 
465
  // --- Événements AR globaux ---
@@ -472,7 +407,7 @@
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) => {
 
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
 
4
  - Aucune dépendance externe autre que PlayCanvas et le GLB
5
  */
6
 
 
91
  user-select: none;
92
  pointer-events: none;
93
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  `;
95
  const styleTag = document.createElement("style");
96
  styleTag.textContent = css;
 
111
  if (!canvas) {
112
  canvas = document.createElement("canvas");
113
  canvas.id = "application-canvas";
114
+ // optionnel: force plein écran si aucun style
115
  canvas.style.width = "100%";
116
  canvas.style.height = "100%";
117
  document.body.appendChild(canvas);
 
119
  return canvas;
120
  }
121
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  // =========================
123
  // 4) Lancement
124
  // =========================
 
130
  message("Impossible de charger PlayCanvas (réseau/CDN). Réessaie plus tard.");
131
  return;
132
  }
133
+ // PlayCanvas dispo -> démarrer
134
  initARApp();
135
  })();
136
 
 
138
  // 5) App AR PlayCanvas
139
  // =========================
140
  function initARApp() {
141
+ const pc = window.pc; // alias
142
  const canvas = ensureCanvas();
 
 
 
 
143
  window.focus();
144
 
145
  const app = new pc.Application(canvas, {
 
198
  let modelLoaded = false;
199
  let placedOnce = false;
200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  // --- Chargement GLB ---
202
  app.assets.loadFromUrl(GLB_URL, "container", (err, asset) => {
203
  if (err) {
 
210
  receiveShadows: false
211
  });
212
  modelRoot.addChild(instance);
213
+
214
+ // Ajuste si besoin selon ton modèle
215
  modelRoot.setLocalScale(0.2, 0.2, 0.2);
216
 
217
  modelLoaded = true;
 
224
  return;
225
  }
226
 
227
+ // --- Démarrage AR ---
228
  const activateAR = () => {
229
  if (!app.xr.isAvailable(pc.XRTYPE_AR)) {
230
  message("AR immersive indisponible sur cet appareil.");
231
  return;
232
  }
233
  camera.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, {
 
 
 
234
  callback: (err) => {
235
  if (err) message(`Échec du démarrage AR : ${err.message}`);
236
  }
237
  });
238
  };
239
 
240
+ // Premier tap / clic → démarrer AR
241
  app.mouse.on("mousedown", () => {
242
  if (!app.xr.active) activateAR();
243
  });
 
249
  });
250
  }
251
 
252
+ // ESC → quitter
253
  app.keyboard.on("keydown", (evt) => {
254
  if (evt.key === pc.KEY_ESCAPE && app.xr.active) app.xr.end();
255
  });
 
271
  if (modelLoaded && !placedOnce) {
272
  modelRoot.enabled = true;
273
  modelRoot.setPosition(pos);
274
+ // Conserver uniquement la yaw (rotation Y)
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
  }
283
  });
284
  });
285
 
286
+ // --- Interactions ---
 
 
287
  let isDragging = false;
288
+ let rotateMode = false;
289
+ let lastAvgX = 0; // rotation tactile (2 doigts)
290
+ let lastMouseX = 0; // rotation souris
291
+ const ROTATE_SENSITIVITY = 0.25; // degrés / pixel approx.
 
 
292
 
293
+ // Déplacement (input XR : maintien/drag)
294
  app.xr.input.on("add", (inputSource) => {
295
  inputSource.on("selectstart", () => {
296
  if (!placedOnce || !modelLoaded) return;
297
+
298
  inputSource.hitTestStart({
299
  entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
300
  callback: (err, transientSource) => {
301
  if (err) return;
302
  isDragging = true;
 
303
 
304
  transientSource.on("result", (pos /*, rot */) => {
305
  if (!isDragging) return;
 
307
  });
308
 
309
  transientSource.once("remove", () => {
310
+ isDragging = false;
 
311
  });
312
  }
313
  });
314
  });
315
 
316
  inputSource.on("selectend", () => {
317
+ isDragging = false;
318
  });
319
  });
320
 
321
+ // Rotation tactile (2 doigts → glisser horizontal)
322
+ if (app.touch) {
323
+ let touches = new Map();
324
+ const getAvgX = () => {
325
+ if (touches.size === 0) return 0;
326
+ let s = 0;
327
+ touches.forEach((t) => (s += t.x));
328
+ return s / touches.size;
329
+ };
330
+
331
+ app.touch.on("touchstart", (e) => {
332
+ for (const t of e.touches) touches.set(t.id, { x: t.x, y: t.y });
333
+ if (touches.size >= 2) {
334
+ rotateMode = true;
335
+ lastAvgX = getAvgX();
336
+ }
337
+ });
338
+
339
+ app.touch.on("touchmove", (e) => {
340
+ for (const t of e.touches) {
341
+ if (touches.has(t.id)) touches.set(t.id, { x: t.x, y: t.y });
342
+ }
343
+ if (rotateMode && modelRoot.enabled) {
344
+ const avgX = getAvgX();
345
+ const dx = avgX - lastAvgX;
346
+ lastAvgX = avgX;
347
+ const euler = modelRoot.getEulerAngles();
348
+ modelRoot.setEulerAngles(euler.x, euler.y + dx * ROTATE_SENSITIVITY, euler.z);
349
+ }
350
+ });
351
+
352
+ app.touch.on("touchend", (e) => {
353
+ for (const t of e.changedTouches) touches.delete(t.id);
354
+ if (touches.size < 2) rotateMode = false;
355
+ });
356
+
357
+ app.touch.on("touchcancel", () => {
358
+ touches.clear();
359
+ rotateMode = false;
360
+ });
361
+ }
362
+
363
+ // Souris : déplacement (clic gauche maintenu) & rotation (clic droit ou Shift+clic gauche)
364
  app.mouse.on("mousedown", (e) => {
365
  if (!app.xr.active || !placedOnce) return;
366
+
367
+ if (e.button === 0 && !e.shiftKey) {
368
  isDragging = true;
369
+ } else if (e.button === 2 || (e.button === 0 && e.shiftKey)) {
370
+ rotateMode = true;
371
+ lastMouseX = e.x;
372
  }
373
  });
374
 
375
  app.mouse.on("mousemove", (e) => {
376
  if (!app.xr.active || !placedOnce) return;
377
+
378
+ if (isDragging) {
379
+ // On s’aligne sur le réticule (alimenté en continu par le hit test)
380
+ if (reticle.enabled) {
381
+ const pos = reticle.getPosition();
382
+ modelRoot.setPosition(pos);
383
+ }
384
+ } else if (rotateMode && modelRoot.enabled) {
385
+ const dx = e.x - lastMouseX;
386
+ lastMouseX = e.x;
387
+ const euler = modelRoot.getEulerAngles();
388
+ modelRoot.setEulerAngles(euler.x, euler.y + dx * ROTATE_SENSITIVITY, euler.z);
389
  }
390
  });
391
 
392
  app.mouse.on("mouseup", () => {
393
  isDragging = false;
394
+ rotateMode = false;
395
  });
396
 
397
+ // Empêcher menu contextuel (utile pour la rotation au clic droit)
398
  window.addEventListener("contextmenu", (e) => e.preventDefault());
399
 
400
  // --- Événements AR globaux ---
 
407
  message("Session AR terminée.");
408
  reticle.enabled = false;
409
  isDragging = false;
410
+ rotateMode = false;
411
  });
412
 
413
  app.xr.on(`available:${pc.XRTYPE_AR}`, (available) => {