MikaFil commited on
Commit
acea2fa
·
verified ·
1 Parent(s): dd5533e

Create viewer_ar_ios.js

Browse files
Files changed (1) hide show
  1. viewer_ar_ios.js +601 -0
viewer_ar_ios.js ADDED
@@ -0,0 +1,601 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* script_ar.js — AR PlayCanvas + GLB HuggingFace (GLB/USDZ via config.json)
2
+ - Lit config.json (data-config) => { "glb_url": "...", "usdz_url": "..." }
3
+ - Android/Desktop: WebXR AR (horizontaux uniquement) + slider yaw + blob shadow
4
+ - iOS: fallback AR Quick Look (USDZ) — pas de WebXR
5
+ */
6
+
7
+ (function () {
8
+ // ===== Utils: config + platform =====
9
+ function findConfigUrl() {
10
+ var el = document.currentScript || null;
11
+ if (!el) {
12
+ var scripts = document.getElementsByTagName('script');
13
+ for (var i = scripts.length - 1; i >= 0; i--) {
14
+ if (scripts[i].getAttribute && scripts[i].getAttribute('data-config')) {
15
+ el = scripts[i];
16
+ break;
17
+ }
18
+ }
19
+ }
20
+ if (!el) return null;
21
+ var url = el.getAttribute('data-config');
22
+ return url || null;
23
+ }
24
+
25
+ function isIOS() {
26
+ return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
27
+ }
28
+
29
+ function timeout(ms) {
30
+ return new Promise(function (_res, rej) {
31
+ setTimeout(function () { rej(new Error("timeout")); }, ms);
32
+ });
33
+ }
34
+
35
+ async function loadConfigJson(url) {
36
+ if (!url) return null;
37
+ try {
38
+ var resp = await fetch(url, { cache: 'no-store' });
39
+ if (!resp.ok) throw new Error("HTTP " + resp.status);
40
+ var json = await resp.json();
41
+ return json;
42
+ } catch (e) {
43
+ console.error("Erreur chargement config.json:", e);
44
+ return null;
45
+ }
46
+ }
47
+
48
+ // ===== PlayCanvas version fixée (Android/Desktop) =====
49
+ var PC_VERSION = "2.11.7";
50
+ var PC_URLS = {
51
+ esm: ["https://cdn.jsdelivr.net/npm/playcanvas@" + PC_VERSION + "/build/playcanvas.min.mjs"],
52
+ umd: ["https://cdn.jsdelivr.net/npm/playcanvas@" + PC_VERSION + "/build/playcanvas.min.js"]
53
+ };
54
+
55
+ async function loadPlayCanvasRobust(opts) {
56
+ opts = opts || {};
57
+ var esmFirst = (typeof opts.esmFirst === "boolean") ? opts.esmFirst : true;
58
+ var loadTimeoutMs = (typeof opts.loadTimeoutMs === "number") ? opts.loadTimeoutMs : 15000;
59
+
60
+ if (window.pc && window.pc.Application) return window.pc;
61
+
62
+ async function tryESM() {
63
+ for (var i = 0; i < PC_URLS.esm.length; i++) {
64
+ var url = PC_URLS.esm[i];
65
+ try {
66
+ var mod = await Promise.race([import(/* @vite-ignore */ url), timeout(loadTimeoutMs)]);
67
+ var ns = (mod && (mod.pc || mod["default"])) || mod;
68
+ if (ns && ns.Application) {
69
+ if (!window.pc) window.pc = ns;
70
+ return window.pc;
71
+ }
72
+ } catch (e) { /* continue */ }
73
+ }
74
+ throw new Error("ESM failed");
75
+ }
76
+
77
+ async function tryUMD() {
78
+ for (var j = 0; j < PC_URLS.umd.length; j++) {
79
+ var url2 = PC_URLS.umd[j];
80
+ try {
81
+ await Promise.race([
82
+ new Promise(function (res, rej) {
83
+ var s = document.createElement("script");
84
+ s.src = url2;
85
+ s.async = true;
86
+ s.onload = function () { res(); };
87
+ s.onerror = function () { rej(new Error("script error")); };
88
+ document.head.appendChild(s);
89
+ }),
90
+ timeout(loadTimeoutMs)
91
+ ]);
92
+ if (window.pc && window.pc.Application) return window.pc;
93
+ } catch (e) { /* continue */ }
94
+ }
95
+ throw new Error("UMD failed");
96
+ }
97
+
98
+ try {
99
+ if (esmFirst) return await tryESM();
100
+ return await tryUMD();
101
+ } catch (e) {
102
+ if (esmFirst) return await tryUMD();
103
+ return await tryESM();
104
+ }
105
+ }
106
+
107
+ // ===== UI / Overlay commun =====
108
+ var css = [
109
+ ".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}",
110
+ "#xr-overlay-root{position:fixed;inset:0;z-index:9999;pointer-events:none}",
111
+
112
+ ".ar-ui{position:absolute;right:12px;top:50%;transform:translateY(-50%);background:rgba(0,0,0,0.55);color:#fff;padding:12px 10px;border-radius:16px;width:56px;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;pointer-events:auto;display:flex;flex-direction:column;align-items:center;gap:8px;box-shadow:0 6px 18px rgba(0,0,0,.25);backdrop-filter:blur(6px);touch-action:none}",
113
+ ".ar-ui .label{font-size:12px;text-align:center;opacity:.95}",
114
+ ".rotY-wrap{position:relative;width:48px;height:200px;display:flex;align-items:center;justify-content:center;touch-action:none;overflow:visible;pointer-events:auto}",
115
+ ".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}",
116
+ ".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}",
117
+ ".ar-ui input[type=\"range\"].rotY{position:absolute;opacity:0;pointer-events:none;width:0;height:0}",
118
+ ".ar-ui .val{font-size:12px;opacity:.95}",
119
+
120
+ /* iOS Quick Look button */
121
+ "#ios-quicklook-btn{position:fixed;left:50%;transform:translateX(-50%);bottom:16px;z-index:10001;display:inline-block;pointer-events:auto}",
122
+ "#ios-quicklook-btn img{display:block;height:44px;width:auto}"
123
+ ].join("\n");
124
+ var styleTag = document.createElement("style");
125
+ styleTag.textContent = css;
126
+ document.head.appendChild(styleTag);
127
+
128
+ function ensureOverlayRoot() {
129
+ var r = document.getElementById("xr-overlay-root");
130
+ if (!r) {
131
+ r = document.createElement("div");
132
+ r.id = "xr-overlay-root";
133
+ document.body.appendChild(r);
134
+ }
135
+ return r;
136
+ }
137
+ var overlayRoot = ensureOverlayRoot();
138
+
139
+ function message(msg) {
140
+ var el = overlayRoot.querySelector(".pc-ar-msg");
141
+ if (!el) {
142
+ el = document.createElement("div");
143
+ el.className = "pc-ar-msg";
144
+ overlayRoot.appendChild(el);
145
+ }
146
+ el.textContent = msg;
147
+ }
148
+
149
+ function ensureQuickLookButton(USDZ_URL) {
150
+ var btn = document.getElementById("ios-quicklook-btn");
151
+ if (btn) return btn;
152
+
153
+ var anchor = document.createElement("a");
154
+ anchor.id = "ios-quicklook-btn";
155
+ anchor.setAttribute("rel", "ar");
156
+ anchor.setAttribute("href", USDZ_URL);
157
+
158
+ var img = document.createElement("img");
159
+ img.alt = "Voir en AR";
160
+ img.src =
161
+ "data:image/svg+xml;charset=utf-8," +
162
+ encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="160" height="44"><rect rx="8" ry="8" width="160" height="44" fill="black"/><text x="80" y="28" font-size="14" text-anchor="middle" fill="white" font-family="system-ui, -apple-system, Segoe UI, Roboto">Voir en AR</text></svg>');
163
+
164
+ anchor.appendChild(img);
165
+ document.body.appendChild(anchor);
166
+ return anchor;
167
+ }
168
+
169
+ function ensureCanvas() {
170
+ var c = document.getElementById("application-canvas");
171
+ if (!c) {
172
+ c = document.createElement("canvas");
173
+ c.id = "application-canvas";
174
+ c.style.width = "100%";
175
+ c.style.height = "100%";
176
+ document.body.appendChild(c);
177
+ }
178
+ return c;
179
+ }
180
+
181
+ function ensureSliderUI() {
182
+ var p = overlayRoot.querySelector(".ar-ui");
183
+ if (p) return p;
184
+ p = document.createElement("div");
185
+ p.className = "ar-ui";
186
+ p.innerHTML =
187
+ '<div class="label">Rotation</div>' +
188
+ '<div class="rotY-wrap" id="ar-rotY-wrap">' +
189
+ ' <div class="rotY-rail"></div>' +
190
+ ' <div class="rotY-knob" id="ar-rotY-knob"></div>' +
191
+ ' <input class="rotY" id="ar-rotY" type="range" min="0" max="360" step="1" value="0"/>' +
192
+ '</div>' +
193
+ '<div class="val" id="ar-rotY-val">0°</div>';
194
+ overlayRoot.appendChild(p);
195
+ return p;
196
+ }
197
+
198
+ // ===== Boot : charge config, route iOS vs Android/Desktop =====
199
+ (async function () {
200
+ // 1) Lire config.json
201
+ var cfgUrl = findConfigUrl();
202
+ var cfg = await loadConfigJson(cfgUrl);
203
+ var GLB_URL = (cfg && typeof cfg.glb_url === "string" && cfg.glb_url) ?
204
+ cfg.glb_url :
205
+ "https://huggingface.co/datasets/MikaFil/viewer_gs/resolve/main/AR/tests/danae_no_metallic.glb";
206
+ var USDZ_URL = (cfg && typeof cfg.usdz_url === "string" && cfg.usdz_url) ?
207
+ cfg.usdz_url :
208
+ null;
209
+
210
+ // 2) iOS → AR Quick Look (fallback)
211
+ if (isIOS()) {
212
+ if (USDZ_URL) {
213
+ ensureQuickLookButton(USDZ_URL);
214
+ message("iOS détecté : utilisez le bouton « Voir en AR » (AR Quick Look).");
215
+ } else {
216
+ message("iOS détecté, mais aucun 'usdz_url' dans config.json.");
217
+ }
218
+ // Option: on pourrait lancer un viewer 3D non-AR ici si tu veux
219
+ return;
220
+ }
221
+
222
+ // 3) Android/Desktop → charger PlayCanvas et lancer l'app WebXR
223
+ try {
224
+ await loadPlayCanvasRobust({ esmFirst: true, loadTimeoutMs: 15000 });
225
+ } catch (e) {
226
+ console.error("Chargement PlayCanvas échoué ->", e);
227
+ message("Impossible de charger PlayCanvas (réseau/CDN). Réessaie plus tard.");
228
+ return;
229
+ }
230
+ initARApp(GLB_URL);
231
+ })();
232
+
233
+ // ===== App Android/Desktop (WebXR) =====
234
+ function initARApp(GLB_URL) {
235
+ var pc = window.pc;
236
+ var canvas = ensureCanvas();
237
+ var ui = ensureSliderUI();
238
+ var rotWrap = ui.querySelector("#ar-rotY-wrap");
239
+ var rotKnob = ui.querySelector("#ar-rotY-knob");
240
+ var rotYInput = ui.querySelector("#ar-rotY");
241
+ var rotYVal = ui.querySelector("#ar-rotY-val");
242
+
243
+ window.focus();
244
+
245
+ var app = new pc.Application(canvas, {
246
+ mouse: new pc.Mouse(canvas),
247
+ touch: new pc.TouchDevice(canvas),
248
+ keyboard: new pc.Keyboard(window),
249
+ graphicsDeviceOptions: { alpha: true }
250
+ });
251
+ app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
252
+ app.setCanvasResolution(pc.RESOLUTION_AUTO);
253
+ app.graphicsDevice.maxPixelRatio = window.devicePixelRatio || 1;
254
+ var onResize = function () { app.resizeCanvas(); };
255
+ window.addEventListener("resize", onResize);
256
+ app.on("destroy", function () { window.removeEventListener("resize", onResize); });
257
+ app.start();
258
+
259
+ // ===== Rendu / PBR defaults =====
260
+ app.scene.gammaCorrection = pc.GAMMA_SRGB;
261
+ app.scene.toneMapping = pc.TONEMAP_ACES;
262
+ app.scene.exposure = 1;
263
+ app.scene.ambientLight = new pc.Color(1, 1, 1);
264
+
265
+ // Camera + lumière
266
+ var camera = new pc.Entity("Camera");
267
+ camera.addComponent("camera", { clearColor: new pc.Color(0, 0, 0, 0), farClip: 10000 });
268
+ app.root.addChild(camera);
269
+
270
+ var light = new pc.Entity("Light");
271
+ light.addComponent("light", { type: "directional", intensity: 1.0, castShadows: true, color: new pc.Color(1, 1, 1) });
272
+ light.setLocalEulerAngles(45, 30, 0);
273
+ app.root.addChild(light);
274
+
275
+ // Réticule
276
+ var reticleMat = new pc.StandardMaterial();
277
+ reticleMat.diffuse = new pc.Color(0.2, 0.8, 1.0);
278
+ reticleMat.opacity = 0.85;
279
+ reticleMat.blendType = pc.BLEND_NORMAL;
280
+ reticleMat.update();
281
+
282
+ var reticle = new pc.Entity("Reticle");
283
+ reticle.addComponent("render", { type: "torus", material: reticleMat });
284
+ reticle.setLocalScale(0.12, 0.005, 0.12);
285
+ reticle.enabled = false;
286
+ app.root.addChild(reticle);
287
+
288
+ // Modèle
289
+ var modelRoot = new pc.Entity("ModelRoot");
290
+ modelRoot.enabled = false;
291
+ app.root.addChild(modelRoot);
292
+ var modelLoaded = false, placedOnce = false;
293
+
294
+ // ===== Blob Shadow =====
295
+ var blob = null;
296
+ var BLOB_SIZE = 0.4;
297
+ var BLOB_OFFSET_Y = 0.005;
298
+
299
+ function makeBlobTexture(app, size) {
300
+ size = size || 256;
301
+ var cvs = document.createElement('canvas');
302
+ cvs.width = cvs.height = size;
303
+ var ctx = cvs.getContext('2d');
304
+ var r = size * 0.45;
305
+ var grd = ctx.createRadialGradient(size/2, size/2, r*0.2, size/2, size/2, r);
306
+ grd.addColorStop(0, 'rgba(0,0,0,0.5)');
307
+ grd.addColorStop(1, 'rgba(0,0,0,0.0)');
308
+ ctx.fillStyle = grd;
309
+ ctx.fillRect(0, 0, size, size);
310
+
311
+ var tex = new pc.Texture(app.graphicsDevice, {
312
+ width: size,
313
+ height: size,
314
+ format: pc.PIXELFORMAT_R8_G8_B8_A8,
315
+ mipmaps: true,
316
+ magFilter: pc.FILTER_LINEAR,
317
+ minFilter: pc.FILTER_LINEAR_MIPMAP_LINEAR
318
+ });
319
+ tex.setSource(cvs);
320
+ return tex;
321
+ }
322
+
323
+ function createBlobShadowAt(pos, rot) {
324
+ var tex = makeBlobTexture(app, 256);
325
+
326
+ var blobMat = new pc.StandardMaterial();
327
+ blobMat.diffuse = new pc.Color(0, 0, 0);
328
+ blobMat.opacity = 1.0;
329
+ blobMat.opacityMap = tex;
330
+ blobMat.opacityMapChannel = 'a';
331
+ blobMat.useLighting = false;
332
+ blobMat.blendType = pc.BLEND_NORMAL;
333
+ blobMat.depthWrite = false;
334
+ blobMat.alphaTest = 0;
335
+ blobMat.update();
336
+
337
+ var e = new pc.Entity("BlobShadow");
338
+ e.addComponent("render", { type: "plane", castShadows: false, receiveShadows: false });
339
+ e.render.material = blobMat;
340
+
341
+ e.setPosition(pos.x, pos.y + BLOB_OFFSET_Y, pos.z);
342
+ e.setRotation(rot);
343
+ e.setLocalScale(BLOB_SIZE, 1, BLOB_SIZE);
344
+
345
+ app.root.addChild(e);
346
+ return e;
347
+ }
348
+
349
+ // Euler base
350
+ var baseEulerX = 0, baseEulerZ = 0;
351
+
352
+ // Rotation via slider
353
+ var rotationYDeg = 0;
354
+ function clamp360(d) { return Math.max(0, Math.min(360, d)); }
355
+
356
+ function updateKnobFromY(yDeg) {
357
+ var t = 1 - (yDeg / 360);
358
+ rotKnob.style.top = String(t * 100) + "%";
359
+ rotYInput.value = String(Math.round(yDeg));
360
+ rotYVal.textContent = String(Math.round(yDeg)) + "°";
361
+ }
362
+ function applyRotationY(deg) {
363
+ var y = clamp360(deg);
364
+ rotationYDeg = y;
365
+ modelRoot.setEulerAngles(baseEulerX, y, baseEulerZ);
366
+ updateKnobFromY(y);
367
+ }
368
+
369
+ function updateBlobPositionUnder(pos, rotLikePlane) {
370
+ if (!blob) return;
371
+ blob.setPosition(pos.x, pos.y + BLOB_OFFSET_Y, pos.z);
372
+ if (rotLikePlane) blob.setRotation(rotLikePlane);
373
+ }
374
+
375
+ // Chargement GLB (config.json)
376
+ app.assets.loadFromUrl(GLB_URL, "container", function (err, asset) {
377
+ if (err) { console.error(err); message("Échec du chargement du modèle GLB."); return; }
378
+ var instance = asset.resource.instantiateRenderEntity({ castShadows: true, receiveShadows: false });
379
+ modelRoot.addChild(instance);
380
+ modelRoot.setLocalScale(1, 1, 1);
381
+
382
+ // Fix matériaux
383
+ var renders = instance.findComponents('render');
384
+ for (var ri = 0; ri < renders.length; ri++) {
385
+ var r = renders[ri];
386
+ r.castShadows = true;
387
+ for (var mi = 0; mi < r.meshInstances.length; mi++) {
388
+ var mat = r.meshInstances[mi].material;
389
+ if (!mat) continue;
390
+ if (mat.diffuse && (mat.diffuse.r !== 1 || mat.diffuse.g !== 1 || mat.diffuse.b !== 1)) {
391
+ mat.diffuse.set(1, 1, 1);
392
+ }
393
+ if (typeof mat.useSkybox !== "undefined") mat.useSkybox = true;
394
+ mat.update();
395
+ }
396
+ }
397
+
398
+ var initE = modelRoot.getEulerAngles();
399
+ baseEulerX = initE.x; baseEulerZ = initE.z;
400
+
401
+ modelLoaded = true;
402
+ message("Modèle chargé. Touchez l’écran pour démarrer l’AR.");
403
+ });
404
+
405
+ if (!app.xr.supported) { message("WebXR n’est pas supporté sur cet appareil."); return; }
406
+
407
+ // ===== Slider fiable : Pointer Events en CAPTURE =====
408
+ var uiInteracting = false;
409
+ var draggingWrap = false;
410
+ var activePointerId = null;
411
+
412
+ function insideWrap(target) { return rotWrap.contains(target); }
413
+ function degFromPointer(e) {
414
+ var rect = rotWrap.getBoundingClientRect();
415
+ var y = (e.clientY != null) ? e.clientY : ((e.touches && e.touches[0] && e.touches[0].clientY) || 0);
416
+ var ratio = (y - rect.top) / rect.height;
417
+ var t = Math.max(0, Math.min(1, ratio));
418
+ return (1 - t) * 360;
419
+ }
420
+
421
+ function onPointerDownCapture(e) {
422
+ if (!insideWrap(e.target)) return;
423
+ uiInteracting = true;
424
+ draggingWrap = true;
425
+ activePointerId = (e.pointerId != null) ? e.pointerId : 1;
426
+ if (rotWrap.setPointerCapture) {
427
+ try { rotWrap.setPointerCapture(activePointerId); } catch (er) {}
428
+ }
429
+ applyRotationY(degFromPointer(e));
430
+ e.preventDefault();
431
+ e.stopPropagation();
432
+ }
433
+ function onPointerMoveCapture(e) {
434
+ if (!draggingWrap || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
435
+ applyRotationY(degFromPointer(e));
436
+ e.preventDefault();
437
+ e.stopPropagation();
438
+ }
439
+ function endDrag(e) {
440
+ if (!draggingWrap || ((e.pointerId != null ? e.pointerId : 1) !== activePointerId)) return;
441
+ draggingWrap = false;
442
+ uiInteracting = false;
443
+ if (rotWrap.releasePointerCapture) {
444
+ try { rotWrap.releasePointerCapture(activePointerId); } catch (er) {}
445
+ }
446
+ activePointerId = null;
447
+ e.preventDefault();
448
+ e.stopPropagation();
449
+ }
450
+
451
+ document.addEventListener("pointerdown", onPointerDownCapture, { capture: true, passive: false });
452
+ document.addEventListener("pointermove", onPointerMoveCapture, { capture: true, passive: false });
453
+ document.addEventListener("pointerup", endDrag, { capture: true, passive: false });
454
+ document.addEventListener("pointercancel", endDrag, { capture: true, passive: false });
455
+
456
+ // --- Démarrage AR (Android/Desktop)
457
+ function activateAR() {
458
+ if (!app.xr.isAvailable(pc.XRTYPE_AR)) { message("AR immersive indisponible sur cet appareil."); return; }
459
+ if (!app.xr.domOverlay) app.xr.domOverlay = {};
460
+ app.xr.domOverlay.root = document.getElementById("xr-overlay-root");
461
+ camera.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, {
462
+ requiredFeatures: ["hit-test", "dom-overlay"],
463
+ domOverlay: { root: app.xr.domOverlay.root },
464
+ callback: function (err) {
465
+ if (err) {
466
+ console.error("Échec du démarrage AR :", err);
467
+ message("Échec du démarrage AR : " + (err.message || err));
468
+ }
469
+ }
470
+ });
471
+ }
472
+ app.mouse.on("mousedown", function () { if (!app.xr.active && !uiInteracting) activateAR(); });
473
+ if (app.touch) {
474
+ app.touch.on("touchend", function (evt) {
475
+ if (!app.xr.active && !uiInteracting) activateAR();
476
+ evt.event.preventDefault();
477
+ evt.event.stopPropagation();
478
+ });
479
+ }
480
+ app.keyboard.on("keydown", function (evt) { if (evt.key === pc.KEY_ESCAPE && app.xr.active) app.xr.end(); });
481
+
482
+ // ====== Hit-test HORIZONTAL uniquement ======
483
+ var TMP_IN = new pc.Vec3(0, 1, 0), TMP_OUT = new pc.Vec3();
484
+ function isHorizontalUpFacing(rot, minDot) {
485
+ minDot = (typeof minDot === "number") ? minDot : 0.75;
486
+ rot.transformVector(TMP_IN, TMP_OUT);
487
+ return TMP_OUT.y >= minDot;
488
+ }
489
+
490
+ // Hit Test global
491
+ app.xr.hitTest.on("available", function () {
492
+ app.xr.hitTest.start({
493
+ entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
494
+ callback: function (err, hitSource) {
495
+ if (err) { message("Le AR hit test n’a pas pu démarrer."); return; }
496
+ hitSource.on("result", function (pos, rot) {
497
+ if (!isHorizontalUpFacing(rot)) return;
498
+
499
+ reticle.enabled = true;
500
+ reticle.setPosition(pos);
501
+ reticle.setRotation(rot);
502
+
503
+ if (modelLoaded && !placedOnce) {
504
+ modelRoot.enabled = true;
505
+ modelRoot.setPosition(pos);
506
+
507
+ // Ombre de contact
508
+ blob = createBlobShadowAt(pos, rot);
509
+
510
+ var e = new pc.Vec3();
511
+ rot.getEulerAngles(e);
512
+ var y0 = ((e.y % 360) + 360) % 360;
513
+ applyRotationY(y0);
514
+
515
+ placedOnce = true;
516
+ rotYInput.disabled = false;
517
+ message("Objet placé. Glissez pour déplacer, tournez-le avec le slider →");
518
+ }
519
+ });
520
+ }
521
+ });
522
+ });
523
+
524
+ // Déplacement XR (drag) — ignoré si UI active
525
+ var isDragging = false;
526
+ app.xr.input.on("add", function (inputSource) {
527
+ inputSource.on("selectstart", function () {
528
+ if (uiInteracting) return;
529
+ if (!placedOnce || !modelLoaded) return;
530
+
531
+ inputSource.hitTestStart({
532
+ entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE],
533
+ callback: function (err, transientSource) {
534
+ if (err) return;
535
+ isDragging = true;
536
+
537
+ transientSource.on("result", function (pos, rot) {
538
+ if (!isDragging) return;
539
+ if (!isHorizontalUpFacing(rot)) return;
540
+ modelRoot.setPosition(pos);
541
+ updateBlobPositionUnder(pos, rot);
542
+ });
543
+
544
+ transientSource.once("remove", function () { isDragging = false; });
545
+ }
546
+ });
547
+ });
548
+ inputSource.on("selectend", function () { isDragging = false; });
549
+ });
550
+
551
+ // Desktop : rotation souris (ignore si UI)
552
+ var rotateMode = false, lastMouseX = 0;
553
+ var ROTATE_SENSITIVITY = 0.25;
554
+ app.mouse.on("mousedown", function (e) {
555
+ if (!app.xr.active || !placedOnce || uiInteracting) return;
556
+ if (e.button === 0 && !e.shiftKey) {
557
+ isDragging = true;
558
+ } else if (e.button === 2 || (e.button === 0 && e.shiftKey)) {
559
+ rotateMode = true;
560
+ lastMouseX = e.x;
561
+ }
562
+ });
563
+ app.mouse.on("mousemove", function (e) {
564
+ if (!app.xr.active || !placedOnce || uiInteracting) return;
565
+ if (isDragging) {
566
+ if (reticle.enabled) {
567
+ var p = reticle.getPosition();
568
+ modelRoot.setPosition(p);
569
+ updateBlobPositionUnder(p, null);
570
+ }
571
+ } else if (rotateMode && modelRoot.enabled) {
572
+ var dx = e.x - lastMouseX;
573
+ lastMouseX = e.x;
574
+ applyRotationY(rotationYDeg + dx * ROTATE_SENSITIVITY);
575
+ }
576
+ });
577
+ app.mouse.on("mouseup", function () { isDragging = false; rotateMode = false; });
578
+ window.addEventListener("contextmenu", function (e) { e.preventDefault(); });
579
+
580
+ // Slider (accessibilité clavier)
581
+ rotYInput.disabled = true;
582
+ rotYInput.addEventListener("input", function (e) {
583
+ if (!modelRoot.enabled) return;
584
+ var v = parseFloat(e.target.value || "0");
585
+ applyRotationY(v);
586
+ }, { passive: true });
587
+
588
+ // AR events
589
+ app.xr.on("start", function () { message("Session AR démarrée. Visez le sol pour détecter un plan…"); reticle.enabled = true; });
590
+ app.xr.on("end", function () { message("Session AR terminée."); reticle.enabled = false; isDragging = false; rotateMode = false; rotYInput.disabled = true; });
591
+ app.xr.on("available:" + pc.XRTYPE_AR, function (a) {
592
+ if (!a) message("AR immersive indisponible.");
593
+ else if (!app.xr.hitTest.supported) message("AR Hit Test non supporté.");
594
+ else message(modelLoaded ? "Touchez l’écran pour démarrer l’AR." : "Chargement du modèle…");
595
+ });
596
+
597
+ if (!app.xr.isAvailable(pc.XRTYPE_AR)) message("AR immersive indisponible.");
598
+ else if (!app.xr.hitTest.supported) message("AR Hit Test non supporté.");
599
+ else message("Chargement du modèle…");
600
+ }
601
+ })();