MikaFil commited on
Commit
c5da84a
Β·
verified Β·
1 Parent(s): 4c489c0

Create interface.js

Browse files
Files changed (1) hide show
  1. interface.js +510 -0
interface.js ADDED
@@ -0,0 +1,510 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 + help HTML
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">
96
+ <span id="help-close-${instanceId}" class="help-close">Γ—</span>
97
+ <div class="help-text">
98
+ </div>
99
+ </div>
100
+ </div>
101
+ <div id="point-tooltip" class="point-tooltip" style="display: none;">
102
+ <div class="point-tooltip-content">
103
+ <span id="point-tooltip-close" class="point-tooltip-close">Γ—</span>
104
+ <div id="point-tooltip-text" class="point-tooltip-text"></div>
105
+ <img id="point-tooltip-image" class="point-tooltip-image" src="" alt="" style="display: none;" />
106
+ </div>
107
+ </div>
108
+ `;
109
+
110
+ // Append the widget container immediately after the <script> tag
111
+ scriptTag.parentNode.appendChild(widgetContainer);
112
+
113
+ // ─── 6. Grab references to new DOM elements ──────────────────────────────────
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 pointsToggleBtn = document.getElementById('points-toggle-' + instanceId);
120
+ const menuContent = document.getElementById('menu-content-' + instanceId);
121
+ const helpTextDiv = menuContent.querySelector('.help-text');
122
+
123
+ // Tooltip elements
124
+ const tooltipDiv = document.getElementById('point-tooltip');
125
+ const tooltipTextDiv = document.getElementById('point-tooltip-text');
126
+ const tooltipImage = document.getElementById('point-tooltip-image');
127
+ const tooltipCloseBtn = document.getElementById('point-tooltip-close');
128
+
129
+ // ─── 6a. Detect mobile vs. desktop ────────────────────────────────────────────
130
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
131
+ const isMobile = isIOS || /Android/i.test(navigator.userAgent);
132
+
133
+ // Conditionally include the French tooltip instruction line if points_url exists
134
+ const tooltipInstruction = config.points_url
135
+ ? '- Cliquez sur β¦Ώ pour afficher/masquer les points d’information.<br>'
136
+ : '';
137
+
138
+ // Fill help text with appropriate instructions
139
+ if (isMobile) {
140
+ helpTextDiv.innerHTML = `
141
+ - Pour vous dΓ©placer, glissez deux doigts sur l'Γ©cran.<br>
142
+ - Pour orbiter, utilisez un doigt.<br>
143
+ - Pour zoomer, pincez avec deux doigts.<br>
144
+ ${tooltipInstruction}
145
+ `;
146
+ } else {
147
+ helpTextDiv.innerHTML = `
148
+ - orbitez avec le clic droit<br>
149
+ - zoomez avec la molette<br>
150
+ - dΓ©placez vous avec le clic gauche<br>
151
+ ${tooltipInstruction}
152
+ `;
153
+ }
154
+
155
+ // Ensure instructions panel is visible by default
156
+ menuContent.style.display = 'block';
157
+
158
+ viewerContainerElem.style.display = 'block';
159
+
160
+ // Variable to hold the drag-hide listener reference
161
+ let dragHide = null;
162
+
163
+ // Utility: hide tooltip and remove drag-hide listener
164
+ function hideTooltip() {
165
+ if (dragHide) {
166
+ viewerContainerElem.removeEventListener('pointermove', dragHide);
167
+ dragHide = null;
168
+ }
169
+ tooltipDiv.style.display = 'none';
170
+ }
171
+
172
+ // Utility: hide instructions panel
173
+ function hideHelpPanel() {
174
+ menuContent.style.display = 'none';
175
+ }
176
+
177
+ // ─── 7. Dynamically load viewer.js ─────────────────────────────────────────
178
+ let viewerModule;
179
+ try {
180
+ viewerModule = await import('./viewer.js');
181
+ await viewerModule.initializeViewer(config, instanceId);
182
+ } catch (err) {
183
+ console.error("Failed to load viewer.js or initialize the 3D viewer:", err);
184
+ return;
185
+ }
186
+
187
+ // After viewer is ready, grab the canvas element
188
+ const canvasId = 'canvas-' + instanceId;
189
+ const canvasEl = document.getElementById(canvasId);
190
+
191
+ // ─── 8. Conditional display of points-toggle button ─────────────────────────
192
+ if (!config.points_url) {
193
+ pointsToggleBtn.style.display = 'none';
194
+ } else {
195
+ fetch(config.points_url)
196
+ .then(resp => {
197
+ if (!resp.ok) {
198
+ pointsToggleBtn.style.display = 'none';
199
+ }
200
+ })
201
+ .catch(() => {
202
+ pointsToggleBtn.style.display = 'none';
203
+ });
204
+ }
205
+
206
+ // ─── 9. Fullscreen / state-preservation logic ───────────────────────────────
207
+ let isFullscreen = false;
208
+ let savedState = null;
209
+
210
+ function saveCurrentState() {
211
+ if (isFullscreen) return;
212
+ const computedWidget = window.getComputedStyle(widgetContainer);
213
+ const computedViewer = window.getComputedStyle(viewerContainerElem);
214
+ const originalAspect = widgetContainer.getAttribute('data-original-aspect') || aspectPercent;
215
+ const containerWidth = widgetContainer.offsetWidth;
216
+ const containerHeight = widgetContainer.clientHeight || viewerContainerElem.offsetHeight;
217
+ const calculatedRatio = (containerHeight / containerWidth * 100) + '%';
218
+
219
+ savedState = {
220
+ widget: {
221
+ position: widgetContainer.style.position,
222
+ top: widgetContainer.style.top,
223
+ left: widgetContainer.style.left,
224
+ width: widgetContainer.style.width,
225
+ height: widgetContainer.style.height,
226
+ maxWidth: widgetContainer.style.maxWidth,
227
+ maxHeight: widgetContainer.style.maxHeight,
228
+ paddingBottom: widgetContainer.style.paddingBottom || originalAspect,
229
+ margin: widgetContainer.style.margin,
230
+ aspectPercent: originalAspect,
231
+ calculatedAspect: calculatedRatio,
232
+ computedWidth: computedWidget.width,
233
+ computedHeight: computedWidget.height,
234
+ offsetWidth: containerWidth,
235
+ offsetHeight: containerHeight
236
+ },
237
+ viewer: {
238
+ borderRadius: viewerContainerElem.style.borderRadius,
239
+ border: viewerContainerElem.style.border,
240
+ computedWidth: computedViewer.width,
241
+ computedHeight: computedViewer.height
242
+ }
243
+ };
244
+ }
245
+
246
+ function restoreOriginalStyles() {
247
+ if (!savedState) return;
248
+ let aspectToUse = aspectPercent;
249
+ if (savedState.widget.offsetWidth && savedState.widget.offsetHeight) {
250
+ const actualRatio = (savedState.widget.offsetHeight / savedState.widget.offsetWidth * 100) + '%';
251
+ aspectToUse = actualRatio;
252
+ } else if (savedState.widget.calculatedAspect) {
253
+ aspectToUse = savedState.widget.calculatedAspect;
254
+ } else if (savedState.widget.aspectPercent) {
255
+ aspectToUse = savedState.widget.aspectPercent;
256
+ } else if (savedState.widget.paddingBottom) {
257
+ aspectToUse = savedState.widget.paddingBottom;
258
+ }
259
+
260
+ widgetContainer.style.position = savedState.widget.position || "";
261
+ widgetContainer.style.top = savedState.widget.top || "";
262
+ widgetContainer.style.left = savedState.widget.left || "";
263
+ widgetContainer.style.width = "100%";
264
+ widgetContainer.style.height = "0";
265
+ widgetContainer.style.maxWidth = savedState.widget.maxWidth || "";
266
+ widgetContainer.style.maxHeight = savedState.widget.maxHeight || "";
267
+ widgetContainer.style.paddingBottom= aspectToUse;
268
+ widgetContainer.style.margin = savedState.widget.margin || "";
269
+ widgetContainer.style.border = savedState.widget.border || "";
270
+ widgetContainer.style.borderRadius = savedState.widget.borderRadius|| "";
271
+ widgetContainer.style.overflow = savedState.widget.overflow || "";
272
+ widgetContainer.classList.remove('fake-fullscreen');
273
+
274
+ viewerContainerElem.style.position = "absolute";
275
+ viewerContainerElem.style.top = "0";
276
+ viewerContainerElem.style.left = "0";
277
+ viewerContainerElem.style.right = "0";
278
+ viewerContainerElem.style.bottom = "0";
279
+ viewerContainerElem.style.width = "100%";
280
+ viewerContainerElem.style.height = "100%";
281
+ viewerContainerElem.style.borderRadius= savedState.viewer.borderRadius || "";
282
+ viewerContainerElem.style.border = savedState.viewer.border || "";
283
+
284
+ if (viewerModule.app) {
285
+ const cw = viewerContainerElem.clientWidth;
286
+ const ch = viewerContainerElem.clientHeight;
287
+ viewerModule.app.resizeCanvas(cw, ch);
288
+ }
289
+
290
+ savedState = null;
291
+ }
292
+
293
+ function applyFullscreenStyles() {
294
+ widgetContainer.style.position = 'fixed';
295
+ widgetContainer.style.top = '0';
296
+ widgetContainer.style.left = '0';
297
+ widgetContainer.style.width = '100vw';
298
+ widgetContainer.style.height = '100vh';
299
+ widgetContainer.style.maxWidth = '100vw';
300
+ widgetContainer.style.maxHeight = '100vh';
301
+ widgetContainer.style.paddingBottom= '0';
302
+ widgetContainer.style.margin = '0';
303
+ widgetContainer.style.border = 'none';
304
+ widgetContainer.style.borderRadius = '0';
305
+
306
+ viewerContainerElem.style.width = '100%';
307
+ viewerContainerElem.style.height = '100%';
308
+ viewerContainerElem.style.borderRadius= '0';
309
+ viewerContainerElem.style.border = 'none';
310
+
311
+ if (viewerModule.app) {
312
+ viewerModule.app.resizeCanvas(window.innerWidth, window.innerHeight);
313
+ }
314
+
315
+ fullscreenToggle.textContent = '⇲';
316
+ isFullscreen = true;
317
+ }
318
+
319
+ function enterFullscreen() {
320
+ if (!savedState) {
321
+ saveCurrentState();
322
+ }
323
+ if (isIOS) {
324
+ applyFullscreenStyles();
325
+ widgetContainer.classList.add('fake-fullscreen');
326
+ } else {
327
+ if (widgetContainer.requestFullscreen) {
328
+ widgetContainer.requestFullscreen()
329
+ .then(() => {
330
+ applyFullscreenStyles();
331
+ })
332
+ .catch(err => {
333
+ console.error("Fullscreen request failed:", err);
334
+ applyFullscreenStyles();
335
+ widgetContainer.classList.add('fake-fullscreen');
336
+ });
337
+ } else if (widgetContainer.webkitRequestFullscreen) {
338
+ widgetContainer.webkitRequestFullscreen();
339
+ applyFullscreenStyles();
340
+ } else if (widgetContainer.mozRequestFullScreen) {
341
+ widgetContainer.mozRequestFullScreen();
342
+ applyFullscreenStyles();
343
+ } else if (widgetContainer.msRequestFullscreen) {
344
+ widgetContainer.msRequestFullscreen();
345
+ applyFullscreenStyles();
346
+ } else {
347
+ applyFullscreenStyles();
348
+ widgetContainer.classList.add('fake-fullscreen');
349
+ }
350
+ }
351
+ }
352
+
353
+ function exitFullscreen() {
354
+ if (document.fullscreenElement === widgetContainer) {
355
+ if (document.exitFullscreen) {
356
+ document.exitFullscreen().catch(err => {
357
+ console.error("Error exiting fullscreen:", err);
358
+ });
359
+ } else if (document.webkitExitFullscreen) {
360
+ document.webkitExitFullscreen();
361
+ } else if (document.mozCancelFullScreen) {
362
+ document.mozCancelFullScreen();
363
+ } else if (document.msExitFullscreen) {
364
+ document.msExitFullscreen();
365
+ }
366
+ }
367
+
368
+ widgetContainer.classList.remove('fake-fullscreen');
369
+ restoreOriginalStyles();
370
+ isFullscreen = false;
371
+ }
372
+
373
+ // ─── 10. Hook up event listeners ───────────────────────────────────────────
374
+
375
+ fullscreenToggle.addEventListener('click', () => {
376
+ hideTooltip();
377
+ if (!isFullscreen) {
378
+ enterFullscreen();
379
+ } else {
380
+ exitFullscreen();
381
+ }
382
+ });
383
+
384
+ document.addEventListener('fullscreenchange', () => {
385
+ if (document.fullscreenElement === widgetContainer) {
386
+ isFullscreen = true;
387
+ applyFullscreenStyles();
388
+ } else if (isFullscreen) {
389
+ isFullscreen = false;
390
+ restoreOriginalStyles();
391
+ }
392
+ });
393
+ document.addEventListener('webkitfullscreenchange', () => {
394
+ if (document.webkitFullscreenElement === widgetContainer) {
395
+ isFullscreen = true;
396
+ applyFullscreenStyles();
397
+ } else if (isFullscreen) {
398
+ isFullscreen = false;
399
+ restoreOriginalStyles();
400
+ }
401
+ });
402
+ document.addEventListener('mozfullscreenchange', () => {
403
+ if (document.mozFullScreenElement === widgetContainer) {
404
+ isFullscreen = true;
405
+ applyFullscreenStyles();
406
+ } else if (isFullscreen) {
407
+ isFullscreen = false;
408
+ restoreOriginalStyles();
409
+ }
410
+ });
411
+ document.addEventListener('MSFullscreenChange', () => {
412
+ if (document.msFullscreenElement === widgetContainer) {
413
+ isFullscreen = true;
414
+ applyFullscreenStyles();
415
+ } else if (isFullscreen) {
416
+ isFullscreen = false;
417
+ restoreOriginalStyles();
418
+ }
419
+ });
420
+
421
+ helpToggle.addEventListener('click', (e) => {
422
+ hideTooltip();
423
+ e.stopPropagation();
424
+ menuContent.style.display = (menuContent.style.display === 'block') ? 'none' : 'block';
425
+ });
426
+
427
+ helpCloseBtn.addEventListener('click', () => {
428
+ hideHelpPanel();
429
+ });
430
+
431
+ resetCameraBtn.addEventListener('click', () => {
432
+ hideTooltip();
433
+ if (viewerModule.resetViewerCamera) {
434
+ viewerModule.resetViewerCamera();
435
+ }
436
+ });
437
+
438
+ // β¦Ώ toggle button
439
+ let pointsVisible = !!config.showPointsDefault;
440
+ if (pointsToggleBtn.style.display !== 'none') {
441
+ if (!pointsVisible) {
442
+ pointsToggleBtn.style.opacity = '0.5';
443
+ }
444
+ pointsToggleBtn.addEventListener('click', () => {
445
+ hideTooltip();
446
+ pointsVisible = !pointsVisible;
447
+ pointsToggleBtn.style.opacity = pointsVisible ? '1' : '0.5';
448
+ document.dispatchEvent(new CustomEvent('toggle-points', { detail: { visible: pointsVisible } }));
449
+ });
450
+ }
451
+
452
+ // Close tooltip on button click
453
+ tooltipCloseBtn.addEventListener('click', () => {
454
+ hideTooltip();
455
+ });
456
+
457
+ // Listen for point-selection events and show tooltip
458
+ document.addEventListener('point-selected', (evt) => {
459
+ const { text, imageUrl } = evt.detail;
460
+ tooltipTextDiv.textContent = text || "";
461
+ if (imageUrl) {
462
+ tooltipImage.src = imageUrl;
463
+ tooltipImage.style.display = 'block';
464
+ } else {
465
+ tooltipImage.style.display = 'none';
466
+ }
467
+ tooltipDiv.style.display = 'flex';
468
+
469
+ // Attach drag-based hide listener
470
+ dragHide = (e) => {
471
+ if ((e.pointerType === 'mouse' && e.buttons !== 0) ||
472
+ (e.pointerType === 'touch')) {
473
+ hideTooltip();
474
+ }
475
+ };
476
+ viewerContainerElem.addEventListener('pointermove', dragHide);
477
+ });
478
+
479
+ // Hide on any wheel event (zoom)
480
+ if (canvasEl) {
481
+ canvasEl.addEventListener('wheel', () => {
482
+ hideTooltip();
483
+ }, { passive: true });
484
+ }
485
+
486
+ // Escape key also exits fullscreen
487
+ document.addEventListener('keydown', (e) => {
488
+ if ((e.key === 'Escape' || e.key === 'Esc') && isFullscreen) {
489
+ exitFullscreen();
490
+ }
491
+ });
492
+
493
+ // Window resize β†’ resize PlayCanvas canvas
494
+ window.addEventListener('resize', () => {
495
+ if (isFullscreen && viewerModule.app) {
496
+ viewerModule.app.resizeCanvas(window.innerWidth, window.innerHeight);
497
+ } else if (viewerModule.app) {
498
+ const cw = viewerContainerElem.clientWidth;
499
+ const ch = viewerContainerElem.clientHeight;
500
+ viewerModule.app.resizeCanvas(cw, ch);
501
+ }
502
+ });
503
+
504
+ // Save β€œinitial state” after a brief delay
505
+ setTimeout(() => {
506
+ saveCurrentState();
507
+ document.dispatchEvent(new CustomEvent('toggle-points', { detail: { visible: pointsVisible } }));
508
+ }, 200);
509
+
510
+ })();