interace_viewer_separes / interface.js
bilca's picture
Update interface.js
ee2879e verified
// ==============================
// interface.js
// ==============================
// Store a reference to the <script> tag that loaded this file
const currentScriptTag = document.currentScript;
(async function() {
// ─── 1. Locate the <script> and read data-config ───────────────────────────────
let scriptTag = currentScriptTag;
if (!scriptTag) {
const scripts = document.getElementsByTagName('script');
for (let i = 0; i < scripts.length; i++) {
if (scripts[i].src.includes('interface.js') && scripts[i].hasAttribute('data-config')) {
scriptTag = scripts[i];
break;
}
}
if (!scriptTag && scripts.length > 0) {
scriptTag = scripts[scripts.length - 1];
}
}
const configUrl = scriptTag.getAttribute('data-config');
let config = {};
if (configUrl) {
try {
const response = await fetch(configUrl);
config = await response.json();
} catch (error) {
console.error("Error loading config file:", error);
return;
}
} else {
console.error("No config file provided. Please set a data-config attribute on the <script> tag.");
return;
}
// ─── 2. If config.css_url is provided, inject a <link> to that CSS ─────────────
if (config.css_url) {
const linkEl = document.createElement('link');
linkEl.rel = "stylesheet";
linkEl.href = config.css_url;
document.head.appendChild(linkEl);
}
// ─── 3. Generate a unique instanceId for this widget ───────────────────────────
const instanceId = Math.random().toString(36).substr(2, 8);
// ─── 4. Compute the aspect ratio (padding-bottom %) ────────────────────────────
let aspectPercent = "100%";
if (config.aspect) {
if (config.aspect.includes(":")) {
const parts = config.aspect.split(":");
const w = parseFloat(parts[0]);
const h = parseFloat(parts[1]);
if (!isNaN(w) && !isNaN(h) && w > 0) {
aspectPercent = (h / w * 100) + "%";
}
} else {
const aspectValue = parseFloat(config.aspect);
if (!isNaN(aspectValue) && aspectValue > 0) {
aspectPercent = (100 / aspectValue) + "%";
}
}
} else {
const parentContainer = scriptTag.parentNode;
const containerWidth = parentContainer.offsetWidth;
const containerHeight = parentContainer.offsetHeight;
if (containerWidth > 0 && containerHeight > 0) {
aspectPercent = (containerHeight / containerWidth * 100) + "%";
}
}
// ─── 5. Create the widget container (no GIF preview, no close button) ───────────
const widgetContainer = document.createElement('div');
widgetContainer.id = 'ply-widget-container-' + instanceId;
widgetContainer.classList.add('ply-widget-container');
widgetContainer.style.height = "0";
widgetContainer.style.paddingBottom = aspectPercent;
widgetContainer.setAttribute('data-original-aspect', aspectPercent);
// Add the 3D-viewer HTML + β¦Ώ toggle + tooltip + help HTML
widgetContainer.innerHTML = `
<div id="viewer-container-${instanceId}" class="viewer-container">
<div id="progress-dialog-${instanceId}" class="progress-dialog">
<progress id="progress-indicator-${instanceId}" max="100" value="0"></progress>
</div>
<button id="fullscreen-toggle-${instanceId}" class="widget-button fullscreen-toggle">⇱</button>
<button id="help-toggle-${instanceId}" class="widget-button help-toggle">?</button>
<button id="reset-camera-btn-${instanceId}" class="widget-button reset-camera-btn">
<span class="reset-icon">⟲</span>
</button>
<button id="points-toggle-${instanceId}" class="widget-button points-toggle">β¦Ώ</button>
<div id="menu-content-${instanceId}" class="menu-content">
<span id="help-close-${instanceId}" class="help-close">Γ—</span>
<div class="help-text">
</div>
</div>
</div>
<div id="point-tooltip" class="point-tooltip" style="display: none;">
<div class="point-tooltip-content">
<span id="point-tooltip-close" class="point-tooltip-close">Γ—</span>
<div id="point-tooltip-text" class="point-tooltip-text"></div>
<img id="point-tooltip-image" class="point-tooltip-image" src="" alt="" style="display: none;" />
</div>
</div>
`;
// Append the widget container immediately after the <script> tag
scriptTag.parentNode.appendChild(widgetContainer);
// ─── 6. Grab references to new DOM elements ──────────────────────────────────
const viewerContainerElem = document.getElementById('viewer-container-' + instanceId);
const fullscreenToggle = document.getElementById('fullscreen-toggle-' + instanceId);
const helpToggle = document.getElementById('help-toggle-' + instanceId);
const helpCloseBtn = document.getElementById('help-close-' + instanceId);
const resetCameraBtn = document.getElementById('reset-camera-btn-' + instanceId);
const pointsToggleBtn = document.getElementById('points-toggle-' + instanceId);
const menuContent = document.getElementById('menu-content-' + instanceId);
const helpTextDiv = menuContent.querySelector('.help-text');
// Tooltip elements
const tooltipDiv = document.getElementById('point-tooltip');
const tooltipTextDiv = document.getElementById('point-tooltip-text');
const tooltipImage = document.getElementById('point-tooltip-image');
const tooltipCloseBtn = document.getElementById('point-tooltip-close');
// ─── 6a. Detect mobile vs. desktop ────────────────────────────────────────────
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
const isMobile = isIOS || /Android/i.test(navigator.userAgent);
// Conditionally include the French tooltip instruction line if points_url exists
const tooltipInstruction = config.points_url
? '- Cliquez sur β¦Ώ pour afficher/masquer les points d’information.<br>'
: '';
// Fill help text with appropriate instructions
if (isMobile) {
helpTextDiv.innerHTML = `
- Pour vous dΓ©placer, glissez deux doigts sur l'Γ©cran.<br>
- Pour orbiter, utilisez un doigt.<br>
- Pour zoomer, pincez avec deux doigts.<br>
${tooltipInstruction}
`;
} else {
helpTextDiv.innerHTML = `
- orbitez avec le clic droit<br>
- zoomez avec la molette<br>
- dΓ©placez vous avec le clic gauche<br>
${tooltipInstruction}
`;
}
// Ensure instructions panel is visible by default
menuContent.style.display = 'block';
viewerContainerElem.style.display = 'block';
// Variable to hold the drag-hide listener reference
let dragHide = null;
// Utility: hide tooltip and remove drag-hide listener
function hideTooltip() {
if (dragHide) {
viewerContainerElem.removeEventListener('pointermove', dragHide);
dragHide = null;
}
tooltipDiv.style.display = 'none';
}
// Utility: hide instructions panel
function hideHelpPanel() {
menuContent.style.display = 'none';
}
// ─── 7. Dynamically load viewer.js ─────────────────────────────────────────
let viewerModule;
try {
viewerModule = await import('./viewer.js');
await viewerModule.initializeViewer(config, instanceId);
} catch (err) {
console.error("Failed to load viewer.js or initialize the 3D viewer:", err);
return;
}
// After viewer is ready, grab the canvas element
const canvasId = 'canvas-' + instanceId;
const canvasEl = document.getElementById(canvasId);
// ─── 8. Conditional display of points-toggle button ─────────────────────────
if (!config.points_url) {
pointsToggleBtn.style.display = 'none';
} else {
fetch(config.points_url)
.then(resp => {
if (!resp.ok) {
pointsToggleBtn.style.display = 'none';
}
})
.catch(() => {
pointsToggleBtn.style.display = 'none';
});
}
// ─── 9. Fullscreen / state-preservation logic ───────────────────────────────
let isFullscreen = false;
let savedState = null;
function saveCurrentState() {
if (isFullscreen) return;
const computedWidget = window.getComputedStyle(widgetContainer);
const computedViewer = window.getComputedStyle(viewerContainerElem);
const originalAspect = widgetContainer.getAttribute('data-original-aspect') || aspectPercent;
const containerWidth = widgetContainer.offsetWidth;
const containerHeight = widgetContainer.clientHeight || viewerContainerElem.offsetHeight;
const calculatedRatio = (containerHeight / containerWidth * 100) + '%';
savedState = {
widget: {
position: widgetContainer.style.position,
top: widgetContainer.style.top,
left: widgetContainer.style.left,
width: widgetContainer.style.width,
height: widgetContainer.style.height,
maxWidth: widgetContainer.style.maxWidth,
maxHeight: widgetContainer.style.maxHeight,
paddingBottom: widgetContainer.style.paddingBottom || originalAspect,
margin: widgetContainer.style.margin,
aspectPercent: originalAspect,
calculatedAspect: calculatedRatio,
computedWidth: computedWidget.width,
computedHeight: computedWidget.height,
offsetWidth: containerWidth,
offsetHeight: containerHeight
},
viewer: {
borderRadius: viewerContainerElem.style.borderRadius,
border: viewerContainerElem.style.border,
computedWidth: computedViewer.width,
computedHeight: computedViewer.height
}
};
}
function restoreOriginalStyles() {
if (!savedState) return;
let aspectToUse = aspectPercent;
if (savedState.widget.offsetWidth && savedState.widget.offsetHeight) {
const actualRatio = (savedState.widget.offsetHeight / savedState.widget.offsetWidth * 100) + '%';
aspectToUse = actualRatio;
} else if (savedState.widget.calculatedAspect) {
aspectToUse = savedState.widget.calculatedAspect;
} else if (savedState.widget.aspectPercent) {
aspectToUse = savedState.widget.aspectPercent;
} else if (savedState.widget.paddingBottom) {
aspectToUse = savedState.widget.paddingBottom;
}
widgetContainer.style.position = savedState.widget.position || "";
widgetContainer.style.top = savedState.widget.top || "";
widgetContainer.style.left = savedState.widget.left || "";
widgetContainer.style.width = "100%";
widgetContainer.style.height = "0";
widgetContainer.style.maxWidth = savedState.widget.maxWidth || "";
widgetContainer.style.maxHeight = savedState.widget.maxHeight || "";
widgetContainer.style.paddingBottom= aspectToUse;
widgetContainer.style.margin = savedState.widget.margin || "";
widgetContainer.style.border = savedState.widget.border || "";
widgetContainer.style.borderRadius = savedState.widget.borderRadius|| "";
widgetContainer.style.overflow = savedState.widget.overflow || "";
widgetContainer.classList.remove('fake-fullscreen');
viewerContainerElem.style.position = "absolute";
viewerContainerElem.style.top = "0";
viewerContainerElem.style.left = "0";
viewerContainerElem.style.right = "0";
viewerContainerElem.style.bottom = "0";
viewerContainerElem.style.width = "100%";
viewerContainerElem.style.height = "100%";
viewerContainerElem.style.borderRadius= savedState.viewer.borderRadius || "";
viewerContainerElem.style.border = savedState.viewer.border || "";
if (viewerModule.app) {
const cw = viewerContainerElem.clientWidth;
const ch = viewerContainerElem.clientHeight;
viewerModule.app.resizeCanvas(cw, ch);
}
savedState = null;
}
function applyFullscreenStyles() {
widgetContainer.style.position = 'fixed';
widgetContainer.style.top = '0';
widgetContainer.style.left = '0';
widgetContainer.style.width = '100vw';
widgetContainer.style.height = '100vh';
widgetContainer.style.maxWidth = '100vw';
widgetContainer.style.maxHeight = '100vh';
widgetContainer.style.paddingBottom= '0';
widgetContainer.style.margin = '0';
widgetContainer.style.border = 'none';
widgetContainer.style.borderRadius = '0';
viewerContainerElem.style.width = '100%';
viewerContainerElem.style.height = '100%';
viewerContainerElem.style.borderRadius= '0';
viewerContainerElem.style.border = 'none';
if (viewerModule.app) {
viewerModule.app.resizeCanvas(window.innerWidth, window.innerHeight);
}
fullscreenToggle.textContent = '⇲';
isFullscreen = true;
}
function enterFullscreen() {
if (!savedState) {
saveCurrentState();
}
if (isIOS) {
applyFullscreenStyles();
widgetContainer.classList.add('fake-fullscreen');
} else {
if (widgetContainer.requestFullscreen) {
widgetContainer.requestFullscreen()
.then(() => {
applyFullscreenStyles();
})
.catch(err => {
console.error("Fullscreen request failed:", err);
applyFullscreenStyles();
widgetContainer.classList.add('fake-fullscreen');
});
} else if (widgetContainer.webkitRequestFullscreen) {
widgetContainer.webkitRequestFullscreen();
applyFullscreenStyles();
} else if (widgetContainer.mozRequestFullScreen) {
widgetContainer.mozRequestFullScreen();
applyFullscreenStyles();
} else if (widgetContainer.msRequestFullscreen) {
widgetContainer.msRequestFullscreen();
applyFullscreenStyles();
} else {
applyFullscreenStyles();
widgetContainer.classList.add('fake-fullscreen');
}
}
}
function exitFullscreen() {
if (document.fullscreenElement === widgetContainer) {
if (document.exitFullscreen) {
document.exitFullscreen().catch(err => {
console.error("Error exiting fullscreen:", err);
});
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
}
widgetContainer.classList.remove('fake-fullscreen');
restoreOriginalStyles();
isFullscreen = false;
}
// ─── 10. Hook up event listeners ───────────────────────────────────────────
fullscreenToggle.addEventListener('click', () => {
hideTooltip();
if (!isFullscreen) {
enterFullscreen();
} else {
exitFullscreen();
}
});
document.addEventListener('fullscreenchange', () => {
if (document.fullscreenElement === widgetContainer) {
isFullscreen = true;
applyFullscreenStyles();
} else if (isFullscreen) {
isFullscreen = false;
restoreOriginalStyles();
}
});
document.addEventListener('webkitfullscreenchange', () => {
if (document.webkitFullscreenElement === widgetContainer) {
isFullscreen = true;
applyFullscreenStyles();
} else if (isFullscreen) {
isFullscreen = false;
restoreOriginalStyles();
}
});
document.addEventListener('mozfullscreenchange', () => {
if (document.mozFullScreenElement === widgetContainer) {
isFullscreen = true;
applyFullscreenStyles();
} else if (isFullscreen) {
isFullscreen = false;
restoreOriginalStyles();
}
});
document.addEventListener('MSFullscreenChange', () => {
if (document.msFullscreenElement === widgetContainer) {
isFullscreen = true;
applyFullscreenStyles();
} else if (isFullscreen) {
isFullscreen = false;
restoreOriginalStyles();
}
});
helpToggle.addEventListener('click', (e) => {
hideTooltip();
e.stopPropagation();
menuContent.style.display = (menuContent.style.display === 'block') ? 'none' : 'block';
});
helpCloseBtn.addEventListener('click', () => {
hideHelpPanel();
});
resetCameraBtn.addEventListener('click', () => {
hideTooltip();
if (viewerModule.resetViewerCamera) {
viewerModule.resetViewerCamera();
}
});
// β¦Ώ toggle button
let pointsVisible = !!config.showPointsDefault;
if (pointsToggleBtn.style.display !== 'none') {
if (!pointsVisible) {
pointsToggleBtn.style.opacity = '0.5';
}
pointsToggleBtn.addEventListener('click', () => {
hideTooltip();
pointsVisible = !pointsVisible;
pointsToggleBtn.style.opacity = pointsVisible ? '1' : '0.5';
document.dispatchEvent(new CustomEvent('toggle-points', { detail: { visible: pointsVisible } }));
});
}
// Close tooltip on button click
tooltipCloseBtn.addEventListener('click', () => {
hideTooltip();
});
// Listen for point-selection events and show tooltip
document.addEventListener('point-selected', (evt) => {
const { text, imageUrl } = evt.detail;
tooltipTextDiv.textContent = text || "";
if (imageUrl) {
tooltipImage.src = imageUrl;
tooltipImage.style.display = 'block';
} else {
tooltipImage.style.display = 'none';
}
tooltipDiv.style.display = 'flex';
// Attach drag-based hide listener
dragHide = (e) => {
if ((e.pointerType === 'mouse' && e.buttons !== 0) ||
(e.pointerType === 'touch')) {
hideTooltip();
}
};
viewerContainerElem.addEventListener('pointermove', dragHide);
});
// Hide on any wheel event (zoom)
if (canvasEl) {
canvasEl.addEventListener('wheel', () => {
hideTooltip();
}, { passive: true });
}
// Escape key also exits fullscreen
document.addEventListener('keydown', (e) => {
if ((e.key === 'Escape' || e.key === 'Esc') && isFullscreen) {
exitFullscreen();
}
});
// Window resize β†’ resize PlayCanvas canvas
window.addEventListener('resize', () => {
if (isFullscreen && viewerModule.app) {
viewerModule.app.resizeCanvas(window.innerWidth, window.innerHeight);
} else if (viewerModule.app) {
const cw = viewerContainerElem.clientWidth;
const ch = viewerContainerElem.clientHeight;
viewerModule.app.resizeCanvas(cw, ch);
}
});
// Save β€œinitial state” after a brief delay
setTimeout(() => {
saveCurrentState();
document.dispatchEvent(new CustomEvent('toggle-points', { detail: { visible: pointsVisible } }));
}, 200);
})();