MikaFil commited on
Commit
56902d9
·
verified ·
1 Parent(s): 3af5ba5

Update tooltips.js

Browse files
Files changed (1) hide show
  1. tooltips.js +136 -35
tooltips.js CHANGED
@@ -1,4 +1,121 @@
1
  // tooltips.js
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  /**
4
  * initializeTooltips(options)
@@ -6,42 +123,30 @@
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;
@@ -71,22 +176,22 @@ export async function initializeTooltips(options) {
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
  }
@@ -94,7 +199,7 @@ export async function initializeTooltips(options) {
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
 
@@ -222,10 +327,7 @@ export async function initializeTooltips(options) {
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;
@@ -235,7 +337,6 @@ export async function initializeTooltips(options) {
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
 
 
1
  // tooltips.js
2
+ //
3
+ // - Charge le JSON depuis GitHub Raw (par défaut) en utilisant un GET conditionnel (If-None-Match),
4
+ // ce qui évite les problèmes Safari/iOS liés aux requêtes HEAD et maximise la compatibilité CORS.
5
+ // - Met en cache la dernière version valide (localStorage si possible, sinon repli mémoire).
6
+ // - En cas d'erreur réseau/parse, retombe sur la dernière copie valide, sinon tente un GET avec cache-buster.
7
+ // - Supporte camX/camY/camZ pour placer la caméra exactement à ces coordonnées et regarder le tooltip.
8
+ //
9
+ // Utilisation minimale :
10
+ // initializeTooltips({ app, cameraEntity, defaultVisible: true });
11
+ //
12
+ // Options utiles :
13
+ // - tooltipsUrl : pour surcharger l'URL (défaut : DEFAULT_TOOLTIPS_URL)
14
+ // - cacheMode : 'default' | 'no-cache' | 'reload' | 'no-store' (défaut : 'no-cache' afin de revalider proprement)
15
+ // - moveDuration: durée (s) de l’animation caméra
16
+
17
+ const DEFAULT_TOOLTIPS_URL =
18
+ "https://raw.githubusercontent.com/mika-fi/sgos_dataset/main/exemples/baleine/tooltips.json";
19
+
20
+ // --- Stockage sûr (localStorage protégé + repli mémoire pour iOS privé / ITP / iframes) ---
21
+ const __memStore = Object.create(null);
22
+ function safeGetItem(key) {
23
+ try { return localStorage.getItem(key); } catch (_) { return __memStore[key] ?? null; }
24
+ }
25
+ function safeSetItem(key, value) {
26
+ try { localStorage.setItem(key, value); } catch (_) { __memStore[key] = value; }
27
+ }
28
+ function safeRemoveItem(key) {
29
+ try { localStorage.removeItem(key); } catch (_) { delete __memStore[key]; }
30
+ }
31
+
32
+ // --- Parse JSON sûr (évite de crasher le flux en cas de contenu invalide) ---
33
+ async function safeParseJson(resp) {
34
+ const text = await resp.text();
35
+ try {
36
+ return JSON.parse(text);
37
+ } catch (e) {
38
+ // JSON invalide (rare sur GitHub Raw, mais mieux vaut prévenir)
39
+ throw new Error("Invalid JSON");
40
+ }
41
+ }
42
+
43
+ /**
44
+ * fetchWithETag(url, { cacheMode, bustOnError })
45
+ * - GET conditionnel avec If-None-Match pour obtenir 304 si pas de changement
46
+ * - Conserve ETag + data localement
47
+ * - Fallback : sert la dernière copie valide, sinon GET "no-store" avec cache-buster
48
+ * - Évite les HEAD (souvent source d'ennuis avec Safari/CORS/redirections)
49
+ */
50
+ async function fetchWithETag(url, { cacheMode = "no-cache", bustOnError = true } = {}) {
51
+ const LS_KEY_DATA = `tooltips:data:${url}`;
52
+ const LS_KEY_ETAG = `tooltips:etag:${url}`;
53
+ const prevEtag = safeGetItem(LS_KEY_ETAG);
54
+ const prevDataStr = safeGetItem(LS_KEY_DATA);
55
+ const prevData = prevDataStr ? (() => { try { return JSON.parse(prevDataStr); } catch { return null; } })() : null;
56
+
57
+ try {
58
+ // 1) GET conditionnel (If-None-Match) pour maximiser la compat safari/ios et CDN
59
+ const headers = {};
60
+ if (prevEtag) headers["If-None-Match"] = prevEtag;
61
+
62
+ const resp = await fetch(url, {
63
+ method: "GET",
64
+ cache: cacheMode, // 'no-cache' pour revalidation, 'no-store' pour refetch strict
65
+ mode: "cors",
66
+ redirect: "follow",
67
+ credentials: "omit",
68
+ headers
69
+ });
70
+
71
+ // 304 = pas de changement -> réutilise la dernière copie locale
72
+ if (resp.status === 304 && prevData) {
73
+ return prevData;
74
+ }
75
+
76
+ if (!resp.ok) {
77
+ if (prevData) return prevData;
78
+ throw new Error(`HTTP ${resp.status}`);
79
+ }
80
+
81
+ // 2) Nouvelle version téléchargée
82
+ const json = await safeParseJson(resp);
83
+
84
+ // 3) Enregistre ETag + data
85
+ const newEtag = resp.headers.get("ETag") || resp.headers.get("etag") || null;
86
+ if (newEtag) safeSetItem(LS_KEY_ETAG, newEtag);
87
+ safeSetItem(LS_KEY_DATA, JSON.stringify(json));
88
+
89
+ return json;
90
+ } catch (err) {
91
+ // 4) Fallback : si échec réseau/parse, renvoie la dernière copie locale si dispo
92
+ if (prevData) return prevData;
93
+
94
+ // 5) Ultime recours : cache-buster no-store (utile contre caches "têtus" de Safari/iOS)
95
+ if (bustOnError) {
96
+ const sep = url.includes("?") ? "&" : "?";
97
+ const bustedUrl = `${url}${sep}t=${Date.now()}`;
98
+ try {
99
+ const bustResp = await fetch(bustedUrl, {
100
+ method: "GET",
101
+ cache: "no-store",
102
+ mode: "cors",
103
+ redirect: "follow",
104
+ credentials: "omit"
105
+ });
106
+ if (!bustResp.ok) throw new Error(`HTTP ${bustResp.status}`);
107
+ const fresh = await safeParseJson(bustResp);
108
+ // sauvegarde sans ETag (inconnu sur la requête bustée)
109
+ safeSetItem(LS_KEY_DATA, JSON.stringify(fresh));
110
+ return fresh;
111
+ } catch (e2) {
112
+ throw err; // On remonte l'erreur d'origine si tout échoue
113
+ }
114
+ }
115
+
116
+ throw err;
117
+ }
118
+ }
119
 
120
  /**
121
  * initializeTooltips(options)
 
123
  * - options.app: the PlayCanvas AppBase instance
124
  * - options.cameraEntity: PlayCanvas camera Entity (utilisant le script orbitCamera)
125
  * - options.modelEntity: the main model entity (for relative positioning, optional)
126
+ * - options.tooltipsUrl: URL du JSON (optionnel, défaut = DEFAULT_TOOLTIPS_URL)
127
+ * - options.defaultVisible: booléen : tooltips visibles au démarrage
128
+ * - options.moveDuration: durée (s) de l’animation caméra (défaut 0.6)
129
+ * - options.cacheMode: 'default' | 'no-cache' | 'reload' | 'no-store' (défaut 'no-cache' → revalidation correcte)
 
 
 
 
 
 
 
 
 
 
 
 
130
  */
131
  export async function initializeTooltips(options) {
132
  const {
133
  app,
134
  cameraEntity,
135
+ modelEntity, // conservé pour compat
136
+ tooltipsUrl = DEFAULT_TOOLTIPS_URL,
137
  defaultVisible,
138
+ moveDuration = 0.6,
139
+ cacheMode = "no-cache"
140
+ } = options || {};
141
 
142
  if (!app || !cameraEntity || !tooltipsUrl) return;
143
 
144
+ // --- Chargement robuste des tooltips (dernière version + fallback) ---
145
  let tooltipsData;
146
  try {
147
+ tooltipsData = await fetchWithETag(tooltipsUrl, { cacheMode, bustOnError: true });
 
148
  } catch (e) {
149
+ // Aucune donnée exploitable -> on abandonne proprement
150
  return;
151
  }
152
  if (!Array.isArray(tooltipsData)) return;
 
176
  sphere.setLocalScale(0.05, 0.05, 0.05);
177
  sphere.setLocalPosition(x, y, z);
178
 
179
+ // Infos du tooltip (UI + cible caméra optionnelle)
180
  sphere.tooltipData = {
181
  title,
182
  description,
183
  imgUrl,
184
+ camTarget:
185
+ Number.isFinite(camX) && Number.isFinite(camY) && Number.isFinite(camZ)
186
+ ? new pc.Vec3(camX, camY, camZ)
187
+ : null
188
  };
189
 
190
  app.root.addChild(sphere);
191
  tooltipEntities.push(sphere);
192
  }
193
 
194
+ // --- Visibilité initiale + contrôle externe ---
195
  function setTooltipsVisibility(visible) {
196
  tooltipEntities.forEach(ent => { ent.enabled = visible; });
197
  }
 
199
 
200
  // Écouteur externe (ex. UI HTML) pour afficher/masquer les tooltips
201
  document.addEventListener("toggle-tooltips", (evt) => {
202
+ const { visible } = evt.detail || {};
203
  setTooltipsVisibility(!!visible);
204
  });
205
 
 
327
 
328
  if (overrideCamWorldPos) {
329
  // --- Nouveau mode : position caméra imposée par le JSON ---
 
330
  const { yaw, pitch, distance } = computeOrbitFromPositions(overrideCamWorldPos, targetPos);
 
 
331
  endYaw = startYaw + shortestAngleDiff(yaw, startYaw);
332
  endPitch = startPitch + shortestAngleDiff(pitch, startPitch);
333
  endDist = distance;
 
337
  const minZoom = orbitCam.distanceMin || 0.1;
338
  const desiredDistance = Math.max(minZoom * 1.2, worldRadius * 4);
339
 
 
340
  const camWorldPos = cameraEntity.getPosition().clone();
341
  const { yaw, pitch } = computeOrbitFromPositions(camWorldPos, targetPos);
342