bilca commited on
Commit
97def3c
·
verified ·
1 Parent(s): 8a79fdf

Update interface.js

Browse files
Files changed (1) hide show
  1. interface.js +406 -203
interface.js CHANGED
@@ -1,234 +1,437 @@
1
- //points.js
2
-
3
- /**
4
- * initializePoints(options)
5
- *
6
- * - options.app: the PlayCanvas App instance
7
- * - options.cameraEntity: the PlayCanvas camera Entity
8
- * - options.modelEntity: the main model entity (for any relative positioning; optional)
9
- * - options.pointsUrl: URL to fetch JSON array of points
10
- * - options.defaultVisible: boolean: whether points are visible initially
11
- * - options.moveDuration: number (seconds) for smooth camera move to selected point
12
- */
13
- export async function initializePoints(options) {
14
- const {
15
- app,
16
- cameraEntity,
17
- modelEntity,
18
- pointsUrl,
19
- defaultVisible,
20
- moveDuration = 0.6
21
- } = options;
22
-
23
- if (!app || !cameraEntity || !pointsUrl) {
24
- console.error("points.js → missing required initialization options");
25
- return;
26
  }
27
 
28
- // Load JSON of points:
29
- let pointsData;
30
- try {
31
- const resp = await fetch(pointsUrl);
32
- pointsData = await resp.json();
33
- } catch (e) {
34
- console.error("points.js failed fetching points.json:", e);
 
 
 
 
 
35
  return;
36
  }
37
- if (!Array.isArray(pointsData)) {
38
- console.error("points.js points.json must be an array");
39
- return;
 
 
 
 
40
  }
41
 
42
- const pointEntities = [];
43
-
44
- // Create a material for info-point spheres
45
- const mat = new pc.StandardMaterial();
46
- mat.diffuse = new pc.Color(1, 0.8, 0);
47
- mat.specular = new pc.Color(1, 1, 1);
48
- mat.shininess = 20;
49
- mat.update();
50
-
51
- // Build each sphere + attach custom data
52
- for (let i = 0; i < pointsData.length; i++) {
53
- const pt = pointsData[i];
54
- const { x, y, z, text, imageUrl } = pt;
55
-
56
- const sphere = new pc.Entity("point-" + i);
57
- sphere.addComponent("model", { type: "sphere" });
58
- sphere.model.material = mat;
59
-
60
- // Scale small (primitive sphere radius = 0.5)
61
- sphere.setLocalScale(0.05, 0.05, 0.05);
62
- sphere.setLocalPosition(x, y, z);
63
- sphere.pointData = { text, imageUrl };
64
- app.root.addChild(sphere);
65
- pointEntities.push(sphere);
 
 
66
  }
67
 
68
- // Show/hide all point spheres
69
- function setPointsVisibility(visible) {
70
- pointEntities.forEach(ent => {
71
- ent.enabled = visible;
72
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  }
74
- setPointsVisibility(!!defaultVisible);
75
 
76
- // Respond to toggle-points event from interface.js
77
- document.addEventListener("toggle-points", (evt) => {
78
- const { visible } = evt.detail;
79
- setPointsVisibility(!!visible);
80
- });
 
 
 
 
 
 
81
 
82
- // Keep track of any in-flight camera tween so we can cancel it
83
- let currentTween = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
- // On mouse down (or touch equivalent), perform manual ray‐sphere intersection
86
- app.mouse.on(pc.EVENT_MOUSEDOWN, (event) => {
87
- // If a tween is running, cancel it immediately
88
- if (currentTween) {
89
- app.off("update", currentTween);
90
- currentTween = null;
 
 
 
 
 
 
91
  }
92
 
93
- const x = event.x;
94
- const y = event.y;
95
- const from = new pc.Vec3();
96
- const to = new pc.Vec3();
97
- const camera = cameraEntity.camera;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
- cameraEntity.camera.screenToWorld(x, y, camera.nearClip, from);
100
- cameraEntity.camera.screenToWorld(x, y, camera.farClip, to);
101
 
102
- const dir = new pc.Vec3().sub2(to, from).normalize();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
- let closestT = Infinity;
105
- let pickedEntity = null;
106
 
107
- for (const ent of pointEntities) {
108
- if (!ent.enabled) continue;
 
 
109
 
110
- const center = ent.getPosition();
111
- const worldRadius = 0.5 * ent.getLocalScale().x;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
- const oc = new pc.Vec3().sub2(center, from);
114
- const tca = oc.dot(dir);
115
- if (tca < 0) continue;
 
 
 
 
 
 
 
 
 
 
 
116
 
117
- const d2 = oc.lengthSq() - (tca * tca);
118
- if (d2 > worldRadius * worldRadius) continue;
 
 
 
 
 
119
 
120
- const thc = Math.sqrt(worldRadius * worldRadius - d2);
121
- const t0 = tca - thc;
122
- if (t0 < closestT && t0 >= 0) {
123
- closestT = t0;
124
- pickedEntity = ent;
125
- }
 
126
  }
 
127
 
128
- if (pickedEntity) {
129
- const { text, imageUrl } = pickedEntity.pointData;
130
- document.dispatchEvent(new CustomEvent("point-selected", {
131
- detail: { text, imageUrl }
132
- }));
133
- tweenCameraToPoint(pickedEntity, moveDuration);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  }
135
  });
136
 
137
- // Also close tooltip if user interacts (mouse or touch) on the canvas
138
- const canvasId = app.graphicsDevice.canvas.id;
139
- const htmlCanvas = document.getElementById(canvasId);
140
- if (htmlCanvas) {
141
- htmlCanvas.addEventListener("mousedown", () => {
142
- document.dispatchEvent(new CustomEvent("hide-tooltip"));
143
- });
144
- htmlCanvas.addEventListener("touchstart", () => {
145
- document.dispatchEvent(new CustomEvent("hide-tooltip"));
146
- });
147
- }
148
 
149
- // Tween helper: smoothly move and reorient camera to focus the chosen point entity
150
- function tweenCameraToPoint(pointEnt, duration) {
151
- const orbitCam = cameraEntity.script.orbitCamera;
152
- if (!orbitCam) return;
153
-
154
- // Compute target pivot exactly at the sphere center
155
- const targetPos = pointEnt.getPosition().clone();
156
- // Compute current state
157
- const startPivot = orbitCam.pivotPoint.clone();
158
- const startYaw = orbitCam._yaw;
159
- const startPitch = orbitCam._pitch;
160
- const startDist = orbitCam._distance;
161
-
162
- // Compute direction & candidate distance:
163
- const worldRadius = 0.5 * pointEnt.getLocalScale().x;
164
- const minZoom = orbitCam.distanceMin;
165
- const desiredDistance = Math.max(minZoom * 1.2, worldRadius * 4);
166
-
167
- // Compute target yaw/pitch from camera pointing at targetPos
168
- // Reuse reset logic: place a temp entity at camera’s current position, have it look at target
169
- const camWorldPos = cameraEntity.getPosition().clone();
170
- const tempEnt = new pc.Entity();
171
- tempEnt.setPosition(camWorldPos);
172
- tempEnt.lookAt(targetPos);
173
- const rotation = tempEnt.getRotation();
174
- const forward = new pc.Vec3();
175
- rotation.transformVector(pc.Vec3.FORWARD, forward);
176
- const tgtYaw = Math.atan2(-forward.x, -forward.z) * pc.math.RAD_TO_DEG;
177
- const yawQuat = new pc.Quat().setFromEulerAngles(0, -tgtYaw, 0);
178
- const rotNoYaw = new pc.Quat().mul2(yawQuat, rotation);
179
- const fNoYaw = new pc.Vec3();
180
- rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw);
181
- const tgtPitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
182
- tempEnt.destroy();
183
-
184
- // Target state:
185
- const endPivot = targetPos.clone();
186
- const endYaw = tgtYaw;
187
- const endPitch = tgtPitch;
188
- const endDist = desiredDistance;
189
-
190
- let elapsed = 0;
191
- const orgPivot = startPivot.clone();
192
- const orgYaw = startYaw;
193
- const orgPitch = startPitch;
194
- const orgDist = startDist;
195
-
196
- // If another tween is running, cancel it
197
- if (currentTween) {
198
- app.off("update", currentTween);
199
- currentTween = null;
200
- }
201
-
202
- // Per-frame update
203
- function lerpUpdate(dt) {
204
- elapsed += dt;
205
- const t = Math.min(elapsed / duration, 1);
206
-
207
- // Interpolate pivot (vector lerp)
208
- const newPivot = new pc.Vec3().lerp(orgPivot, endPivot, t);
209
- orbitCam.pivotPoint.copy(newPivot);
210
-
211
- // Interpolate yaw/pitch/distance (simple lerp)
212
- const newYaw = pc.math.lerp(orgYaw, endYaw, t);
213
- const newPitch = pc.math.lerp(orgPitch, endPitch, t);
214
- const newDist = pc.math.lerp(orgDist, endDist, t);
215
-
216
- orbitCam._targetYaw = newYaw;
217
- orbitCam._yaw = newYaw;
218
- orbitCam._targetPitch = newPitch;
219
- orbitCam._pitch = newPitch;
220
- orbitCam._targetDistance = newDist;
221
- orbitCam._distance = newDist;
222
-
223
- orbitCam._updatePosition();
224
-
225
- if (t >= 1) {
226
- app.off("update", lerpUpdate);
227
- currentTween = null;
228
- }
229
  }
 
230
 
231
- currentTween = lerpUpdate;
232
- app.on("update", lerpUpdate);
 
 
233
  }
234
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ==============================
2
+ // interface.js
3
+ // ==============================
4
+
5
+ // Store a reference to the <script> tag that loaded this file
6
+ const currentScriptTag = document.currentScript;
7
+
8
+ (async function() {
9
+ // ─── 1. Locate the <script> and read data-config ───────────────────────────────
10
+ let scriptTag = currentScriptTag;
11
+ if (!scriptTag) {
12
+ const scripts = document.getElementsByTagName('script');
13
+ for (let i = 0; i < scripts.length; i++) {
14
+ if (scripts[i].src.includes('interface.js') && scripts[i].hasAttribute('data-config')) {
15
+ scriptTag = scripts[i];
16
+ break;
17
+ }
18
+ }
19
+ if (!scriptTag && scripts.length > 0) {
20
+ scriptTag = scripts[scripts.length - 1];
21
+ }
 
 
 
 
22
  }
23
 
24
+ const configUrl = scriptTag.getAttribute('data-config');
25
+ let config = {};
26
+ if (configUrl) {
27
+ try {
28
+ const response = await fetch(configUrl);
29
+ config = await response.json();
30
+ } catch (error) {
31
+ console.error("Error loading config file:", error);
32
+ return;
33
+ }
34
+ } else {
35
+ console.error("No config file provided. Please set a data-config attribute on the <script> tag.");
36
  return;
37
  }
38
+
39
+ // ─── 2. If config.css_url is provided, inject a <link> to that CSS ─────────────
40
+ if (config.css_url) {
41
+ const linkEl = document.createElement('link');
42
+ linkEl.rel = "stylesheet";
43
+ linkEl.href = config.css_url;
44
+ document.head.appendChild(linkEl);
45
  }
46
 
47
+ // ─── 3. Generate a unique instanceId for this widget ───────────────────────────
48
+ const instanceId = Math.random().toString(36).substr(2, 8);
49
+
50
+ // ─── 4. Compute the aspect ratio (padding-bottom %) ────────────────────────────
51
+ let aspectPercent = "100%";
52
+ if (config.aspect) {
53
+ if (config.aspect.includes(":")) {
54
+ const parts = config.aspect.split(":");
55
+ const w = parseFloat(parts[0]);
56
+ const h = parseFloat(parts[1]);
57
+ if (!isNaN(w) && !isNaN(h) && w > 0) {
58
+ aspectPercent = (h / w * 100) + "%";
59
+ }
60
+ } else {
61
+ const aspectValue = parseFloat(config.aspect);
62
+ if (!isNaN(aspectValue) && aspectValue > 0) {
63
+ aspectPercent = (100 / aspectValue) + "%";
64
+ }
65
+ }
66
+ } else {
67
+ const parentContainer = scriptTag.parentNode;
68
+ const containerWidth = parentContainer.offsetWidth;
69
+ const containerHeight = parentContainer.offsetHeight;
70
+ if (containerWidth > 0 && containerHeight > 0) {
71
+ aspectPercent = (containerHeight / containerWidth * 100) + "%";
72
+ }
73
  }
74
 
75
+ // ─── 5. Create the widget container (no GIF preview, no close button) ───────────
76
+ const widgetContainer = document.createElement('div');
77
+ widgetContainer.id = 'ply-widget-container-' + instanceId;
78
+ widgetContainer.classList.add('ply-widget-container');
79
+ widgetContainer.style.height = "0";
80
+ widgetContainer.style.paddingBottom = aspectPercent;
81
+ widgetContainer.setAttribute('data-original-aspect', aspectPercent);
82
+
83
+ // Add the 3D-viewer HTML + 📍 toggle + tooltip markup
84
+ widgetContainer.innerHTML = `
85
+ <div id="viewer-container-${instanceId}" class="viewer-container">
86
+ <div id="progress-dialog-${instanceId}" class="progress-dialog">
87
+ <progress id="progress-indicator-${instanceId}" max="100" value="0"></progress>
88
+ </div>
89
+ <button id="fullscreen-toggle-${instanceId}" class="widget-button fullscreen-toggle">⇱</button>
90
+ <button id="help-toggle-${instanceId}" class="widget-button help-toggle">?</button>
91
+ <button id="reset-camera-btn-${instanceId}" class="widget-button reset-camera-btn">
92
+ <span class="reset-icon">⟲</span>
93
+ </button>
94
+ <button id="points-toggle-${instanceId}" class="widget-button points-toggle">📍</button>
95
+ <div id="menu-content-${instanceId}" class="menu-content"></div>
96
+ </div>
97
+ <div id="point-tooltip" class="point-tooltip" style="display: none;">
98
+ <div class="point-tooltip-content">
99
+ <span id="point-tooltip-close" class="point-tooltip-close">×</span>
100
+ <div id="point-tooltip-text" class="point-tooltip-text"></div>
101
+ <img id="point-tooltip-image" class="point-tooltip-image" src="" alt="" style="display: none;" />
102
+ </div>
103
+ </div>
104
+ `;
105
+
106
+ // Append the widget container immediately after the <script> tag
107
+ scriptTag.parentNode.appendChild(widgetContainer);
108
+
109
+ // ─── 6. Grab references to new DOM elements ──────────────────────────────────
110
+ const viewerContainerElem = document.getElementById('viewer-container-' + instanceId);
111
+ const fullscreenToggle = document.getElementById('fullscreen-toggle-' + instanceId);
112
+ const helpToggle = document.getElementById('help-toggle-' + instanceId);
113
+ const resetCameraBtn = document.getElementById('reset-camera-btn-' + instanceId);
114
+ const pointsToggleBtn = document.getElementById('points-toggle-' + instanceId);
115
+ const menuContent = document.getElementById('menu-content-' + instanceId);
116
+
117
+ // Tooltip elements
118
+ const tooltipDiv = document.getElementById('point-tooltip');
119
+ const tooltipTextDiv = document.getElementById('point-tooltip-text');
120
+ const tooltipImage = document.getElementById('point-tooltip-image');
121
+ const tooltipCloseBtn = document.getElementById('point-tooltip-close');
122
+
123
+ // Device detection for help text
124
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
125
+ const isMobile = isIOS || /Android/i.test(navigator.userAgent);
126
+ if (isMobile) {
127
+ menuContent.innerHTML = `
128
+ - Pour vous déplacer, glissez deux doigts sur l'écran.<br>
129
+ - Pour orbiter, utilisez un doigt.<br>
130
+ - Pour zoomer, pincez avec deux doigts.
131
+ `;
132
+ } else {
133
+ menuContent.innerHTML = `
134
+ - orbitez avec le clic droit<br>
135
+ - zoomez avec la molette<br>
136
+ - déplacez vous avec le clic gauche
137
+ `;
138
  }
 
139
 
140
+ viewerContainerElem.style.display = 'block';
141
+
142
+ // ─── 7. Dynamically load viewer.js ─────────────────────────────────────────
143
+ let viewerModule;
144
+ try {
145
+ viewerModule = await import('./viewer.js');
146
+ await viewerModule.initializeViewer(config, instanceId);
147
+ } catch (err) {
148
+ console.error("Failed to load viewer.js or initialize the 3D viewer:", err);
149
+ return;
150
+ }
151
 
152
+ // ─── 8. Fullscreen / state-preservation logic ───────────────────────────────
153
+ let isFullscreen = false;
154
+ let savedState = null;
155
+
156
+ function saveCurrentState() {
157
+ if (isFullscreen) return;
158
+ const computedWidget = window.getComputedStyle(widgetContainer);
159
+ const computedViewer = window.getComputedStyle(viewerContainerElem);
160
+ const originalAspect = widgetContainer.getAttribute('data-original-aspect') || aspectPercent;
161
+ const containerWidth = widgetContainer.offsetWidth;
162
+ const containerHeight = widgetContainer.clientHeight || viewerContainerElem.offsetHeight;
163
+ const calculatedRatio = (containerHeight / containerWidth * 100) + '%';
164
+
165
+ savedState = {
166
+ widget: {
167
+ position: widgetContainer.style.position,
168
+ top: widgetContainer.style.top,
169
+ left: widgetContainer.style.left,
170
+ width: widgetContainer.style.width,
171
+ height: widgetContainer.style.height,
172
+ maxWidth: widgetContainer.style.maxWidth,
173
+ maxHeight: widgetContainer.style.maxHeight,
174
+ paddingBottom: widgetContainer.style.paddingBottom || originalAspect,
175
+ margin: widgetContainer.style.margin,
176
+ aspectPercent: originalAspect,
177
+ calculatedAspect: calculatedRatio,
178
+ computedWidth: computedWidget.width,
179
+ computedHeight: computedWidget.height,
180
+ offsetWidth: containerWidth,
181
+ offsetHeight: containerHeight
182
+ },
183
+ viewer: {
184
+ borderRadius: viewerContainerElem.style.borderRadius,
185
+ border: viewerContainerElem.style.border,
186
+ computedWidth: computedViewer.width,
187
+ computedHeight: computedViewer.height
188
+ }
189
+ };
190
+ }
191
 
192
+ function restoreOriginalStyles() {
193
+ if (!savedState) return;
194
+ let aspectToUse = aspectPercent;
195
+ if (savedState.widget.offsetWidth && savedState.widget.offsetHeight) {
196
+ const actualRatio = (savedState.widget.offsetHeight / savedState.widget.offsetWidth * 100) + '%';
197
+ aspectToUse = actualRatio;
198
+ } else if (savedState.widget.calculatedAspect) {
199
+ aspectToUse = savedState.widget.calculatedAspect;
200
+ } else if (savedState.widget.aspectPercent) {
201
+ aspectToUse = savedState.widget.aspectPercent;
202
+ } else if (savedState.widget.paddingBottom) {
203
+ aspectToUse = savedState.widget.paddingBottom;
204
  }
205
 
206
+ widgetContainer.style.position = savedState.widget.position || "";
207
+ widgetContainer.style.top = savedState.widget.top || "";
208
+ widgetContainer.style.left = savedState.widget.left || "";
209
+ widgetContainer.style.width = "100%";
210
+ widgetContainer.style.height = "0";
211
+ widgetContainer.style.maxWidth = savedState.widget.maxWidth || "";
212
+ widgetContainer.style.maxHeight = savedState.widget.maxHeight || "";
213
+ widgetContainer.style.paddingBottom= aspectToUse;
214
+ widgetContainer.style.margin = savedState.widget.margin || "";
215
+ widgetContainer.style.border = savedState.widget.border || "";
216
+ widgetContainer.style.borderRadius = savedState.widget.borderRadius|| "";
217
+ widgetContainer.style.overflow = savedState.widget.overflow || "";
218
+ widgetContainer.classList.remove('fake-fullscreen');
219
+
220
+ viewerContainerElem.style.position = "absolute";
221
+ viewerContainerElem.style.top = "0";
222
+ viewerContainerElem.style.left = "0";
223
+ viewerContainerElem.style.right = "0";
224
+ viewerContainerElem.style.bottom = "0";
225
+ viewerContainerElem.style.width = "100%";
226
+ viewerContainerElem.style.height = "100%";
227
+ viewerContainerElem.style.borderRadius= savedState.viewer.borderRadius || "";
228
+ viewerContainerElem.style.border = savedState.viewer.border || "";
229
+
230
+ if (viewerModule.app) {
231
+ const cw = viewerContainerElem.clientWidth;
232
+ const ch = viewerContainerElem.clientHeight;
233
+ viewerModule.app.resizeCanvas(cw, ch);
234
+ }
235
 
236
+ savedState = null;
237
+ }
238
 
239
+ function applyFullscreenStyles() {
240
+ widgetContainer.style.position = 'fixed';
241
+ widgetContainer.style.top = '0';
242
+ widgetContainer.style.left = '0';
243
+ widgetContainer.style.width = '100vw';
244
+ widgetContainer.style.height = '100vh';
245
+ widgetContainer.style.maxWidth = '100vw';
246
+ widgetContainer.style.maxHeight = '100vh';
247
+ widgetContainer.style.paddingBottom= '0';
248
+ widgetContainer.style.margin = '0';
249
+ widgetContainer.style.border = 'none';
250
+ widgetContainer.style.borderRadius = '0';
251
+
252
+ viewerContainerElem.style.width = '100%';
253
+ viewerContainerElem.style.height = '100%';
254
+ viewerContainerElem.style.borderRadius= '0';
255
+ viewerContainerElem.style.border = 'none';
256
+
257
+ if (viewerModule.app) {
258
+ viewerModule.app.resizeCanvas(window.innerWidth, window.innerHeight);
259
+ }
260
 
261
+ fullscreenToggle.textContent = '⇲';
262
+ isFullscreen = true;
263
 
264
+ if (viewerModule.resetViewerCamera) {
265
+ viewerModule.resetViewerCamera();
266
+ }
267
+ }
268
 
269
+ function enterFullscreen() {
270
+ if (!savedState) {
271
+ saveCurrentState();
272
+ }
273
+ if (isIOS) {
274
+ applyFullscreenStyles();
275
+ widgetContainer.classList.add('fake-fullscreen');
276
+ } else {
277
+ if (widgetContainer.requestFullscreen) {
278
+ widgetContainer.requestFullscreen()
279
+ .then(() => {
280
+ applyFullscreenStyles();
281
+ })
282
+ .catch(err => {
283
+ console.error("Fullscreen request failed:", err);
284
+ applyFullscreenStyles();
285
+ widgetContainer.classList.add('fake-fullscreen');
286
+ });
287
+ } else if (widgetContainer.webkitRequestFullscreen) {
288
+ widgetContainer.webkitRequestFullscreen();
289
+ applyFullscreenStyles();
290
+ } else if (widgetContainer.mozRequestFullScreen) {
291
+ widgetContainer.mozRequestFullScreen();
292
+ applyFullscreenStyles();
293
+ } else if (widgetContainer.msRequestFullscreen) {
294
+ widgetContainer.msRequestFullscreen();
295
+ applyFullscreenStyles();
296
+ } else {
297
+ applyFullscreenStyles();
298
+ widgetContainer.classList.add('fake-fullscreen');
299
+ }
300
+ }
301
+ }
302
 
303
+ function exitFullscreen() {
304
+ if (document.fullscreenElement === widgetContainer) {
305
+ if (document.exitFullscreen) {
306
+ document.exitFullscreen().catch(err => {
307
+ console.error("Error exiting fullscreen:", err);
308
+ });
309
+ } else if (document.webkitExitFullscreen) {
310
+ document.webkitExitFullscreen();
311
+ } else if (document.mozCancelFullScreen) {
312
+ document.mozCancelFullScreen();
313
+ } else if (document.msExitFullscreen) {
314
+ document.msExitFullscreen();
315
+ }
316
+ }
317
 
318
+ widgetContainer.classList.remove('fake-fullscreen');
319
+ restoreOriginalStyles();
320
+ if (viewerModule.resetViewerCamera) {
321
+ viewerModule.resetViewerCamera();
322
+ }
323
+ isFullscreen = false;
324
+ }
325
 
326
+ // ─── 9. Hook up event listeners ───────────────────────────────────────────
327
+
328
+ fullscreenToggle.addEventListener('click', () => {
329
+ if (!isFullscreen) {
330
+ enterFullscreen();
331
+ } else {
332
+ exitFullscreen();
333
  }
334
+ });
335
 
336
+ document.addEventListener('fullscreenchange', () => {
337
+ if (document.fullscreenElement === widgetContainer) {
338
+ isFullscreen = true;
339
+ applyFullscreenStyles();
340
+ } else if (isFullscreen) {
341
+ isFullscreen = false;
342
+ restoreOriginalStyles();
343
+ }
344
+ });
345
+ document.addEventListener('webkitfullscreenchange', () => {
346
+ if (document.webkitFullscreenElement === widgetContainer) {
347
+ isFullscreen = true;
348
+ applyFullscreenStyles();
349
+ } else if (isFullscreen) {
350
+ isFullscreen = false;
351
+ restoreOriginalStyles();
352
+ }
353
+ });
354
+ document.addEventListener('mozfullscreenchange', () => {
355
+ if (document.mozFullScreenElement === widgetContainer) {
356
+ isFullscreen = true;
357
+ applyFullscreenStyles();
358
+ } else if (isFullscreen) {
359
+ isFullscreen = false;
360
+ restoreOriginalStyles();
361
+ }
362
+ });
363
+ document.addEventListener('MSFullscreenChange', () => {
364
+ if (document.msFullscreenElement === widgetContainer) {
365
+ isFullscreen = true;
366
+ applyFullscreenStyles();
367
+ } else if (isFullscreen) {
368
+ isFullscreen = false;
369
+ restoreOriginalStyles();
370
  }
371
  });
372
 
373
+ helpToggle.addEventListener('click', (e) => {
374
+ e.stopPropagation();
375
+ menuContent.style.display = (menuContent.style.display === 'block') ? 'none' : 'block';
376
+ });
 
 
 
 
 
 
 
377
 
378
+ resetCameraBtn.addEventListener('click', () => {
379
+ if (viewerModule.resetViewerCamera) {
380
+ viewerModule.resetViewerCamera();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
  }
382
+ });
383
 
384
+ // 📍 toggle button
385
+ let pointsVisible = !!config.showPointsDefault;
386
+ if (!pointsVisible) {
387
+ pointsToggleBtn.style.opacity = '0.5';
388
  }
389
+ pointsToggleBtn.addEventListener('click', () => {
390
+ pointsVisible = !pointsVisible;
391
+ pointsToggleBtn.style.opacity = pointsVisible ? '1' : '0.5';
392
+ document.dispatchEvent(new CustomEvent('toggle-points', { detail: { visible: pointsVisible } }));
393
+ });
394
+
395
+ // Close tooltip on button click
396
+ tooltipCloseBtn.addEventListener('click', () => {
397
+ tooltipDiv.style.display = 'none';
398
+ });
399
+
400
+ // Listen for point-selection events and show tooltip
401
+ document.addEventListener('point-selected', (evt) => {
402
+ const { text, imageUrl } = evt.detail;
403
+ tooltipTextDiv.textContent = text || "";
404
+ if (imageUrl) {
405
+ tooltipImage.src = imageUrl;
406
+ tooltipImage.style.display = 'block';
407
+ } else {
408
+ tooltipImage.style.display = 'none';
409
+ }
410
+ tooltipDiv.style.display = 'flex';
411
+ });
412
+
413
+ // Escape key also exits fullscreen
414
+ document.addEventListener('keydown', (e) => {
415
+ if ((e.key === 'Escape' || e.key === 'Esc') && isFullscreen) {
416
+ exitFullscreen();
417
+ }
418
+ });
419
+
420
+ // Window resize → resize PlayCanvas canvas
421
+ window.addEventListener('resize', () => {
422
+ if (isFullscreen && viewerModule.app) {
423
+ viewerModule.app.resizeCanvas(window.innerWidth, window.innerHeight);
424
+ } else if (viewerModule.app) {
425
+ const cw = viewerContainerElem.clientWidth;
426
+ const ch = viewerContainerElem.clientHeight;
427
+ viewerModule.app.resizeCanvas(cw, ch);
428
+ }
429
+ });
430
+
431
+ // Save “initial state” after a brief delay
432
+ setTimeout(() => {
433
+ saveCurrentState();
434
+ document.dispatchEvent(new CustomEvent('toggle-points', { detail: { visible: pointsVisible } }));
435
+ }, 200);
436
+
437
+ })();