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

Update interface.js

Browse files
Files changed (1) hide show
  1. interface.js +417 -257
interface.js CHANGED
@@ -1,294 +1,454 @@
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
- }
 
 
 
 
1
+ // interface.js
2
+ // ==============================
3
+
4
+ const currentScriptTag = document.currentScript;
5
+
6
+ (async function() {
7
+ // 1. Locate the <script> and read data-config
8
+ let scriptTag = currentScriptTag;
9
+ if (!scriptTag) {
10
+ const scripts = document.getElementsByTagName('script');
11
+ for (let i = 0; i < scripts.length; i++) {
12
+ if (scripts[i].src.includes('interface.js') && scripts[i].hasAttribute('data-config')) {
13
+ scriptTag = scripts[i];
14
+ break;
15
+ }
16
+ }
17
+ if (!scriptTag && scripts.length > 0) {
18
+ scriptTag = scripts[scripts.length - 1];
19
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
+ const configUrl = scriptTag.getAttribute('data-config');
23
+ let config = {};
24
+ if (configUrl) {
25
+ try {
26
+ const response = await fetch(configUrl);
27
+ config = await response.json();
28
+ } catch (error) {
29
+ console.error("Could not load config: " + error);
30
+ return;
31
+ }
32
+ } else {
33
+ console.error("No configUrl found for viewer");
34
+ return;
35
  }
36
 
37
+ // 2. Inject CSS if specified
38
+ if (config.css_url) {
39
+ const linkEl = document.createElement('link');
40
+ linkEl.rel = "stylesheet";
41
+ linkEl.href = config.css_url;
42
+ document.head.appendChild(linkEl);
43
  }
 
44
 
45
+ // 3. Setup unique instanceId for this widget
46
+ const instanceId = Math.random().toString(36).substr(2, 8);
47
+
48
+ // 4. Calculate aspect ratio (for padding-bottom hack)
49
+ let aspectPercent = "100%";
50
+ if (config.aspect) {
51
+ if (config.aspect.includes(":")) {
52
+ const parts = config.aspect.split(":");
53
+ const w = parseFloat(parts[0]);
54
+ const h = parseFloat(parts[1]);
55
+ if (!isNaN(w) && !isNaN(h) && w > 0) {
56
+ aspectPercent = (h / w * 100) + "%";
57
+ }
58
+ } else {
59
+ const aspectValue = parseFloat(config.aspect);
60
+ if (!isNaN(aspectValue) && aspectValue > 0) {
61
+ aspectPercent = (100 / aspectValue) + "%";
62
+ }
63
+ }
64
+ } else {
65
+ // fallback: try to use parent container's aspect
66
+ const parentContainer = scriptTag.parentNode;
67
+ const containerWidth = parentContainer.offsetWidth;
68
+ const containerHeight = parentContainer.offsetHeight;
69
+ if (containerWidth > 0 && containerHeight > 0) {
70
+ aspectPercent = (containerHeight / containerWidth * 100) + "%";
71
+ }
72
+ }
73
 
74
+ // 5. Create the widget container and controls
75
+ const widgetContainer = document.createElement('div');
76
+ widgetContainer.id = 'ply-widget-container-' + instanceId;
77
+ widgetContainer.classList.add('ply-widget-container');
78
+ widgetContainer.style.height = "0";
79
+ widgetContainer.style.paddingBottom = aspectPercent;
80
+ widgetContainer.setAttribute('data-original-aspect', aspectPercent);
81
+
82
+ const tooltipsButtonHTML = config.tooltips_url
83
+ ? `<button id="tooltips-toggle-${instanceId}" class="widget-button tooltips-toggle">⦿</button>`
84
+ : '';
85
+
86
+ widgetContainer.innerHTML = `
87
+ <div id="viewer-container-${instanceId}" class="viewer-container">
88
+ <div id="progress-dialog-${instanceId}" class="progress-dialog">
89
+ <progress id="progress-indicator-${instanceId}" max="100" value="0"></progress>
90
+ </div>
91
+ <button id="fullscreen-toggle-${instanceId}" class="widget-button fullscreen-toggle">⇱</button>
92
+ <button id="help-toggle-${instanceId}" class="widget-button help-toggle">?</button>
93
+ <button id="reset-camera-btn-${instanceId}" class="widget-button reset-camera-btn">
94
+ <span class="reset-icon">⟲</span>
95
+ </button>
96
+ ${tooltipsButtonHTML}
97
+ <div id="menu-content-${instanceId}" class="menu-content">
98
+ <span id="help-close-${instanceId}" class="help-close">×</span>
99
+ <div class="help-text"></div>
100
+ </div>
101
+ </div>
102
+ <div id="tooltip-panel" class="tooltip-panel" style="display: none;">
103
+ <div class="tooltip-content">
104
+ <span id="tooltip-close" class="tooltip-close">×</span>
105
+ <div id="tooltip-text" class="tooltip-text"></div>
106
+ <img id="tooltip-image" class="tooltip-image" src="" alt="" style="display: none;" />
107
+ </div>
108
+ </div>
109
+ `;
110
+
111
+ scriptTag.parentNode.appendChild(widgetContainer);
112
+
113
+ // 6. Grab references for UI controls
114
+ const viewerContainerElem = document.getElementById('viewer-container-' + instanceId);
115
+ const fullscreenToggle = document.getElementById('fullscreen-toggle-' + instanceId);
116
+ const helpToggle = document.getElementById('help-toggle-' + instanceId);
117
+ const helpCloseBtn = document.getElementById('help-close-' + instanceId);
118
+ const resetCameraBtn = document.getElementById('reset-camera-btn-' + instanceId);
119
+ const tooltipsToggleBtn = document.getElementById('tooltips-toggle-' + instanceId);
120
+ const menuContent = document.getElementById('menu-content-' + instanceId);
121
+ const helpTextDiv = menuContent.querySelector('.help-text');
122
+ const tooltipPanel = document.getElementById('tooltip-panel');
123
+ const tooltipTextDiv = document.getElementById('tooltip-text');
124
+ const tooltipImage = document.getElementById('tooltip-image');
125
+ const tooltipCloseBtn = document.getElementById('tooltip-close');
126
+
127
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
128
+ const isMobile = isIOS || /Android/i.test(navigator.userAgent);
129
+
130
+ const tooltipInstruction = config.tooltips_url
131
+ ? '- Cliquez sur ⦿ pour afficher/masquer les tooltips.<br>'
132
+ : '';
133
+
134
+ if (isMobile) {
135
+ helpTextDiv.innerHTML =
136
+ '- Déplacez vous en glissant deux doigts sur l\'écran.<br>' +
137
+ '- Orbitez en glissant un doigt.<br>' +
138
+ '- Zoomez en pinçant avec deux doigts.<br>' +
139
+ tooltipInstruction +
140
+ '- Cliquez sur ⟲ pour réinitialiser la caméra.<br>' +
141
+ '- Cliquez sur ⇱ pour passer en plein écran.<br>';
142
+ } else {
143
+ helpTextDiv.innerHTML =
144
+ '- Orbitez avec le clic droit<br>' +
145
+ '- Zoomez avec la molette<br>' +
146
+ '- Déplacez vous avec le clic gauche<br>' +
147
+ tooltipInstruction +
148
+ '- Cliquez sur ⟲ pour réinitialiser la caméra.<br>' +
149
+ '- Cliquez sur ⇱ pour passer en plein écran.<br>';
150
+ }
151
 
152
+ // --- DYNAMIC MENU SIZING ---
153
+ function setMenuContentMaxSize() {
154
+ if (!isMobile) {
155
+ menuContent.style.maxWidth = "";
156
+ menuContent.style.maxHeight = "";
157
+ menuContent.style.width = "";
158
+ menuContent.style.height = "";
159
+ menuContent.style.overflowY = "";
160
+ menuContent.style.overflowX = "";
161
+ return;
162
  }
163
+ let parent = viewerContainerElem;
164
+ if (parent) {
165
+ const vw = parent.offsetWidth;
166
+ const vh = parent.offsetHeight;
167
+ if (vw && vh) {
168
+ menuContent.style.maxWidth = Math.round(vw * 0.8) + "px";
169
+ menuContent.style.maxHeight = Math.round(vh * 0.8) + "px";
170
+ menuContent.style.width = ""; // Let it shrink if smaller
171
+ menuContent.style.height = "";
172
+ menuContent.style.overflowY = "auto";
173
+ menuContent.style.overflowX = "auto";
174
+ } else {
175
+ menuContent.style.maxWidth = "80vw";
176
+ menuContent.style.maxHeight = "80vh";
177
+ menuContent.style.overflowY = "auto";
178
+ menuContent.style.overflowX = "auto";
179
+ }
180
+ }
181
+ }
182
+ setMenuContentMaxSize();
183
+ window.addEventListener('resize', setMenuContentMaxSize);
184
+ document.addEventListener('fullscreenchange', setMenuContentMaxSize);
185
+ window.addEventListener('orientationchange', setMenuContentMaxSize);
186
 
187
+ // --- HELP PANEL DEFAULT VISIBILITY ---
188
+ menuContent.style.display = 'block';
 
 
 
 
 
189
 
190
+ viewerContainerElem.style.display = 'block';
191
 
192
+ let dragHide = null;
 
193
 
194
+ function hideTooltipPanel() {
195
+ if (dragHide) {
196
+ viewerContainerElem.removeEventListener('pointermove', dragHide);
197
+ dragHide = null;
198
+ }
199
+ tooltipPanel.style.display = 'none';
200
+ }
201
+ function hideHelpPanel() {
202
+ menuContent.style.display = 'none';
203
+ }
204
 
205
+ // 7. Dynamically load viewer.js (modern ES2024 import, always fresh load)
206
+ let viewerModule;
207
+ try {
208
+ viewerModule = await import('https://mikafil-viewer-sgos.static.hf.space/viewer.js?' + Date.now());
209
+ await viewerModule.initializeViewer(config, instanceId);
210
+ } catch (err) {
211
+ console.error("Could not load viewer.js: " + err);
212
+ return;
213
+ }
214
 
215
+ // 8. Canvas reference (for tooltips and resizing)
216
+ const canvasId = 'canvas-' + instanceId;
217
+ const canvasEl = document.getElementById(canvasId);
218
 
219
+ // 9. Tooltips toggle if any
220
+ if (tooltipsToggleBtn) {
221
+ if (!config.tooltips_url) {
222
+ tooltipsToggleBtn.style.display = 'none';
223
+ } else {
224
+ fetch(config.tooltips_url)
225
+ .then(resp => { if (!resp.ok) tooltipsToggleBtn.style.display = 'none'; })
226
+ .catch(() => { tooltipsToggleBtn.style.display = 'none'; });
227
+ }
228
+ }
229
 
230
+ let isFullscreen = false;
231
+ let savedState = null;
232
+
233
+ function saveCurrentState() {
234
+ if (isFullscreen) return;
235
+ const originalAspect = widgetContainer.getAttribute('data-original-aspect') || aspectPercent;
236
+ savedState = {
237
+ widget: {
238
+ position: widgetContainer.style.position,
239
+ top: widgetContainer.style.top,
240
+ left: widgetContainer.style.left,
241
+ width: widgetContainer.style.width,
242
+ height: widgetContainer.style.height,
243
+ maxWidth: widgetContainer.style.maxWidth,
244
+ maxHeight:widgetContainer.style.maxHeight,
245
+ paddingBottom: widgetContainer.style.paddingBottom || originalAspect,
246
+ margin: widgetContainer.style.margin,
247
+ },
248
+ viewer: {
249
+ borderRadius: viewerContainerElem.style.borderRadius,
250
+ border: viewerContainerElem.style.border,
251
  }
252
+ };
253
+ }
254
+
255
+ function restoreOriginalStyles() {
256
+ if (!savedState) return;
257
+ const aspectToUse = savedState.widget.paddingBottom;
258
+ widgetContainer.style.position = savedState.widget.position || "";
259
+ widgetContainer.style.top = savedState.widget.top || "";
260
+ widgetContainer.style.left = savedState.widget.left || "";
261
+ widgetContainer.style.width = "100%";
262
+ widgetContainer.style.height = "0";
263
+ widgetContainer.style.maxWidth = savedState.widget.maxWidth || "";
264
+ widgetContainer.style.maxHeight = savedState.widget.maxHeight || "";
265
+ widgetContainer.style.paddingBottom= aspectToUse;
266
+ widgetContainer.style.margin = savedState.widget.margin || "";
267
+ widgetContainer.classList.remove('fake-fullscreen');
268
+
269
+ viewerContainerElem.style.position = "absolute";
270
+ viewerContainerElem.style.top = "0";
271
+ viewerContainerElem.style.left = "0";
272
+ viewerContainerElem.style.right = "0";
273
+ viewerContainerElem.style.bottom = "0";
274
+ viewerContainerElem.style.width = "100%";
275
+ viewerContainerElem.style.height = "100%";
276
+ viewerContainerElem.style.borderRadius = savedState.viewer.borderRadius || "";
277
+ viewerContainerElem.style.border = savedState.viewer.border || "";
278
+
279
+ if (viewerModule.app) {
280
+ viewerModule.app.resizeCanvas(
281
+ viewerContainerElem.clientWidth,
282
+ viewerContainerElem.clientHeight
283
+ );
284
  }
285
 
286
+ if (fullscreenToggle) fullscreenToggle.textContent = '⇱';
287
+
288
+ savedState = null;
289
+ setMenuContentMaxSize();
290
+ }
 
291
 
292
+ function applyFullscreenStyles() {
293
+ widgetContainer.style.position = 'fixed';
294
+ widgetContainer.style.top = '0';
295
+ widgetContainer.style.left = '0';
296
+ widgetContainer.style.width = '100vw';
297
+ widgetContainer.style.height = '100vh';
298
+ widgetContainer.style.maxWidth = '100vw';
299
+ widgetContainer.style.maxHeight = '100vh';
300
+ widgetContainer.style.paddingBottom = '0';
301
+ widgetContainer.style.margin = '0';
302
+ widgetContainer.style.border = 'none';
303
+ widgetContainer.style.borderRadius = '0';
304
+
305
+ viewerContainerElem.style.width = '100%';
306
+ viewerContainerElem.style.height = '100%';
307
+ viewerContainerElem.style.borderRadius= '0';
308
+ viewerContainerElem.style.border = 'none';
309
+
310
+ if (viewerModule.app) {
311
+ viewerModule.app.resizeCanvas(window.innerWidth, window.innerHeight);
312
  }
 
313
 
314
+ if (fullscreenToggle) fullscreenToggle.textContent = '⇲';
315
+ isFullscreen = true;
316
+ setMenuContentMaxSize();
 
 
 
317
  }
318
 
319
+ function enterFullscreen() {
320
+ if (!savedState) saveCurrentState();
321
+ if (isIOS) {
322
+ applyFullscreenStyles();
323
+ widgetContainer.classList.add('fake-fullscreen');
324
+ } else if (widgetContainer.requestFullscreen) {
325
+ widgetContainer.requestFullscreen()
326
+ .then(applyFullscreenStyles)
327
+ .catch(() => {
328
+ applyFullscreenStyles();
329
+ widgetContainer.classList.add('fake-fullscreen');
330
+ });
331
+ } else {
332
+ applyFullscreenStyles();
333
+ widgetContainer.classList.add('fake-fullscreen');
334
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  }
336
 
337
+ function exitFullscreen() {
338
+ if (document.fullscreenElement === widgetContainer && document.exitFullscreen) {
339
+ document.exitFullscreen().catch(() => {});
340
+ }
341
+ widgetContainer.classList.remove('fake-fullscreen');
342
+ restoreOriginalStyles();
343
+ isFullscreen = false;
344
+ if (fullscreenToggle) fullscreenToggle.textContent = '⇱';
345
+ setMenuContentMaxSize();
346
+ }
347
+
348
+ fullscreenToggle.addEventListener('click', () => {
349
+ hideTooltipPanel();
350
+ isFullscreen ? exitFullscreen() : enterFullscreen();
351
+ });
352
+
353
+ document.addEventListener('fullscreenchange', () => {
354
+ if (!document.fullscreenElement && isFullscreen) {
355
+ isFullscreen = false;
356
+ restoreOriginalStyles();
357
+ if (fullscreenToggle) fullscreenToggle.textContent = '⇱';
358
+ } else if (document.fullscreenElement === widgetContainer) {
359
+ if (fullscreenToggle) fullscreenToggle.textContent = '⇲';
360
+ }
361
+ setMenuContentMaxSize();
362
+ });
363
+
364
+ helpToggle.addEventListener('click', (e) => {
365
+ hideTooltipPanel();
366
+ e.stopPropagation();
367
+ // Toggle menu panel
368
+ if (menuContent.style.display === 'block') {
369
+ menuContent.style.display = 'none';
370
  } else {
371
+ menuContent.style.display = 'block';
372
+ setMenuContentMaxSize();
 
 
 
 
 
 
 
 
 
 
373
  }
374
+ });
375
+ helpCloseBtn.addEventListener('click', hideHelpPanel);
376
 
377
+ resetCameraBtn.addEventListener('click', () => {
378
+ hideTooltipPanel();
379
+ if (viewerModule.resetViewerCamera) {
380
+ viewerModule.resetViewerCamera();
381
+ }
382
+ });
383
 
384
+ if (tooltipsToggleBtn) {
385
+ let tooltipsVisible = !!config.showTooltipsDefault;
386
+ tooltipsToggleBtn.style.opacity = tooltipsVisible ? '1' : '0.5';
387
+ tooltipsToggleBtn.addEventListener('click', () => {
388
+ hideTooltipPanel();
389
+ tooltipsVisible = !tooltipsVisible;
390
+ tooltipsToggleBtn.style.opacity = tooltipsVisible ? '1' : '0.5';
391
+ document.dispatchEvent(new CustomEvent('toggle-tooltips', { detail: { visible: tooltipsVisible } }));
392
+ });
393
+ }
394
+
395
+ tooltipCloseBtn.addEventListener('click', hideTooltipPanel);
396
 
397
+ document.addEventListener('tooltip-selected', (evt) => {
398
+ // Always show panel, cancel hide first
399
+ if (dragHide) {
400
+ viewerContainerElem.removeEventListener('pointermove', dragHide);
401
+ dragHide = null;
402
+ }
403
+ const { title, description, imgUrl } = evt.detail;
404
+ tooltipTextDiv.innerHTML = `<strong>${title}</strong><br>${description}`;
405
+ // Force a repaint: clear src before setting, for repeated images
406
+ tooltipImage.style.display = 'none';
407
+ tooltipImage.src = '';
408
+ if (imgUrl) {
409
+ tooltipImage.onload = () => {
410
+ tooltipImage.style.display = 'block';
411
+ };
412
+ tooltipImage.src = imgUrl;
413
+ } else {
414
+ tooltipImage.style.display = 'none';
415
  }
416
+ tooltipPanel.style.display = 'flex';
417
+ // --- DELAYED pointermove handler ---
418
+ setTimeout(() => {
419
+ dragHide = (e) => {
420
+ if ((e.pointerType === 'mouse' && e.buttons !== 0) || e.pointerType === 'touch') {
421
+ hideTooltipPanel();
422
+ }
423
+ };
424
+ viewerContainerElem.addEventListener('pointermove', dragHide);
425
+ }, 100);
426
+ });
427
 
428
+ if (canvasEl) {
429
+ canvasEl.addEventListener('wheel', hideTooltipPanel, { passive: true });
430
+ }
431
+ document.addEventListener('keydown', (e) => {
432
+ if ((e.key === 'Escape' || e.key === 'Esc') && isFullscreen) exitFullscreen();
433
+ });
434
+ window.addEventListener('resize', () => {
435
+ if (viewerModule.app) {
436
+ if (isFullscreen) {
437
+ viewerModule.app.resizeCanvas(window.innerWidth, window.innerHeight);
438
+ } else {
439
+ viewerModule.app.resizeCanvas(
440
+ viewerContainerElem.clientWidth,
441
+ viewerContainerElem.clientHeight
442
+ );
 
 
 
 
 
 
 
 
 
 
 
 
443
  }
444
  }
445
+ setMenuContentMaxSize();
446
+ });
447
 
448
+ setTimeout(() => {
449
+ saveCurrentState();
450
+ document.dispatchEvent(new CustomEvent('toggle-tooltips', { detail: { visible: !!config.showTooltipsDefault } }));
451
+ setMenuContentMaxSize();
452
+ }, 200);
453
+
454
+ })();