MikaFil commited on
Commit
c0af744
·
verified ·
1 Parent(s): 9791406

Update interface.js

Browse files
Files changed (1) hide show
  1. interface.js +255 -454
interface.js CHANGED
@@ -1,493 +1,294 @@
1
- // interface.js
2
- // ==============================
3
-
4
- const currentScriptTag = document.currentScript;
5
-
6
- (async function () {
7
- // 1) Localiser la balise <script> et lire data-config
8
- let scriptTag = currentScriptTag;
9
-
10
- if (!scriptTag) {
11
- const scripts = document.getElementsByTagName('script');
12
- for (let i = 0; i < scripts.length; i++) {
13
- if (
14
- scripts[i].src.includes('interface.js') &&
15
- scripts[i].hasAttribute('data-config')
16
- ) {
17
- scriptTag = scripts[i];
18
- break;
19
- }
20
- }
21
- if (!scriptTag && scripts.length > 0) {
22
- scriptTag = scripts[scripts.length - 1];
23
- }
24
- }
25
-
26
- const configUrl = scriptTag.getAttribute('data-config');
27
- let config = {};
28
-
29
- if (configUrl) {
30
- try {
31
- const response = await fetch(configUrl);
32
- config = await response.json();
33
- } catch (error) {
34
- return;
35
- }
36
- } else {
 
 
 
 
 
 
 
 
37
  return;
38
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
- // 2) CSS optionnelle
41
- if (config.css_url) {
42
- const linkEl = document.createElement('link');
43
- linkEl.rel = 'stylesheet';
44
- linkEl.href = config.css_url;
45
- document.head.appendChild(linkEl);
46
- }
47
-
48
- // 3) ID d’instance
49
- const instanceId = Math.random().toString(36).substr(2, 8);
50
-
51
- // 4) Aspect ratio
52
- let aspectPercent = '100%';
53
- if (config.aspect) {
54
- if (config.aspect.includes(':')) {
55
- const parts = config.aspect.split(':');
56
- const w = parseFloat(parts[0]);
57
- const h = parseFloat(parts[1]);
58
- if (!isNaN(w) && !isNaN(h) && w > 0) {
59
- aspectPercent = (h / w) * 100 + '%';
60
- }
61
- } else {
62
- const aspectValue = parseFloat(config.aspect);
63
- if (!isNaN(aspectValue) && aspectValue > 0) {
64
- aspectPercent = 100 / aspectValue + '%';
65
- }
66
- }
67
- } else {
68
- const parentContainer = scriptTag.parentNode;
69
- const containerWidth = parentContainer.offsetWidth;
70
- const containerHeight = parentContainer.offsetHeight;
71
- if (containerWidth > 0 && containerHeight > 0) {
72
- aspectPercent = (containerHeight / containerWidth) * 100 + '%';
73
- }
74
- }
75
-
76
- // 5) Conteneur widget
77
- const widgetContainer = document.createElement('div');
78
- widgetContainer.id = 'ply-widget-container-' + instanceId;
79
- widgetContainer.classList.add('ply-widget-container');
80
- widgetContainer.style.height = '0';
81
- widgetContainer.style.paddingBottom = aspectPercent;
82
- widgetContainer.setAttribute('data-original-aspect', aspectPercent);
83
-
84
- const tooltipsButtonHTML = config.tooltips_url
85
- ? `<button id="tooltips-toggle-${instanceId}" class="widget-button tooltips-toggle">⦿</button>`
86
- : '';
87
-
88
- // HTML du widget (IDs spécifiques à l’instance)
89
- widgetContainer.innerHTML = `
90
- <div id="viewer-container-${instanceId}" class="viewer-container">
91
- <div id="progress-dialog-${instanceId}" class="progress-dialog">
92
- <progress id="progress-indicator-${instanceId}" max="100" value="0"></progress>
93
- </div>
94
- <button id="fullscreen-toggle-${instanceId}" class="widget-button fullscreen-toggle">⇱</button>
95
- <button id="help-toggle-${instanceId}" class="widget-button help-toggle">?</button>
96
- <button id="reset-camera-btn-${instanceId}" class="widget-button reset-camera-btn">
97
- <span class="reset-icon">⟲</span>
98
- </button>
99
- ${tooltipsButtonHTML}
100
- <div id="menu-content-${instanceId}" class="menu-content">
101
- <span id="help-close-${instanceId}" class="help-close">×</span>
102
- <div class="help-text"></div>
103
- </div>
104
- </div>
105
-
106
- <div id="tooltip-panel-${instanceId}" class="tooltip-panel" style="display: none;">
107
- <div class="tooltip-content">
108
- <span id="tooltip-close-${instanceId}" class="tooltip-close">×</span>
109
- <div id="tooltip-text-${instanceId}" class="tooltip-text"></div>
110
- <img id="tooltip-image-${instanceId}" class="tooltip-image" src="" alt="" style="display: none;" />
111
- </div>
112
- </div>
113
- `;
114
-
115
- // Insérer dans le DOM
116
- scriptTag.parentNode.appendChild(widgetContainer);
117
-
118
- // 6) Références DOM de l’instance
119
- const viewerContainerElem = document.getElementById('viewer-container-' + instanceId);
120
- const fullscreenToggle = document.getElementById('fullscreen-toggle-' + instanceId);
121
- const helpToggle = document.getElementById('help-toggle-' + instanceId);
122
- const helpCloseBtn = document.getElementById('help-close-' + instanceId);
123
- const resetCameraBtn = document.getElementById('reset-camera-btn-' + instanceId);
124
- const tooltipsToggleBtn = document.getElementById('tooltips-toggle-' + instanceId);
125
- const menuContent = document.getElementById('menu-content-' + instanceId);
126
- const helpTextDiv = menuContent.querySelector('.help-text');
127
-
128
- const tooltipPanel = document.getElementById('tooltip-panel-' + instanceId);
129
- const tooltipTextDiv = document.getElementById('tooltip-text-' + instanceId);
130
- const tooltipImage = document.getElementById('tooltip-image-' + instanceId);
131
- const tooltipCloseBtn = document.getElementById('tooltip-close-' + instanceId);
132
-
133
- // 7) Aide / textes
134
- const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
135
- const isMobile = isIOS || /Android/i.test(navigator.userAgent);
136
-
137
- const tooltipInstruction = config.tooltips_url
138
- ? '- ⦿ : annotations.<br>'
139
- : '';
140
-
141
- if (isMobile) {
142
- helpTextDiv.innerHTML =
143
- "- Déplacement avec deux doigts.<br>" +
144
- "- Rotation avec un doigt.<br>" +
145
- '- Zoom en pinçant avec deux doigts.<br>' +
146
- tooltipInstruction +
147
- '- ⟲ réinitialisation de la caméra.<br>' +
148
- '- ⇱ plein écran.<br>';
149
- } else {
150
- helpTextDiv.innerHTML =
151
- '- Rotation avec le clic droit ou maj + ←↑↓→<br>' +
152
- '- Zoom avec la molette ou ctrl + ↑↓<br>' +
153
- '- Déplacement avec le clic gauche ou ←↑↓→<br>' +
154
- tooltipInstruction +
155
- '- ⟲ réinitialisation de la caméra.<br>' +
156
- '- ⇱ plein écran.<br>';
157
  }
158
 
159
- // 8) Sizing dynamique du panneau d’aide
160
- function setMenuContentMaxSize() {
161
- if (!isMobile) {
162
- menuContent.style.maxWidth = '';
163
- menuContent.style.maxHeight = '';
164
- menuContent.style.width = '';
165
- menuContent.style.height = '';
166
- menuContent.style.overflowY = '';
167
- menuContent.style.overflowX = '';
168
- return;
169
- }
170
- const parent = viewerContainerElem;
171
- if (parent) {
172
- const vw = parent.offsetWidth;
173
- const vh = parent.offsetHeight;
174
- if (vw && vh) {
175
- menuContent.style.maxWidth = Math.round(vw * 0.8) + 'px';
176
- menuContent.style.maxHeight = Math.round(vh * 0.8) + 'px';
177
- menuContent.style.width = '';
178
- menuContent.style.height = '';
179
- menuContent.style.overflowY = 'auto';
180
- menuContent.style.overflowX = 'auto';
181
- } else {
182
- menuContent.style.maxWidth = '80vw';
183
- menuContent.style.maxHeight = '80vh';
184
- menuContent.style.overflowY = 'auto';
185
- menuContent.style.overflowX = 'auto';
186
- }
187
- }
188
  }
 
189
 
190
- setMenuContentMaxSize();
191
- window.addEventListener('resize', setMenuContentMaxSize);
192
- document.addEventListener('fullscreenchange', setMenuContentMaxSize);
193
- window.addEventListener('orientationchange', setMenuContentMaxSize);
194
-
195
- // 9) Aide visible par défaut
196
- menuContent.style.display = 'block';
197
- viewerContainerElem.style.display = 'block';
198
 
199
- // 10) Gestion du panneau tooltips
200
- let dragHide = null;
201
 
202
- function hideTooltipPanel() {
203
- if (dragHide) {
204
- viewerContainerElem.removeEventListener('pointermove', dragHide);
205
- dragHide = null;
 
206
  }
207
- tooltipPanel.style.display = 'none';
208
- }
209
 
210
- function hideHelpPanel() {
211
- menuContent.style.display = 'none';
212
- }
213
 
214
- // 11) Charger viewer.js (avec cache-busting par instance)
215
- let viewerModule;
216
- try {
217
- const viewerUrl = `https://mikafil-viewer-sgos.static.hf.space/viewer.js?inst=${instanceId}`;
218
- viewerModule = await import(viewerUrl);
219
- await viewerModule.initializeViewer(config, instanceId);
220
- } catch (err) {
221
- return;
222
- }
223
 
224
- const canvasId = 'canvas-' + instanceId;
225
- const canvasEl = document.getElementById(canvasId);
226
 
227
- // 12) Bouton tooltips : cacher si URL non valide
228
- if (tooltipsToggleBtn) {
229
- if (!config.tooltips_url) {
230
- tooltipsToggleBtn.style.display = 'none';
231
- } else {
232
- fetch(config.tooltips_url)
233
- .then((resp) => {
234
- if (!resp.ok) tooltipsToggleBtn.style.display = 'none';
235
- })
236
- .catch(() => {
237
- tooltipsToggleBtn.style.display = 'none';
238
- });
239
- }
240
- }
241
 
242
- // 13) Interactions locales / tooltips
243
- if (canvasEl) {
244
- canvasEl.addEventListener('wheel', hideTooltipPanel, { passive: true });
245
- }
246
 
247
- document.addEventListener('tooltip-selected', (evt) => {
248
- // Toujours afficher le panneau, annuler un hide différé si présent
249
- if (dragHide) {
250
- viewerContainerElem.removeEventListener('pointermove', dragHide);
251
- dragHide = null;
252
- }
253
-
254
- const { title, description, imgUrl } = evt.detail || {};
255
- tooltipTextDiv.innerHTML = `<strong>${title || ''}</strong><br>${description || ''}`;
256
 
257
- // Forcer un repaint : nettoyer src avant de réassigner
258
- tooltipImage.style.display = 'none';
259
- tooltipImage.src = '';
260
-
261
- if (imgUrl) {
262
- tooltipImage.onload = () => {
263
- tooltipImage.style.display = 'block';
264
- };
265
- tooltipImage.src = imgUrl;
266
- } else {
267
- tooltipImage.style.display = 'none';
268
- }
269
 
270
- tooltipPanel.style.display = 'flex';
271
-
272
- // Fermer en cas de drag (après un petit délai pour éviter un flicker)
273
- setTimeout(() => {
274
- dragHide = (e) => {
275
- if (
276
- (e.pointerType === 'mouse' && e.buttons !== 0) ||
277
- e.pointerType === 'touch'
278
- ) {
279
- hideTooltipPanel();
280
- }
281
- };
282
- viewerContainerElem.addEventListener('pointermove', dragHide);
283
- }, 100);
284
- });
285
 
286
- // 14) Fullscreen
287
- let isFullscreen = false;
288
- let savedState = null;
289
-
290
- function saveCurrentState() {
291
- if (isFullscreen) return;
292
- const originalAspect =
293
- widgetContainer.getAttribute('data-original-aspect') || aspectPercent;
294
-
295
- savedState = {
296
- widget: {
297
- position: widgetContainer.style.position,
298
- top: widgetContainer.style.top,
299
- left: widgetContainer.style.left,
300
- width: widgetContainer.style.width,
301
- height: widgetContainer.style.height,
302
- maxWidth: widgetContainer.style.maxWidth,
303
- maxHeight: widgetContainer.style.maxHeight,
304
- paddingBottom: widgetContainer.style.paddingBottom || originalAspect,
305
- margin: widgetContainer.style.margin
306
- },
307
- viewer: {
308
- borderRadius: viewerContainerElem.style.borderRadius,
309
- border: viewerContainerElem.style.border
310
  }
311
- };
312
- }
313
-
314
- function restoreOriginalStyles() {
315
- if (!savedState) return;
316
- const aspectToUse = savedState.widget.paddingBottom;
317
-
318
- widgetContainer.style.position = savedState.widget.position || '';
319
- widgetContainer.style.top = savedState.widget.top || '';
320
- widgetContainer.style.left = savedState.widget.left || '';
321
- widgetContainer.style.width = '100%';
322
- widgetContainer.style.height = '0';
323
- widgetContainer.style.maxWidth = savedState.widget.maxWidth || '';
324
- widgetContainer.style.maxHeight = savedState.widget.maxHeight || '';
325
- widgetContainer.style.paddingBottom = aspectToUse;
326
- widgetContainer.style.margin = savedState.widget.margin || '';
327
- widgetContainer.classList.remove('fake-fullscreen');
328
-
329
- viewerContainerElem.style.position = 'absolute';
330
- viewerContainerElem.style.top = '0';
331
- viewerContainerElem.style.left = '0';
332
- viewerContainerElem.style.right = '0';
333
- viewerContainerElem.style.bottom = '0';
334
- viewerContainerElem.style.width = '100%';
335
- viewerContainerElem.style.height = '100%';
336
- viewerContainerElem.style.borderRadius = savedState.viewer.borderRadius || '';
337
- viewerContainerElem.style.border = savedState.viewer.border || '';
338
-
339
- if (viewerModule.app) {
340
- viewerModule.app.resizeCanvas(
341
- viewerContainerElem.clientWidth,
342
- viewerContainerElem.clientHeight
343
- );
344
  }
345
 
346
- if (fullscreenToggle) fullscreenToggle.textContent = '⇱';
347
-
348
- savedState = null;
349
- setMenuContentMaxSize();
350
- }
 
351
 
352
- function applyFullscreenStyles() {
353
- widgetContainer.style.position = 'fixed';
354
- widgetContainer.style.top = '0';
355
- widgetContainer.style.left = '0';
356
- widgetContainer.style.width = '100vw';
357
- widgetContainer.style.height = '100vh';
358
- widgetContainer.style.maxWidth = '100vw';
359
- widgetContainer.style.maxHeight = '100vh';
360
- widgetContainer.style.paddingBottom = '0';
361
- widgetContainer.style.margin = '0';
362
- widgetContainer.style.border = 'none';
363
- widgetContainer.style.borderRadius = '0';
364
-
365
- viewerContainerElem.style.width = '100%';
366
- viewerContainerElem.style.height = '100%';
367
- viewerContainerElem.style.borderRadius = '0';
368
- viewerContainerElem.style.border = 'none';
369
-
370
- if (viewerModule.app) {
371
- viewerModule.app.resizeCanvas(window.innerWidth, window.innerHeight);
372
  }
 
373
 
374
- if (fullscreenToggle) fullscreenToggle.textContent = '⇲';
375
- isFullscreen = true;
376
- setMenuContentMaxSize();
 
 
 
377
  }
378
 
379
- function enterFullscreen() {
380
- if (!savedState) saveCurrentState();
381
-
382
- if (isIOS) {
383
- applyFullscreenStyles();
384
- widgetContainer.classList.add('fake-fullscreen');
385
- } else if (widgetContainer.requestFullscreen) {
386
- widgetContainer
387
- .requestFullscreen()
388
- .then(applyFullscreenStyles)
389
- .catch(() => {
390
- applyFullscreenStyles();
391
- widgetContainer.classList.add('fake-fullscreen');
392
- });
393
- } else {
394
- applyFullscreenStyles();
395
- widgetContainer.classList.add('fake-fullscreen');
396
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
397
  }
398
 
399
- function exitFullscreen() {
400
- if (document.fullscreenElement === widgetContainer && document.exitFullscreen) {
401
- document.exitFullscreen().catch(() => {});
402
- }
403
- widgetContainer.classList.remove('fake-fullscreen');
404
- restoreOriginalStyles();
405
- isFullscreen = false;
406
- if (fullscreenToggle) fullscreenToggle.textContent = '⇱';
407
- setMenuContentMaxSize();
408
- }
409
-
410
- fullscreenToggle.addEventListener('click', () => {
411
- hideTooltipPanel();
412
- isFullscreen ? exitFullscreen() : enterFullscreen();
413
- });
414
-
415
- document.addEventListener('fullscreenchange', () => {
416
- if (!document.fullscreenElement && isFullscreen) {
417
- isFullscreen = false;
418
- restoreOriginalStyles();
419
- if (fullscreenToggle) fullscreenToggle.textContent = '⇱';
420
- } else if (document.fullscreenElement === widgetContainer) {
421
- if (fullscreenToggle) fullscreenToggle.textContent = '⇲';
422
- }
423
- setMenuContentMaxSize();
424
- });
425
-
426
- // 15) Aide / boutons
427
- helpToggle.addEventListener('click', (e) => {
428
- hideTooltipPanel();
429
- e.stopPropagation();
430
- if (menuContent.style.display === 'block') {
431
- menuContent.style.display = 'none';
432
  } else {
433
- menuContent.style.display = 'block';
434
- setMenuContentMaxSize();
 
 
 
 
 
 
 
 
 
 
435
  }
436
- });
437
 
438
- helpCloseBtn.addEventListener('click', hideHelpPanel);
 
 
 
 
439
 
440
- resetCameraBtn.addEventListener('click', () => {
441
- hideTooltipPanel();
442
- if (viewerModule.resetViewerCamera) {
443
- viewerModule.resetViewerCamera();
444
- }
445
- });
446
-
447
- if (tooltipsToggleBtn) {
448
- let tooltipsVisible = !!config.showTooltipsDefault;
449
- tooltipsToggleBtn.style.opacity = tooltipsVisible ? '1' : '0.5';
450
- tooltipsToggleBtn.addEventListener('click', () => {
451
- hideTooltipPanel();
452
- tooltipsVisible = !tooltipsVisible;
453
- tooltipsToggleBtn.style.opacity = tooltipsVisible ? '1' : '0.5';
454
- document.dispatchEvent(
455
- new CustomEvent('toggle-tooltips', { detail: { visible: tooltipsVisible } })
456
- );
457
- });
458
- }
459
-
460
- tooltipCloseBtn.addEventListener('click', hideTooltipPanel);
461
 
462
- // 16) Échappement / resize
463
- document.addEventListener('keydown', (e) => {
464
- if ((e.key === 'Escape' || e.key === 'Esc') && isFullscreen) {
465
- exitFullscreen();
466
  }
467
- });
468
 
469
- window.addEventListener('resize', () => {
470
- if (viewerModule.app) {
471
- if (isFullscreen) {
472
- viewerModule.app.resizeCanvas(window.innerWidth, window.innerHeight);
473
- } else {
474
- viewerModule.app.resizeCanvas(
475
- viewerContainerElem.clientWidth,
476
- viewerContainerElem.clientHeight
477
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
  }
479
  }
480
- setMenuContentMaxSize();
481
- });
482
 
483
- // 17) Init par défaut
484
- setTimeout(() => {
485
- // Sauvegarder l’état non-fullscreen
486
- saveCurrentState();
487
- // Propager l’état par défaut des tooltips
488
- document.dispatchEvent(
489
- new CustomEvent('toggle-tooltips', { detail: { visible: !!config.showTooltipsDefault } })
490
- );
491
- setMenuContentMaxSize();
492
- }, 200);
493
- })();
 
1
+ // tooltips.js
2
+
3
+ /**
4
+ * initializeTooltips(options)
5
+ *
6
+ * - options.app: the PlayCanvas AppBase instance
7
+ * - options.cameraEntity: PlayCanvas camera Entity (utilisant le script orbitCamera)
8
+ * - options.modelEntity: the main model entity (for relative positioning, optional)
9
+ * - options.tooltipsUrl: URL to fetch JSON array of tooltip definitions
10
+ * - options.defaultVisible: boolean: whether tooltips are visible initially
11
+ * - options.moveDuration: number (seconds) for smooth camera move to selected tooltip
12
+ *
13
+ * JSON attendu pour chaque tooltip :
14
+ * {
15
+ * x, y, z, // position du tooltip (obligatoire)
16
+ * title, description, imgUrl, // infos UI (optionnelles)
17
+ * camX, camY, camZ // position de caméra cible (optionnelles)
18
+ * }
19
+ *
20
+ * Comportement :
21
+ * - Si camX/camY/camZ sont fournis, la caméra se déplacera exactement
22
+ * vers (camX, camY, camZ) et s'orientera pour regarder le tooltip.
23
+ * - Sinon, on conserve l'ancien comportement : la caméra orbite vers le tooltip
24
+ * avec une distance calculée (zoom minimum + taille du tooltip).
25
+ */
26
+ export async function initializeTooltips(options) {
27
+ const {
28
+ app,
29
+ cameraEntity,
30
+ modelEntity, // non utilisé directement ici mais conservé pour compat
31
+ tooltipsUrl,
32
+ defaultVisible,
33
+ moveDuration = 0.6
34
+ } = options;
35
+
36
+ if (!app || !cameraEntity || !tooltipsUrl) return;
37
+
38
+ // --- Chargement du JSON de tooltips ---
39
+ let tooltipsData;
40
+ try {
41
+ const resp = await fetch(tooltipsUrl);
42
+ tooltipsData = await resp.json();
43
+ } catch (e) {
44
+ // Échec du fetch/parse JSON -> on abandonne proprement
45
  return;
46
  }
47
+ if (!Array.isArray(tooltipsData)) return;
48
+
49
+ const tooltipEntities = [];
50
+
51
+ // --- Matériau des sphères (tooltips) ---
52
+ const mat = new pc.StandardMaterial();
53
+ mat.diffuse = new pc.Color(1, 0.8, 0);
54
+ mat.specular = new pc.Color(1, 1, 1);
55
+ mat.shininess = 20;
56
+ mat.emissive = new pc.Color(0.85, 0.85, 0.85);
57
+ mat.emissiveIntensity = 1;
58
+ mat.useLighting = false;
59
+ mat.update();
60
+
61
+ // --- Création des entités sphères pour chaque tooltip ---
62
+ for (let i = 0; i < tooltipsData.length; i++) {
63
+ const tt = tooltipsData[i];
64
+ const { x, y, z, title, description, imgUrl, camX, camY, camZ } = tt;
65
+
66
+ const sphere = new pc.Entity("tooltip-" + i);
67
+ sphere.addComponent("model", { type: "sphere" });
68
+ sphere.model.material = mat;
69
+
70
+ // Taille par défaut des "pins"
71
+ sphere.setLocalScale(0.05, 0.05, 0.05);
72
+ sphere.setLocalPosition(x, y, z);
73
+
74
+ // On stocke toutes les infos utiles sur l'entité
75
+ sphere.tooltipData = {
76
+ title,
77
+ description,
78
+ imgUrl,
79
+ // Nouvelle partie : coordonnées de caméra cibles (optionnelles)
80
+ camTarget: (Number.isFinite(camX) && Number.isFinite(camY) && Number.isFinite(camZ))
81
+ ? new pc.Vec3(camX, camY, camZ)
82
+ : null
83
+ };
84
 
85
+ app.root.addChild(sphere);
86
+ tooltipEntities.push(sphere);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  }
88
 
89
+ // --- Gestion de la visibilité des tooltips ---
90
+ function setTooltipsVisibility(visible) {
91
+ tooltipEntities.forEach(ent => { ent.enabled = visible; });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  }
93
+ setTooltipsVisibility(!!defaultVisible);
94
 
95
+ // Écouteur externe (ex. UI HTML) pour afficher/masquer les tooltips
96
+ document.addEventListener("toggle-tooltips", (evt) => {
97
+ const { visible } = evt.detail;
98
+ setTooltipsVisibility(!!visible);
99
+ });
 
 
 
100
 
101
+ // --- Picking (détection de clic sur un tooltip) ---
102
+ let currentTween = null;
103
 
104
+ app.mouse.on(pc.EVENT_MOUSEDOWN, (event) => {
105
+ // Si une interpolation est en cours, on l'arrête proprement
106
+ if (currentTween) {
107
+ app.off("update", currentTween);
108
+ currentTween = null;
109
  }
 
 
110
 
111
+ const x = event.x, y = event.y;
112
+ const from = new pc.Vec3(), to = new pc.Vec3();
113
+ const camera = cameraEntity.camera;
114
 
115
+ // Ray picking écran -> monde
116
+ camera.screenToWorld(x, y, camera.nearClip, from);
117
+ camera.screenToWorld(x, y, camera.farClip, to);
 
 
 
 
 
 
118
 
119
+ const dir = new pc.Vec3().sub2(to, from).normalize();
 
120
 
121
+ let closestT = Infinity;
122
+ let pickedEntity = null;
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
+ // Test d'intersection rayon/sphère (simple et suffisant ici)
125
+ for (const ent of tooltipEntities) {
126
+ if (!ent.enabled) continue;
 
127
 
128
+ const center = ent.getPosition();
129
+ const worldRadius = 0.5 * ent.getLocalScale().x;
 
 
 
 
 
 
 
130
 
131
+ const oc = new pc.Vec3().sub2(center, from);
132
+ const tca = oc.dot(dir);
133
+ if (tca < 0) continue;
 
 
 
 
 
 
 
 
 
134
 
135
+ const d2 = oc.lengthSq() - (tca * tca);
136
+ if (d2 > worldRadius * worldRadius) continue;
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
+ const thc = Math.sqrt(worldRadius * worldRadius - d2);
139
+ const t0 = tca - thc;
140
+ if (t0 < closestT && t0 >= 0) {
141
+ closestT = t0;
142
+ pickedEntity = ent;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  }
145
 
146
+ if (pickedEntity) {
147
+ // Notifier l'UI (titre, description, image)
148
+ const { title, description, imgUrl } = pickedEntity.tooltipData;
149
+ document.dispatchEvent(new CustomEvent("tooltip-selected", {
150
+ detail: { title, description, imgUrl }
151
+ }));
152
 
153
+ // Si on a une position caméra cible, on l'utilise
154
+ const desiredCamPos = pickedEntity.tooltipData.camTarget;
155
+ tweenCameraToTooltip(pickedEntity, moveDuration, desiredCamPos);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  }
157
+ });
158
 
159
+ // --- Helpers math/angles ---
160
+ function shortestAngleDiff(target, current) {
161
+ // Retourne l'écart angulaire [-180, 180] pour interpoler par le plus court chemin
162
+ let delta = target - current;
163
+ delta = ((delta + 180) % 360 + 360) % 360 - 180;
164
+ return delta;
165
  }
166
 
167
+ /**
168
+ * Calcule {yaw, pitch, distance} pour une caméra à cameraPos regardant pivotPos,
169
+ * selon la convention de l'orbitCamera.
170
+ */
171
+ function computeOrbitFromPositions(cameraPos, pivotPos) {
172
+ const tempEnt = new pc.Entity();
173
+ tempEnt.setPosition(cameraPos);
174
+ tempEnt.lookAt(pivotPos);
175
+
176
+ const rotation = tempEnt.getRotation();
177
+
178
+ // Direction "forward" (de la caméra vers le pivot)
179
+ const forward = new pc.Vec3();
180
+ rotation.transformVector(pc.Vec3.FORWARD, forward);
181
+
182
+ // Yaw : rotation horizontale
183
+ const rawYaw = Math.atan2(-forward.x, -forward.z) * pc.math.RAD_TO_DEG;
184
+
185
+ // Pitch : on retire d'abord l'influence du yaw pour isoler la composante verticale
186
+ const yawQuat = new pc.Quat().setFromEulerAngles(0, -rawYaw, 0);
187
+ const rotNoYaw = new pc.Quat().mul2(yawQuat, rotation);
188
+ const fNoYaw = new pc.Vec3();
189
+ rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw);
190
+ const rawPitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
191
+
192
+ // Distance : norme du vecteur pivot - caméra
193
+ const toPivot = new pc.Vec3().sub2(pivotPos, cameraPos);
194
+ const dist = toPivot.length();
195
+
196
+ tempEnt.destroy();
197
+ return { yaw: rawYaw, pitch: rawPitch, distance: dist };
198
  }
199
 
200
+ /**
201
+ * Animation caméra vers un tooltip.
202
+ * - tooltipEnt: entité du tooltip cliqué
203
+ * - duration: durée de l'interpolation (s)
204
+ * - overrideCamWorldPos (pc.Vec3|null): si fourni, la caméra ira EXACTEMENT à cette position
205
+ * tout en regardant le tooltip.
206
+ */
207
+ function tweenCameraToTooltip(tooltipEnt, duration, overrideCamWorldPos = null) {
208
+ const orbitCam = cameraEntity.script && cameraEntity.script.orbitCamera;
209
+ if (!orbitCam) return;
210
+
211
+ const targetPos = tooltipEnt.getPosition().clone();
212
+
213
+ // État initial (depuis l'orbitCamera)
214
+ const startPivot = orbitCam.pivotPoint.clone();
215
+ const startYaw = orbitCam._yaw;
216
+ const startPitch = orbitCam._pitch;
217
+ const startDist = orbitCam._distance;
218
+
219
+ // Valeurs finales à déterminer
220
+ let endPivot = targetPos.clone();
221
+ let endYaw, endPitch, endDist;
222
+
223
+ if (overrideCamWorldPos) {
224
+ // --- Nouveau mode : position caméra imposée par le JSON ---
225
+ // On calcule l'orbite (yaw/pitch/dist) qui correspond exactement à cette position
226
+ const { yaw, pitch, distance } = computeOrbitFromPositions(overrideCamWorldPos, targetPos);
227
+
228
+ // Interpolation par le plus court chemin depuis l'état courant
229
+ endYaw = startYaw + shortestAngleDiff(yaw, startYaw);
230
+ endPitch = startPitch + shortestAngleDiff(pitch, startPitch);
231
+ endDist = distance;
 
232
  } else {
233
+ // --- Comportement historique (aucune camX/Y/Z fournie) ---
234
+ const worldRadius = 0.5 * tooltipEnt.getLocalScale().x;
235
+ const minZoom = orbitCam.distanceMin || 0.1;
236
+ const desiredDistance = Math.max(minZoom * 1.2, worldRadius * 4);
237
+
238
+ // On garde la position caméra actuelle comme point de départ pour calculer les angles
239
+ const camWorldPos = cameraEntity.getPosition().clone();
240
+ const { yaw, pitch } = computeOrbitFromPositions(camWorldPos, targetPos);
241
+
242
+ endYaw = startYaw + shortestAngleDiff(yaw, startYaw);
243
+ endPitch = startPitch + shortestAngleDiff(pitch, startPitch);
244
+ endDist = desiredDistance;
245
  }
 
246
 
247
+ // Sauvegarde des origines pour l'interpolation
248
+ const orgPivot = startPivot.clone();
249
+ const orgYaw = startYaw;
250
+ const orgPitch = startPitch;
251
+ const orgDist = startDist;
252
 
253
+ let elapsed = 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
 
255
+ // Si une interpolation était déjà en cours, on la débranche
256
+ if (currentTween) {
257
+ app.off("update", currentTween);
258
+ currentTween = null;
259
  }
 
260
 
261
+ // --- Lerp frame-by-frame ---
262
+ function lerpUpdate(dt) {
263
+ elapsed += dt;
264
+ const t = Math.min(elapsed / duration, 1);
265
+
266
+ // Pivot (regard) vers le tooltip
267
+ const newPivot = new pc.Vec3().lerp(orgPivot, endPivot, t);
268
+ orbitCam.pivotPoint.copy(newPivot);
269
+
270
+ // Yaw, Pitch, Distance (on met aussi les "target" pour rester cohérent avec le script orbitCamera)
271
+ const newYaw = pc.math.lerp(orgYaw, endYaw, t);
272
+ const newPitch = pc.math.lerp(orgPitch, endPitch, t);
273
+ const newDist = pc.math.lerp(orgDist, endDist, t);
274
+
275
+ orbitCam._targetYaw = newYaw;
276
+ orbitCam._yaw = newYaw;
277
+ orbitCam._targetPitch = newPitch;
278
+ orbitCam._pitch = newPitch;
279
+ orbitCam._targetDistance = newDist;
280
+ orbitCam._distance = newDist;
281
+
282
+ // Mise à jour de la position monde de la caméra à partir des paramètres d'orbite
283
+ orbitCam._updatePosition();
284
+
285
+ if (t >= 1) {
286
+ app.off("update", lerpUpdate);
287
+ currentTween = null;
288
  }
289
  }
 
 
290
 
291
+ currentTween = lerpUpdate;
292
+ app.on("update", lerpUpdate);
293
+ }
294
+ }