MikaFil commited on
Commit
4cafc6d
·
verified ·
1 Parent(s): 3b4f453

Create viewer_ar.js

Browse files
Files changed (1) hide show
  1. viewer_ar.js +375 -0
viewer_ar.js ADDED
@@ -0,0 +1,375 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // script_ar.js
2
+ (() => {
3
+ const PLAYCANVAS_CDN = "https://cdn.playcanvas.com/playcanvas-stable.min.js";
4
+ const GLB_URL = "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/AR/tests/fraisier.glb";
5
+
6
+ // ============ Utils DOM ============
7
+ const ensureCanvas = () => {
8
+ let canvas = document.getElementById("application-canvas");
9
+ if (!canvas) {
10
+ canvas = document.createElement("canvas");
11
+ canvas.id = "application-canvas";
12
+ document.body.appendChild(canvas);
13
+ }
14
+ return canvas;
15
+ };
16
+
17
+ const css = `
18
+ .pc-ar-msg {
19
+ position: fixed; left: 50%; transform: translateX(-50%);
20
+ bottom: 16px; z-index: 9999; padding: 10px 14px;
21
+ background: rgba(0,0,0,.65); color: #fff; border-radius: 8px;
22
+ font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
23
+ font-size: 14px; line-height: 1.3; text-align: center;
24
+ max-width: min(90vw, 600px);
25
+ box-shadow: 0 6px 20px rgba(0,0,0,.25);
26
+ backdrop-filter: blur(4px);
27
+ }
28
+ `;
29
+ const styleTag = document.createElement("style");
30
+ styleTag.textContent = css;
31
+ document.head.appendChild(styleTag);
32
+
33
+ const message = (msg) => {
34
+ let el = document.querySelector(".pc-ar-msg");
35
+ if (!el) {
36
+ el = document.createElement("div");
37
+ el.className = "pc-ar-msg";
38
+ document.body.appendChild(el);
39
+ }
40
+ el.textContent = msg;
41
+ };
42
+
43
+ // ============ Loader PlayCanvas ============
44
+ const loadPcIfNeeded = () =>
45
+ new Promise((resolve, reject) => {
46
+ if (window.pc) return resolve();
47
+ const s = document.createElement("script");
48
+ s.src = PLAYCANVAS_CDN;
49
+ s.async = true;
50
+ s.onload = () => resolve();
51
+ s.onerror = () => reject(new Error("Échec du chargement de PlayCanvas"));
52
+ document.head.appendChild(s);
53
+ });
54
+
55
+ // ============ Main ============
56
+ loadPcIfNeeded()
57
+ .then(() => init())
58
+ .catch((e) => {
59
+ console.error(e);
60
+ message("Impossible de charger PlayCanvas.");
61
+ });
62
+
63
+ function init() {
64
+ const canvas = ensureCanvas();
65
+ window.focus();
66
+
67
+ // Crée l'app PlayCanvas (fond transparent pour l'AR)
68
+ const app = new pc.Application(canvas, {
69
+ mouse: new pc.Mouse(canvas),
70
+ touch: new pc.TouchDevice(canvas),
71
+ keyboard: new pc.Keyboard(window),
72
+ graphicsDeviceOptions: { alpha: true },
73
+ });
74
+
75
+ app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
76
+ app.setCanvasResolution(pc.RESOLUTION_AUTO);
77
+ app.graphicsDevice.maxPixelRatio = window.devicePixelRatio || 1;
78
+
79
+ const onResize = () => app.resizeCanvas();
80
+ window.addEventListener("resize", onResize);
81
+ app.on("destroy", () => window.removeEventListener("resize", onResize));
82
+
83
+ app.start();
84
+
85
+ // Caméra
86
+ const camera = new pc.Entity("Camera");
87
+ camera.addComponent("camera", {
88
+ clearColor: new pc.Color(0, 0, 0, 0),
89
+ farClip: 10000,
90
+ });
91
+ app.root.addChild(camera);
92
+
93
+ // Une lumière douce (utile en fallback / pré-AR)
94
+ const light = new pc.Entity("Light");
95
+ light.addComponent("light", {
96
+ type: "spot",
97
+ range: 30,
98
+ intensity: 1.2,
99
+ castShadows: false,
100
+ });
101
+ light.setLocalPosition(0, 10, 0);
102
+ app.root.addChild(light);
103
+
104
+ // Réticule (anneau) pour visualiser l’emplacement proposé par le hit-test
105
+ const reticleMat = new pc.StandardMaterial();
106
+ reticleMat.diffuse = new pc.Color(0.2, 0.8, 1.0);
107
+ reticleMat.opacity = 0.85;
108
+ reticleMat.blendType = pc.BLEND_NORMAL;
109
+ reticleMat.update();
110
+
111
+ const reticle = new pc.Entity("Reticle");
112
+ reticle.addComponent("render", { type: "torus", material: reticleMat });
113
+ reticle.setLocalScale(0.12, 0.005, 0.12);
114
+ reticle.enabled = false; // activé quand AR démarre
115
+ app.root.addChild(reticle);
116
+
117
+ // Conteneur du modèle GLB
118
+ const modelRoot = new pc.Entity("ModelRoot");
119
+ modelRoot.enabled = false; // activé après chargement + 1er placement
120
+ app.root.addChild(modelRoot);
121
+
122
+ let modelLoaded = false;
123
+ let placedOnce = false;
124
+
125
+ // Chargement GLB (Container)
126
+ app.assets.loadFromUrl(GLB_URL, "container", (err, asset) => {
127
+ if (err) {
128
+ console.error(err);
129
+ message("Échec du chargement du modèle GLB.");
130
+ return;
131
+ }
132
+ // Instancie le contenu GLB
133
+ const instance = asset.resource.instantiateRenderEntity({
134
+ castShadows: false,
135
+ receiveShadows: false,
136
+ });
137
+ modelRoot.addChild(instance);
138
+
139
+ // Mise à l’échelle raisonnable si le modèle est énorme/petit
140
+ // (ajuste si besoin)
141
+ modelRoot.setLocalScale(0.2, 0.2, 0.2);
142
+
143
+ modelLoaded = true;
144
+ message("Modèle chargé. Touchez l’écran pour démarrer l’AR.");
145
+ });
146
+
147
+ // ======== Gestion AR / Hit Test ========
148
+ if (!app.xr.supported) {
149
+ message("WebXR n’est pas supporté sur cet appareil.");
150
+ return;
151
+ }
152
+
153
+ const activateAR = () => {
154
+ if (!app.xr.isAvailable(pc.XRTYPE_AR)) {
155
+ message("AR immersive indisponible sur cet appareil.");
156
+ return;
157
+ }
158
+ camera.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, {
159
+ callback: (err) => {
160
+ if (err) {
161
+ message(`Échec du démarrage AR : ${err.message}`);
162
+ }
163
+ },
164
+ });
165
+ };
166
+
167
+ // Démarrer AR au premier tap/clic
168
+ app.mouse.on("mousedown", () => {
169
+ if (!app.xr.active) activateAR();
170
+ });
171
+ if (app.touch) {
172
+ app.touch.on("touchend", (evt) => {
173
+ if (!app.xr.active) activateAR();
174
+ evt.event.preventDefault();
175
+ evt.event.stopPropagation();
176
+ });
177
+ }
178
+
179
+ // ESC pour quitter
180
+ app.keyboard.on("keydown", (evt) => {
181
+ if (evt.key === pc.KEY_ESCAPE && app.xr.active) app.xr.end();
182
+ });
183
+
184
+ // Hit test global (pour le réticule et le premier placement auto)
185
+ app.xr.hitTest.on("available", () => {
186
+ app.xr.hitTest.start({
187
+ entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
188
+ callback: (err, hitSource) => {
189
+ if (err) {
190
+ message("Le AR hit test n’a pas pu démarrer.");
191
+ return;
192
+ }
193
+ hitSource.on("result", (pos, rot) => {
194
+ reticle.enabled = true;
195
+ reticle.setPosition(pos);
196
+ reticle.setRotation(rot);
197
+
198
+ // Premier placement auto du modèle dès que possible
199
+ if (modelLoaded && !placedOnce) {
200
+ modelRoot.enabled = true;
201
+ modelRoot.setPosition(pos);
202
+ // Conserver uniquement la yaw (rotation Y) issue du plan
203
+ const euler = new pc.Vec3();
204
+ rot.getEulerAngles(euler);
205
+ modelRoot.setEulerAngles(0, euler.y, 0);
206
+ placedOnce = true;
207
+ message("Objet placé. ⚠️ Glissez pour déplacer, pincez/2 doigts pour tourner.");
208
+ }
209
+ });
210
+ },
211
+ });
212
+ });
213
+
214
+ // État & helpers d’interaction
215
+ let isDragging = false;
216
+ let activeInputSource = null;
217
+ let rotateMode = false;
218
+ let lastAvgX = 0; // pour rotation à 2 doigts
219
+ let lastMouseX = 0;
220
+ const ROTATE_SENSITIVITY = 0.25; // degrés par pixel env.
221
+
222
+ // Déplacement avec un input source XR (touch en AR)
223
+ app.xr.input.on("add", (inputSource) => {
224
+ // Sélection (tap et maintien)
225
+ inputSource.on("selectstart", () => {
226
+ if (!placedOnce || !modelLoaded) return;
227
+
228
+ // Démarre un hit test TRANSITOIRE lié à la source pour suivre le doigt
229
+ inputSource.hitTestStart({
230
+ entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
231
+ callback: (err, transientSource) => {
232
+ if (err) return;
233
+ isDragging = true;
234
+ activeInputSource = { source: inputSource, ht: transientSource };
235
+
236
+ transientSource.on("result", (pos, rot) => {
237
+ if (!isDragging) return;
238
+ // Déplacer sur le plan détecté
239
+ modelRoot.setPosition(pos);
240
+ });
241
+
242
+ transientSource.once("remove", () => {
243
+ if (activeInputSource && activeInputSource.ht === transientSource) {
244
+ isDragging = false;
245
+ activeInputSource = null;
246
+ }
247
+ });
248
+ },
249
+ });
250
+ });
251
+
252
+ inputSource.on("selectend", () => {
253
+ isDragging = false;
254
+ activeInputSource = null;
255
+ });
256
+ });
257
+
258
+ // ======== Gestes Tactiles pour rotation (2 doigts) ========
259
+ if (app.touch) {
260
+ let touches = new Map();
261
+
262
+ const getAvgX = () => {
263
+ if (touches.size === 0) return 0;
264
+ let sum = 0;
265
+ touches.forEach((t) => (sum += t.x));
266
+ return sum / touches.size;
267
+ };
268
+
269
+ app.touch.on("touchstart", (e) => {
270
+ for (const t of e.touches) touches.set(t.id, { x: t.x, y: t.y });
271
+ if (touches.size >= 2) {
272
+ rotateMode = true;
273
+ lastAvgX = getAvgX();
274
+ }
275
+ });
276
+
277
+ app.touch.on("touchmove", (e) => {
278
+ for (const t of e.touches) {
279
+ if (touches.has(t.id)) touches.set(t.id, { x: t.x, y: t.y });
280
+ }
281
+ if (rotateMode && modelRoot.enabled) {
282
+ const avgX = getAvgX();
283
+ const dx = avgX - lastAvgX;
284
+ lastAvgX = avgX;
285
+ // Appliquer rotation Y
286
+ const euler = modelRoot.getEulerAngles();
287
+ modelRoot.setEulerAngles(euler.x, euler.y + dx * ROTATE_SENSITIVITY, euler.z);
288
+ }
289
+ });
290
+
291
+ app.touch.on("touchend", (e) => {
292
+ for (const t of e.changedTouches) touches.delete(t.id);
293
+ if (touches.size < 2) rotateMode = false;
294
+ });
295
+
296
+ app.touch.on("touchcancel", () => {
297
+ touches.clear();
298
+ rotateMode = false;
299
+ });
300
+ }
301
+
302
+ // ======== Souris (déplacement = clic gauche maintenu; rotation = clic droit ou Shift+clic) ========
303
+ app.mouse.on("mousedown", (e) => {
304
+ if (!app.xr.active || !placedOnce) return;
305
+
306
+ if (e.button === 0 && !e.shiftKey) {
307
+ // drag pour déplacement : on démarre un hit-test à partir du centre de l’écran à chaque move
308
+ isDragging = true;
309
+ } else if (e.button === 2 || (e.button === 0 && e.shiftKey)) {
310
+ rotateMode = true;
311
+ lastMouseX = e.x;
312
+ }
313
+ });
314
+
315
+ app.mouse.on("mousemove", (e) => {
316
+ if (!app.xr.active || !placedOnce) return;
317
+
318
+ if (isDragging && app.xr.hitTest.supported) {
319
+ // Créer un hit test ponctuel lié au pointeur n’est pas direct via souris.
320
+ // Astuce : on projette un point écran -> monde en AR via hit test global du reticle
321
+ // Ici, on se sert du reticle déjà alimenté par le hit test continu pour repositionner rapidement.
322
+ if (reticle.enabled) {
323
+ const pos = reticle.getPosition();
324
+ modelRoot.setPosition(pos);
325
+ }
326
+ } else if (rotateMode && modelRoot.enabled) {
327
+ const dx = e.x - lastMouseX;
328
+ lastMouseX = e.x;
329
+ const euler = modelRoot.getEulerAngles();
330
+ modelRoot.setEulerAngles(euler.x, euler.y + dx * ROTATE_SENSITIVITY, euler.z);
331
+ }
332
+ });
333
+
334
+ app.mouse.on("mouseup", () => {
335
+ isDragging = false;
336
+ rotateMode = false;
337
+ });
338
+
339
+ // Empêcher le menu contextuel sur clic droit (utile pour la rotation)
340
+ window.addEventListener("contextmenu", (e) => e.preventDefault());
341
+
342
+ // ======== Événements AR globaux ========
343
+ app.xr.on("start", () => {
344
+ message("Session AR démarrée. Visez le sol pour détecter un plan…");
345
+ reticle.enabled = true;
346
+ });
347
+
348
+ app.xr.on("end", () => {
349
+ message("Session AR terminée.");
350
+ reticle.enabled = false;
351
+ isDragging = false;
352
+ rotateMode = false;
353
+ activeInputSource = null;
354
+ });
355
+
356
+ app.xr.on(`available:${pc.XRTYPE_AR}`, (available) => {
357
+ if (!available) {
358
+ message("AR immersive indisponible.");
359
+ } else if (!app.xr.hitTest.supported) {
360
+ message("AR Hit Test non supporté.");
361
+ } else {
362
+ message(modelLoaded ? "Touchez l’écran pour démarrer l’AR." : "Chargement du modèle…");
363
+ }
364
+ });
365
+
366
+ // Messages initiaux
367
+ if (!app.xr.isAvailable(pc.XRTYPE_AR)) {
368
+ message("AR immersive indisponible.");
369
+ } else if (!app.xr.hitTest.supported) {
370
+ message("AR Hit Test non supporté.");
371
+ } else {
372
+ message("Chargement du modèle…");
373
+ }
374
+ }
375
+ })();