MikaFil commited on
Commit
1b64a80
Β·
verified Β·
1 Parent(s): bee6833

Update viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +422 -59
viewer.js CHANGED
@@ -1,6 +1,7 @@
1
  // viewer.js
2
  // ==============================
3
- // WhatsApp iOS: Fallback to webgl1 if webgl2 fails
 
4
 
5
  let pc; // will hold the PlayCanvas module once imported
6
  export let app = null;
@@ -14,15 +15,6 @@ let minZoom, maxZoom, minAngle, maxAngle, minAzimuth, maxAzimuth, minPivotY, min
14
  let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRotationZ;
15
  let plyUrl, glbUrl;
16
 
17
- function isWhatsappIOS() {
18
- return /iphone|ipod|ipad/i.test(navigator.userAgent) &&
19
- /WhatsApp/i.test(navigator.userAgent);
20
- }
21
- function isInstagramIOS() {
22
- return /iphone|ipod|ipad/i.test(navigator.userAgent) &&
23
- /Instagram/i.test(navigator.userAgent);
24
- }
25
-
26
  /**
27
  * initializeViewer(config, instanceId)
28
  */
@@ -31,15 +23,6 @@ export async function initializeViewer(config, instanceId) {
31
 
32
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
33
  const isMobile = isIOS || /Android/i.test(navigator.userAgent);
34
- const isWA = isWhatsappIOS();
35
- const isIG = isInstagramIOS();
36
-
37
- if (isWA) {
38
- alert("WhatsApp iOS detected: trying WebGL fallback.");
39
- }
40
- if (isIG) {
41
- // Optional: alert("Instagram iOS detected.");
42
- }
43
 
44
  // 1. Read config
45
  plyUrl = config.ply_url;
@@ -112,43 +95,14 @@ export async function initializeViewer(config, instanceId) {
112
 
113
  try {
114
  // 6. Setup device & app
115
- let device = null;
116
- let contextSuccess = false;
117
- let contextType = null;
118
-
119
- // Try webgl2, then webgl1
120
- try {
121
- device = await pc.createGraphicsDevice(canvas, {
122
- deviceTypes: ["webgl2"],
123
- glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
124
- twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
125
- antialias: false
126
- });
127
- device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
128
- contextType = "webgl2";
129
- contextSuccess = true;
130
- alert("WebGL2 context creation successful.");
131
- } catch (err) {
132
- // Try webgl1 if webgl2 fails
133
- try {
134
- device = await pc.createGraphicsDevice(canvas, {
135
- deviceTypes: ["webgl1"],
136
- glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
137
- twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
138
- antialias: false
139
- });
140
- device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
141
- contextType = "webgl1";
142
- contextSuccess = true;
143
- alert("WebGL1 context creation successful (WebGL2 failed).");
144
- } catch (err2) {
145
- alert("WebGL context creation FAILED.\nWhatsApp iOS browser does not support WebGL. Try opening this link in Safari or another browser.");
146
- progressDialog.innerHTML = `<p style="color: red">WebGL is not supported in this browser. Please try opening in Safari or Chrome.</p>`;
147
- throw err2;
148
- }
149
- }
150
 
151
- // ... continue as before ...
152
  const opts = new pc.AppOptions();
153
  opts.graphicsDevice = device;
154
  opts.mouse = new pc.Mouse(canvas);
@@ -172,6 +126,8 @@ export async function initializeViewer(config, instanceId) {
172
  app = new pc.Application(canvas, opts);
173
  app.setCanvasFillMode(pc.FILLMODE_NONE);
174
  app.setCanvasResolution(pc.RESOLUTION_AUTO);
 
 
175
 
176
  // Attach ResizeObserver to keep canvas in sync with container size
177
  resizeObserver = new ResizeObserver(entries => {
@@ -197,7 +153,7 @@ export async function initializeViewer(config, instanceId) {
197
  orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
198
  galerie: new pc.Asset('galerie', 'container', { url: glbUrl }),
199
  hdr: new pc.Asset('hdr', 'texture', {
200
- url: `https://huggingface.co/datasets/bilca/ply_files/resolve/main/galeries/blanc.png`
201
  }, { type: pc.TEXTURETYPE_RGBP, mipmaps: false })
202
  };
203
 
@@ -206,7 +162,7 @@ export async function initializeViewer(config, instanceId) {
206
  assets.model.on('load', () => progressDialog.style.display = 'none');
207
  assets.model.on('error', err => {
208
  console.error("Error loading PLY file:", err);
209
- progressDialog.innerHTML = `<p style="color: red">Error loading model: ${err}</p>`;
210
  });
211
 
212
  const progCheck = setInterval(() => {
@@ -263,6 +219,7 @@ export async function initializeViewer(config, instanceId) {
263
  clearColor: config.canvas_background
264
  ? parseInt(config.canvas_background.substr(1, 2), 16) / 255
265
  : 0,
 
266
  });
267
  cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
268
  cameraEntity.lookAt(modelEntity.getPosition());
@@ -334,11 +291,10 @@ export async function initializeViewer(config, instanceId) {
334
 
335
  } catch (error) {
336
  console.error("Error initializing PlayCanvas viewer:", error);
337
- progressDialog.innerHTML = `<p style="color: red">Error loading viewer: ${error.message}</p>`;
338
  }
339
  }
340
 
341
- // (resetViewerCamera and cleanupViewer unchanged)
342
  export function resetViewerCamera() {
343
  try {
344
  if (!cameraEntity || !modelEntity || !app) return;
@@ -402,3 +358,410 @@ export function cleanupViewer() {
402
  resizeObserver = null;
403
  }
404
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  // viewer.js
2
  // ==============================
3
+ // viewer.js
4
+ // ==============================
5
 
6
  let pc; // will hold the PlayCanvas module once imported
7
  export let app = null;
 
15
  let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRotationZ;
16
  let plyUrl, glbUrl;
17
 
 
 
 
 
 
 
 
 
 
18
  /**
19
  * initializeViewer(config, instanceId)
20
  */
 
23
 
24
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
25
  const isMobile = isIOS || /Android/i.test(navigator.userAgent);
 
 
 
 
 
 
 
 
 
26
 
27
  // 1. Read config
28
  plyUrl = config.ply_url;
 
95
 
96
  try {
97
  // 6. Setup device & app
98
+ const device = await pc.createGraphicsDevice(canvas, {
99
+ deviceTypes: ["webgl2"],
100
+ glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
101
+ twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
102
+ antialias: false
103
+ });
104
+ device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
 
106
  const opts = new pc.AppOptions();
107
  opts.graphicsDevice = device;
108
  opts.mouse = new pc.Mouse(canvas);
 
126
  app = new pc.Application(canvas, opts);
127
  app.setCanvasFillMode(pc.FILLMODE_NONE);
128
  app.setCanvasResolution(pc.RESOLUTION_AUTO);
129
+ //app.scene.exposure = 0.5;
130
+ //app.scene.toneMapping = pc.TONEMAP_ACES;
131
 
132
  // Attach ResizeObserver to keep canvas in sync with container size
133
  resizeObserver = new ResizeObserver(entries => {
 
153
  orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
154
  galerie: new pc.Asset('galerie', 'container', { url: glbUrl }),
155
  hdr: new pc.Asset('hdr', 'texture', {
156
+ url: https://huggingface.co/datasets/bilca/ply_files/resolve/main/galeries/blanc.png
157
  }, { type: pc.TEXTURETYPE_RGBP, mipmaps: false })
158
  };
159
 
 
162
  assets.model.on('load', () => progressDialog.style.display = 'none');
163
  assets.model.on('error', err => {
164
  console.error("Error loading PLY file:", err);
165
+ progressDialog.innerHTML = <p style="color: red">Error loading model: ${err}</p>;
166
  });
167
 
168
  const progCheck = setInterval(() => {
 
219
  clearColor: config.canvas_background
220
  ? parseInt(config.canvas_background.substr(1, 2), 16) / 255
221
  : 0,
222
+ //toneMapping: pc.TONEMAP_ACES
223
  });
224
  cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
225
  cameraEntity.lookAt(modelEntity.getPosition());
 
291
 
292
  } catch (error) {
293
  console.error("Error initializing PlayCanvas viewer:", error);
294
+ progressDialog.innerHTML = <p style="color: red">Error loading viewer: ${error.message}</p>;
295
  }
296
  }
297
 
 
298
  export function resetViewerCamera() {
299
  try {
300
  if (!cameraEntity || !modelEntity || !app) return;
 
358
  resizeObserver = null;
359
  }
360
  }
361
+
362
+ interface.js :
363
+
364
+ // interface.js
365
+ // ==============================
366
+ // interface.js
367
+ // ==============================
368
+
369
+ // Store a reference to the <script> tag that loaded this file
370
+ const currentScriptTag = document.currentScript;
371
+
372
+ (async function() {
373
+ // ─── 1. Locate the <script> and read data-config ───────────────────────────────
374
+ let scriptTag = currentScriptTag;
375
+ if (!scriptTag) {
376
+ const scripts = document.getElementsByTagName('script');
377
+ for (let i = 0; i < scripts.length; i++) {
378
+ if (scripts[i].src.includes('interface.js') && scripts[i].hasAttribute('data-config')) {
379
+ scriptTag = scripts[i];
380
+ break;
381
+ }
382
+ }
383
+ if (!scriptTag && scripts.length > 0) {
384
+ scriptTag = scripts[scripts.length - 1];
385
+ }
386
+ }
387
+
388
+ const configUrl = scriptTag.getAttribute('data-config');
389
+ let config = {};
390
+ if (configUrl) {
391
+ try {
392
+ const response = await fetch(configUrl);
393
+ config = await response.json();
394
+ } catch (error) {
395
+ console.error("Error loading config file:", error);
396
+ return;
397
+ }
398
+ } else {
399
+ console.error("No config file provided. Please set a data-config attribute on the <script> tag.");
400
+ return;
401
+ }
402
+
403
+ // ─── 2. If config.css_url is provided, inject a <link> to that CSS ─────────────
404
+ if (config.css_url) {
405
+ const linkEl = document.createElement('link');
406
+ linkEl.rel = "stylesheet";
407
+ linkEl.href = config.css_url;
408
+ document.head.appendChild(linkEl);
409
+ }
410
+
411
+ // ─── 3. Generate a unique instanceId for this widget ───────────────────────────
412
+ const instanceId = Math.random().toString(36).substr(2, 8);
413
+
414
+ // ─── 4. Compute the aspect ratio (padding-bottom %) ────────────────────────────
415
+ let aspectPercent = "100%";
416
+ if (config.aspect) {
417
+ if (config.aspect.includes(":")) {
418
+ const parts = config.aspect.split(":");
419
+ const w = parseFloat(parts[0]);
420
+ const h = parseFloat(parts[1]);
421
+ if (!isNaN(w) && !isNaN(h) && w > 0) {
422
+ aspectPercent = (h / w * 100) + "%";
423
+ }
424
+ } else {
425
+ const aspectValue = parseFloat(config.aspect);
426
+ if (!isNaN(aspectValue) && aspectValue > 0) {
427
+ aspectPercent = (100 / aspectValue) + "%";
428
+ }
429
+ }
430
+ } else {
431
+ const parentContainer = scriptTag.parentNode;
432
+ const containerWidth = parentContainer.offsetWidth;
433
+ const containerHeight = parentContainer.offsetHeight;
434
+ if (containerWidth > 0 && containerHeight > 0) {
435
+ aspectPercent = (containerHeight / containerWidth * 100) + "%";
436
+ }
437
+ }
438
+
439
+ // ─── 5. Create the widget container (no GIF preview, no close button) ───────────
440
+ const widgetContainer = document.createElement('div');
441
+ widgetContainer.id = 'ply-widget-container-' + instanceId;
442
+ widgetContainer.classList.add('ply-widget-container');
443
+ widgetContainer.style.height = "0";
444
+ widgetContainer.style.paddingBottom = aspectPercent;
445
+ widgetContainer.setAttribute('data-original-aspect', aspectPercent);
446
+
447
+ // Conditionally include the β€œtooltips-toggle” button only if config.tooltips_url is defined
448
+ const tooltipsButtonHTML = config.tooltips_url
449
+ ? <button id="tooltips-toggle-${instanceId}" class="widget-button tooltips-toggle">β¦Ώ</button>
450
+ : '';
451
+
452
+ // Add the 3D-viewer HTML + tooltip + help HTML
453
+ widgetContainer.innerHTML =
454
+ <div id="viewer-container-${instanceId}" class="viewer-container">
455
+ <div id="progress-dialog-${instanceId}" class="progress-dialog">
456
+ <progress id="progress-indicator-${instanceId}" max="100" value="0"></progress>
457
+ </div>
458
+ <button id="fullscreen-toggle-${instanceId}" class="widget-button fullscreen-toggle">⇱</button>
459
+ <button id="help-toggle-${instanceId}" class="widget-button help-toggle">?</button>
460
+ <button id="reset-camera-btn-${instanceId}" class="widget-button reset-camera-btn">
461
+ <span class="reset-icon">⟲</span>
462
+ </button>
463
+ ${tooltipsButtonHTML}
464
+ <div id="menu-content-${instanceId}" class="menu-content">
465
+ <span id="help-close-${instanceId}" class="help-close">Γ—</span>
466
+ <div class="help-text"></div>
467
+ </div>
468
+ </div>
469
+ <div id="tooltip-panel" class="tooltip-panel" style="display: none;">
470
+ <div class="tooltip-content">
471
+ <span id="tooltip-close" class="tooltip-close">Γ—</span>
472
+ <div id="tooltip-text" class="tooltip-text"></div>
473
+ <img id="tooltip-image" class="tooltip-image" src="" alt="" style="display: none;" />
474
+ </div>
475
+ </div>
476
+ ;
477
+
478
+ // Append the widget container immediately after the <script> tag
479
+ scriptTag.parentNode.appendChild(widgetContainer);
480
+
481
+ // ─── 6. Grab references to new DOM elements ──────────────────────────────────
482
+ const viewerContainerElem = document.getElementById('viewer-container-' + instanceId);
483
+ const fullscreenToggle = document.getElementById('fullscreen-toggle-' + instanceId);
484
+ const helpToggle = document.getElementById('help-toggle-' + instanceId);
485
+ const helpCloseBtn = document.getElementById('help-close-' + instanceId);
486
+ const resetCameraBtn = document.getElementById('reset-camera-btn-' + instanceId);
487
+ const tooltipsToggleBtn = document.getElementById('tooltips-toggle-' + instanceId);
488
+ const menuContent = document.getElementById('menu-content-' + instanceId);
489
+ const helpTextDiv = menuContent.querySelector('.help-text');
490
+
491
+ // Tooltip panel elements
492
+ const tooltipPanel = document.getElementById('tooltip-panel');
493
+ const tooltipTextDiv = document.getElementById('tooltip-text');
494
+ const tooltipImage = document.getElementById('tooltip-image');
495
+ const tooltipCloseBtn = document.getElementById('tooltip-close');
496
+
497
+ // ─── 6a. Detect mobile vs. desktop ────────────────────────────────────────────
498
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
499
+ const isMobile = isIOS || /Android/i.test(navigator.userAgent);
500
+
501
+ // Conditionally include the French tooltip instruction line if tooltips_url exists
502
+ const tooltipInstruction = config.tooltips_url
503
+ ? '- Cliquez sur β¦Ώ pour afficher/masquer les tooltips.<br>'
504
+ : '';
505
+
506
+ // Fill help text with instructions plus the two new French lines
507
+ if (isMobile) {
508
+ helpTextDiv.innerHTML =
509
+ - Pour vous dΓ©placer, glissez deux doigts sur l'Γ©cran.<br>
510
+ - Pour orbiter, utilisez un doigt.<br>
511
+ - Pour zoomer, pincez avec deux doigts.<br>
512
+ ${tooltipInstruction}
513
+ - ⟲ Réinitialise la caméra.<br>
514
+ - ⇱ Passe en plein Γ©cran.<br>
515
+ ;
516
+ } else {
517
+ helpTextDiv.innerHTML =
518
+ - orbitez avec le clic droit<br>
519
+ - zoomez avec la molette<br>
520
+ - dΓ©placez vous avec le clic gauche<br>
521
+ ${tooltipInstruction}
522
+ - ⟲ Réinitialise la caméra.<br>
523
+ - ⇱ Passe en plein Γ©cran.<br>
524
+ ;
525
+ }
526
+
527
+ // Ensure instructions panel is visible by default
528
+ menuContent.style.display = 'block';
529
+ viewerContainerElem.style.display = 'block';
530
+
531
+ // Variable to hold the drag-hide listener reference
532
+ let dragHide = null;
533
+
534
+ // Utilities to hide panels
535
+ function hideTooltipPanel() {
536
+ if (dragHide) {
537
+ viewerContainerElem.removeEventListener('pointermove', dragHide);
538
+ dragHide = null;
539
+ }
540
+ tooltipPanel.style.display = 'none';
541
+ }
542
+ function hideHelpPanel() {
543
+ menuContent.style.display = 'none';
544
+ }
545
+
546
+ // ─── 7. Dynamically load viewer.js ─────────────────────────────────────────
547
+ let viewerModule;
548
+ try {
549
+ viewerModule = await import('https://mikafil-viewer-gs.static.hf.space/viewer.js');
550
+ await viewerModule.initializeViewer(config, instanceId);
551
+ } catch (err) {
552
+ console.error("Failed to load viewer.js or initialize the 3D viewer:", err);
553
+ return;
554
+ }
555
+
556
+ const canvasId = 'canvas-' + instanceId;
557
+ const canvasEl = document.getElementById(canvasId);
558
+
559
+ // ─── 8. Conditional display of tooltips-toggle button ─────────────────────────
560
+ if (tooltipsToggleBtn) {
561
+ if (!config.tooltips_url) {
562
+ tooltipsToggleBtn.style.display = 'none';
563
+ } else {
564
+ fetch(config.tooltips_url)
565
+ .then(resp => { if (!resp.ok) tooltipsToggleBtn.style.display = 'none'; })
566
+ .catch(() => { tooltipsToggleBtn.style.display = 'none'; });
567
+ }
568
+ }
569
+
570
+ // ─── 9. Fullscreen / state-preservation logic ───────────────────────────────
571
+ let isFullscreen = false;
572
+ let savedState = null;
573
+
574
+ function saveCurrentState() {
575
+ if (isFullscreen) return;
576
+ const computedWidget = window.getComputedStyle(widgetContainer);
577
+ const computedViewer = window.getComputedStyle(viewerContainerElem);
578
+ const originalAspect = widgetContainer.getAttribute('data-original-aspect') || aspectPercent;
579
+ savedState = {
580
+ widget: {
581
+ position: widgetContainer.style.position,
582
+ top: widgetContainer.style.top,
583
+ left: widgetContainer.style.left,
584
+ width: widgetContainer.style.width,
585
+ height: widgetContainer.style.height,
586
+ maxWidth: widgetContainer.style.maxWidth,
587
+ maxHeight:widgetContainer.style.maxHeight,
588
+ paddingBottom: widgetContainer.style.paddingBottom || originalAspect,
589
+ margin: widgetContainer.style.margin,
590
+ },
591
+ viewer: {
592
+ borderRadius: viewerContainerElem.style.borderRadius,
593
+ border: viewerContainerElem.style.border,
594
+ }
595
+ };
596
+ }
597
+
598
+ function restoreOriginalStyles() {
599
+ if (!savedState) return;
600
+ const aspectToUse = savedState.widget.paddingBottom;
601
+ widgetContainer.style.position = savedState.widget.position || "";
602
+ widgetContainer.style.top = savedState.widget.top || "";
603
+ widgetContainer.style.left = savedState.widget.left || "";
604
+ widgetContainer.style.width = "100%";
605
+ widgetContainer.style.height = "0";
606
+ widgetContainer.style.maxWidth = savedState.widget.maxWidth || "";
607
+ widgetContainer.style.maxHeight = savedState.widget.maxHeight || "";
608
+ widgetContainer.style.paddingBottom= aspectToUse;
609
+ widgetContainer.style.margin = savedState.widget.margin || "";
610
+ widgetContainer.classList.remove('fake-fullscreen');
611
+
612
+ viewerContainerElem.style.position = "absolute";
613
+ viewerContainerElem.style.top = "0";
614
+ viewerContainerElem.style.left = "0";
615
+ viewerContainerElem.style.right = "0";
616
+ viewerContainerElem.style.bottom = "0";
617
+ viewerContainerElem.style.width = "100%";
618
+ viewerContainerElem.style.height = "100%";
619
+ viewerContainerElem.style.borderRadius = savedState.viewer.borderRadius || "";
620
+ viewerContainerElem.style.border = savedState.viewer.border || "";
621
+
622
+ if (viewerModule.app) {
623
+ viewerModule.app.resizeCanvas(
624
+ viewerContainerElem.clientWidth,
625
+ viewerContainerElem.clientHeight
626
+ );
627
+ }
628
+
629
+ savedState = null;
630
+ }
631
+
632
+ function applyFullscreenStyles() {
633
+ widgetContainer.style.position = 'fixed';
634
+ widgetContainer.style.top = '0';
635
+ widgetContainer.style.left = '0';
636
+ widgetContainer.style.width = '100vw';
637
+ widgetContainer.style.height = '100vh';
638
+ widgetContainer.style.maxWidth = '100vw';
639
+ widgetContainer.style.maxHeight = '100vh';
640
+ widgetContainer.style.paddingBottom = '0';
641
+ widgetContainer.style.margin = '0';
642
+ widgetContainer.style.border = 'none';
643
+ widgetContainer.style.borderRadius = '0';
644
+
645
+ viewerContainerElem.style.width = '100%';
646
+ viewerContainerElem.style.height = '100%';
647
+ viewerContainerElem.style.borderRadius= '0';
648
+ viewerContainerElem.style.border = 'none';
649
+
650
+ if (viewerModule.app) {
651
+ viewerModule.app.resizeCanvas(window.innerWidth, window.innerHeight);
652
+ }
653
+
654
+ fullscreenToggle.textContent = '⇲';
655
+ isFullscreen = true;
656
+ }
657
+
658
+ function enterFullscreen() {
659
+ if (!savedState) saveCurrentState();
660
+ if (isIOS) {
661
+ applyFullscreenStyles();
662
+ widgetContainer.classList.add('fake-fullscreen');
663
+ } else if (widgetContainer.requestFullscreen) {
664
+ widgetContainer.requestFullscreen()
665
+ .then(applyFullscreenStyles)
666
+ .catch(() => {
667
+ applyFullscreenStyles();
668
+ widgetContainer.classList.add('fake-fullscreen');
669
+ });
670
+ } else {
671
+ applyFullscreenStyles();
672
+ widgetContainer.classList.add('fake-fullscreen');
673
+ }
674
+ }
675
+
676
+ function exitFullscreen() {
677
+ if (document.fullscreenElement === widgetContainer && document.exitFullscreen) {
678
+ document.exitFullscreen().catch(() => {});
679
+ }
680
+ widgetContainer.classList.remove('fake-fullscreen');
681
+ restoreOriginalStyles();
682
+ isFullscreen = false;
683
+ }
684
+
685
+ // ─── 10. Hook up event listeners ───────────────────────────────────────────
686
+ fullscreenToggle.addEventListener('click', () => {
687
+ hideTooltipPanel();
688
+ isFullscreen ? exitFullscreen() : enterFullscreen();
689
+ });
690
+ document.addEventListener('fullscreenchange', () => {
691
+ if (!document.fullscreenElement && isFullscreen) {
692
+ isFullscreen = false;
693
+ restoreOriginalStyles();
694
+ }
695
+ });
696
+
697
+ helpToggle.addEventListener('click', (e) => {
698
+ hideTooltipPanel();
699
+ e.stopPropagation();
700
+ menuContent.style.display = menuContent.style.display === 'block' ? 'none' : 'block';
701
+ });
702
+ helpCloseBtn.addEventListener('click', hideHelpPanel);
703
+
704
+ resetCameraBtn.addEventListener('click', () => {
705
+ hideTooltipPanel();
706
+ if (viewerModule.resetViewerCamera) {
707
+ viewerModule.resetViewerCamera();
708
+ }
709
+ });
710
+
711
+ if (tooltipsToggleBtn) {
712
+ let tooltipsVisible = !!config.showTooltipsDefault;
713
+ tooltipsToggleBtn.style.opacity = tooltipsVisible ? '1' : '0.5';
714
+ tooltipsToggleBtn.addEventListener('click', () => {
715
+ hideTooltipPanel();
716
+ tooltipsVisible = !tooltipsVisible;
717
+ tooltipsToggleBtn.style.opacity = tooltipsVisible ? '1' : '0.5';
718
+ document.dispatchEvent(new CustomEvent('toggle-tooltips', { detail: { visible: tooltipsVisible } }));
719
+ });
720
+ }
721
+
722
+ tooltipCloseBtn.addEventListener('click', hideTooltipPanel);
723
+
724
+ document.addEventListener('tooltip-selected', (evt) => {
725
+ const { title, description, imgUrl } = evt.detail;
726
+ tooltipTextDiv.innerHTML = <strong>${title}</strong><br>${description};
727
+ if (imgUrl) {
728
+ tooltipImage.src = imgUrl;
729
+ tooltipImage.style.display = 'block';
730
+ } else {
731
+ tooltipImage.style.display = 'none';
732
+ }
733
+ tooltipPanel.style.display = 'flex';
734
+ dragHide = (e) => {
735
+ if ((e.pointerType === 'mouse' && e.buttons !== 0) || e.pointerType === 'touch') {
736
+ hideTooltipPanel();
737
+ }
738
+ };
739
+ viewerContainerElem.addEventListener('pointermove', dragHide);
740
+ });
741
+
742
+ if (canvasEl) {
743
+ canvasEl.addEventListener('wheel', hideTooltipPanel, { passive: true });
744
+ }
745
+ document.addEventListener('keydown', (e) => {
746
+ if ((e.key === 'Escape' || e.key === 'Esc') && isFullscreen) exitFullscreen();
747
+ });
748
+ window.addEventListener('resize', () => {
749
+ if (viewerModule.app) {
750
+ if (isFullscreen) {
751
+ viewerModule.app.resizeCanvas(window.innerWidth, window.innerHeight);
752
+ } else {
753
+ viewerModule.app.resizeCanvas(
754
+ viewerContainerElem.clientWidth,
755
+ viewerContainerElem.clientHeight
756
+ );
757
+ }
758
+ }
759
+ });
760
+
761
+ // Save β€œinitial state” after a brief delay
762
+ setTimeout(() => {
763
+ saveCurrentState();
764
+ document.dispatchEvent(new CustomEvent('toggle-tooltips', { detail: { visible: !!config.showTooltipsDefault } }));
765
+ }, 200);
766
+
767
+ })();