viewer_glb / interface.js
MikaFil's picture
Update interface.js
b4b4422 verified
// interface.js
// ==============================
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) {
return;
}
} else {
return;
}
if (config.css_url) {
const linkEl = document.createElement('link');
linkEl.rel = "stylesheet";
linkEl.href = config.css_url;
document.head.appendChild(linkEl);
}
const instanceId = Math.random().toString(36).substr(2, 8);
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) + "%";
}
}
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);
// TOOLTIP BUTTON HTML IF NEEDED
const tooltipsButtonHTML = config.tooltips_url
? `<button id="tooltips-toggle-${instanceId}" class="widget-button tooltips-toggle" title="Tooltips" aria-label="Afficher/Masquer les tooltips">⦿</button>`
: '';
// COLOR BUTTON: will open the color panel
const colorButtonId = `color-btn-${instanceId}`;
const colorPanelId = `color-panel-${instanceId}`;
const PALETTE_IMG_URL = 'https://huggingface.co/datasets/MikaFil/3D_models/resolve/main/EARCARE/images/palette.png';
// Color choices: add more as needed
const colorChoices = Array.isArray(config.colorChoices) ? config.colorChoices : [];
// Generate color swatch HTML
const colorPanelHTML = `
<div id="${colorPanelId}" class="color-panel" aria-label="Sélection de couleur" tabindex="-1" style="display:none;">
${colorChoices.map((c, i) => `
<button
class="color-swatch-btn"
style="background: ${c.swatch};"
data-dr="${c.color[0]}" data-dg="${c.color[1]}" data-db="${c.color[2]}"
data-er="${c.emit[0]}" data-eg="${c.emit[1]}" data-eb="${c.emit[2]}"
data-ei= "${c.emitI}"
data-op="${c.op}"
data-boolTrans="${c.boolTrans}"
aria-label="${c.title}"
title="${c.title}"
tabindex="0"
></button>
`).join("")}
</div>
`;
// --- Controls as vertical stack, with color button to the left (first row is horizontal) ---
widgetContainer.innerHTML = `
<div id="viewer-container-${instanceId}" class="viewer-container">
<div class="controls-row">
<button id="${colorButtonId}" class="widget-button color-menu-btn" aria-label="Ouvrir le menu de couleurs" title="Couleur">
<img src="${PALETTE_IMG_URL}" alt="palette" class="palette-btn-img" draggable="false">
</button>
<button id="fullscreen-toggle-${instanceId}" class="widget-button fullscreen-toggle" title="Plein écran" aria-label="Plein écran">⇱</button>
<button id="help-toggle-${instanceId}" class="widget-button help-toggle" title="Aide" aria-label="Aide">?</button>
<button id="reset-camera-btn-${instanceId}" class="widget-button reset-camera-btn" title="Réinitialiser la caméra" aria-label="Réinitialiser la caméra">
<span class="reset-icon">⟲</span>
</button>
${tooltipsButtonHTML}
${colorPanelHTML}
</div>
<div id="progress-dialog-${instanceId}" class="progress-dialog">
<progress id="progress-indicator-${instanceId}" max="100" value="0"></progress>
</div>
<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="tooltip-panel" class="tooltip-panel" style="display: none;">
<div class="tooltip-content">
<span id="tooltip-close" class="tooltip-close">×</span>
<div id="tooltip-text" class="tooltip-text"></div>
<img id="tooltip-image" class="tooltip-image" src="" alt="" style="display: none;" />
</div>
</div>
`;
scriptTag.parentNode.appendChild(widgetContainer);
// ---- Element references ----
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 tooltipsToggleBtn = document.getElementById('tooltips-toggle-' + instanceId);
const menuContent = document.getElementById('menu-content-' + instanceId);
const helpTextDiv = menuContent.querySelector('.help-text');
const tooltipPanel = document.getElementById('tooltip-panel');
const tooltipTextDiv = document.getElementById('tooltip-text');
const tooltipImage = document.getElementById('tooltip-image');
const tooltipCloseBtn = document.getElementById('tooltip-close');
const colorMenuBtn = document.getElementById(colorButtonId);
const colorPanelElem = document.getElementById(colorPanelId);
const colorSwatchBtns = colorPanelElem.querySelectorAll('.color-swatch-btn');
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
const isMobile = isIOS || /Android/i.test(navigator.userAgent);
const tooltipInstruction = config.tooltips_url
? '- Cliquez sur ⦿ pour afficher/masquer les tooltips.<br>'
: '';
// ==== MODIFIED HELP TEXT, with PNG palette image in helptext =====
const paletteHelpLine =
`- Cliquez sur <img src="${PALETTE_IMG_URL}" alt="palette" class="inline-palette" draggable="false"> pour ouvrir le menu de couleurs.<br>`;
const resetHelpLine = '- Cliquez sur ⟲ pour réinitialiser la caméra.<br>';
if (isMobile) {
helpTextDiv.innerHTML =
'- Déplacez vous en glissant deux doigts sur l\'écran.<br>' +
'- Orbitez en glissant un doigt.<br>' +
'- Zoomez en pinçant avec deux doigts.<br>' +
tooltipInstruction +
paletteHelpLine +
resetHelpLine +
'- Cliquez sur ⇱ pour passer en plein écran.<br>';
} else {
helpTextDiv.innerHTML =
'- Orbitez avec le clic droit<br>' +
'- Zoomez avec la molette<br>' +
'- Déplacez vous avec le clic gauche<br>' +
tooltipInstruction +
paletteHelpLine +
resetHelpLine +
'- Cliquez sur ⇱ pour passer en plein écran.<br>';
}
// ==== END HELP TEXT MOD =====
// --- DYNAMIC MENU SIZING ---
function setMenuContentMaxSize() {
if (!isMobile) {
menuContent.style.maxWidth = "";
menuContent.style.maxHeight = "";
menuContent.style.width = "";
menuContent.style.height = "";
menuContent.style.overflowY = "";
menuContent.style.overflowX = "";
return;
}
let parent = viewerContainerElem;
if (parent) {
const vw = parent.offsetWidth;
const vh = parent.offsetHeight;
if (vw && vh) {
menuContent.style.maxWidth = Math.round(vw * 0.8) + "px";
menuContent.style.maxHeight = Math.round(vh * 0.8) + "px";
menuContent.style.width = ""; // Let it shrink if smaller
menuContent.style.height = "";
menuContent.style.overflowY = "auto";
menuContent.style.overflowX = "auto";
} else {
menuContent.style.maxWidth = "80vw";
menuContent.style.maxHeight = "80vh";
menuContent.style.overflowY = "auto";
menuContent.style.overflowX = "auto";
}
}
}
setMenuContentMaxSize();
window.addEventListener('resize', setMenuContentMaxSize);
document.addEventListener('fullscreenchange', setMenuContentMaxSize);
window.addEventListener('orientationchange', setMenuContentMaxSize);
// --- HELP PANEL DEFAULT VISIBILITY ---
menuContent.style.display = 'block';
viewerContainerElem.style.display = 'block';
let dragHide = null;
function hideTooltipPanel() {
if (dragHide) {
viewerContainerElem.removeEventListener('pointermove', dragHide);
dragHide = null;
}
tooltipPanel.style.display = 'none';
}
function hideHelpPanel() {
menuContent.style.display = 'none';
}
// 7. Dynamically load viewer.js
let viewerModule;
try {
viewerModule = await import('https://mikafil-viewer-glb.static.hf.space/viewer.js');
await viewerModule.initializeViewer(config, instanceId);
} catch (err) {
return;
}
const canvasId = 'canvas-' + instanceId;
const canvasEl = document.getElementById(canvasId);
if (tooltipsToggleBtn) {
if (!config.tooltips_url) {
tooltipsToggleBtn.style.display = 'none';
} else {
fetch(config.tooltips_url)
.then(resp => { if (!resp.ok) tooltipsToggleBtn.style.display = 'none'; })
.catch(() => { tooltipsToggleBtn.style.display = 'none'; });
}
}
let isFullscreen = false;
let savedState = null;
function saveCurrentState() {
if (isFullscreen) return;
const originalAspect = widgetContainer.getAttribute('data-original-aspect') || aspectPercent;
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,
},
viewer: {
borderRadius: viewerContainerElem.style.borderRadius,
border: viewerContainerElem.style.border,
}
};
}
function restoreOriginalStyles() {
if (!savedState) return;
const 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.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) {
viewerModule.app.resizeCanvas(
viewerContainerElem.clientWidth,
viewerContainerElem.clientHeight
);
}
if (fullscreenToggle) fullscreenToggle.textContent = '⇱';
savedState = null;
setMenuContentMaxSize();
}
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);
}
if (fullscreenToggle) fullscreenToggle.textContent = '⇲';
isFullscreen = true;
setMenuContentMaxSize();
}
function enterFullscreen() {
if (!savedState) saveCurrentState();
if (isIOS) {
applyFullscreenStyles();
widgetContainer.classList.add('fake-fullscreen');
} else if (widgetContainer.requestFullscreen) {
widgetContainer.requestFullscreen()
.then(applyFullscreenStyles)
.catch(() => {
applyFullscreenStyles();
widgetContainer.classList.add('fake-fullscreen');
});
} else {
applyFullscreenStyles();
widgetContainer.classList.add('fake-fullscreen');
}
}
function exitFullscreen() {
if (document.fullscreenElement === widgetContainer && document.exitFullscreen) {
document.exitFullscreen().catch(() => {});
}
widgetContainer.classList.remove('fake-fullscreen');
restoreOriginalStyles();
isFullscreen = false;
if (fullscreenToggle) fullscreenToggle.textContent = '⇱';
setMenuContentMaxSize();
}
fullscreenToggle.addEventListener('click', () => {
hideTooltipPanel();
isFullscreen ? exitFullscreen() : enterFullscreen();
});
document.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement && isFullscreen) {
isFullscreen = false;
restoreOriginalStyles();
if (fullscreenToggle) fullscreenToggle.textContent = '⇱';
} else if (document.fullscreenElement === widgetContainer) {
if (fullscreenToggle) fullscreenToggle.textContent = '⇲';
}
setMenuContentMaxSize();
});
helpToggle.addEventListener('click', (e) => {
hideTooltipPanel();
e.stopPropagation();
// Toggle menu panel
if (menuContent.style.display === 'block') {
menuContent.style.display = 'none';
} else {
menuContent.style.display = 'block';
setMenuContentMaxSize();
}
});
helpCloseBtn.addEventListener('click', hideHelpPanel);
resetCameraBtn.addEventListener('click', () => {
hideTooltipPanel();
if (viewerModule.resetViewerCamera) {
viewerModule.resetViewerCamera();
}
});
if (tooltipsToggleBtn) {
let tooltipsVisible = !!config.showTooltipsDefault;
tooltipsToggleBtn.style.opacity = tooltipsVisible ? '1' : '0.5';
tooltipsToggleBtn.addEventListener('click', () => {
hideTooltipPanel();
tooltipsVisible = !tooltipsVisible;
tooltipsToggleBtn.style.opacity = tooltipsVisible ? '1' : '0.5';
document.dispatchEvent(new CustomEvent('toggle-tooltips', { detail: { visible: tooltipsVisible } }));
});
}
tooltipCloseBtn.addEventListener('click', hideTooltipPanel);
// --------- COLOR BUTTON LOGIC ----------
let colorPanelOpen = false;
function openColorPanel() {
colorPanelElem.style.display = 'grid';
colorMenuBtn.classList.add('active');
colorPanelElem.focus();
colorPanelOpen = true;
}
function closeColorPanel() {
colorPanelElem.style.display = 'none';
colorMenuBtn.classList.remove('active');
colorPanelOpen = false;
}
function toggleColorPanel() {
if (colorPanelOpen) {
closeColorPanel();
} else {
openColorPanel();
}
}
// Toggle color panel on color button click
colorMenuBtn.addEventListener('click', (e) => {
e.stopPropagation();
toggleColorPanel();
});
// Change color on swatch click and close panel
colorSwatchBtns.forEach((btn) => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
if (viewerModule && viewerModule.changeColor) {
const dr = parseFloat(btn.getAttribute('data-dr'));
const dg = parseFloat(btn.getAttribute('data-dg'));
const db = parseFloat(btn.getAttribute('data-db'));
const er = parseFloat(btn.getAttribute('data-er'));
const eg = parseFloat(btn.getAttribute('data-eg'));
const eb = parseFloat(btn.getAttribute('data-eb'));
const ei = parseFloat(btn.getAttribute('data-ei'));
const op = parseFloat(btn.getAttribute('data-op'));
const bt = parseFloat(btn.getAttribute('data-boolTrans'));
viewerModule.changeColor(dr, dg, db, er, eg, eb, ei, op, bt);
}
closeColorPanel();
});
});
// Close color panel when clicking outside
document.addEventListener('mousedown', (e) => {
if (!colorPanelOpen) return;
if (!colorPanelElem.contains(e.target) && e.target !== colorMenuBtn) {
closeColorPanel();
}
});
// Accessibility: ESC closes panel
document.addEventListener('keydown', (e) => {
if (colorPanelOpen && (e.key === "Escape" || e.key === "Esc")) {
closeColorPanel();
colorMenuBtn.focus();
}
if ((e.key === 'Escape' || e.key === 'Esc') && isFullscreen) exitFullscreen();
});
// =======================================
document.addEventListener('tooltip-selected', (evt) => {
// Always show panel, cancel hide first
if (dragHide) {
viewerContainerElem.removeEventListener('pointermove', dragHide);
dragHide = null;
}
const { title, description, imgUrl } = evt.detail;
tooltipTextDiv.innerHTML = `<strong>${title}</strong><br>${description}`;
// Force a repaint: clear src before setting, for repeated images
tooltipImage.style.display = 'none';
tooltipImage.src = '';
if (imgUrl) {
tooltipImage.onload = () => {
tooltipImage.style.display = 'block';
};
tooltipImage.src = imgUrl;
} else {
tooltipImage.style.display = 'none';
}
tooltipPanel.style.display = 'flex';
// --- DELAYED pointermove handler ---
setTimeout(() => {
dragHide = (e) => {
if ((e.pointerType === 'mouse' && e.buttons !== 0) || e.pointerType === 'touch') {
hideTooltipPanel();
}
};
viewerContainerElem.addEventListener('pointermove', dragHide);
}, 100);
});
if (canvasEl) {
canvasEl.addEventListener('wheel', hideTooltipPanel, { passive: true });
}
window.addEventListener('resize', () => {
if (viewerModule.app) {
if (isFullscreen) {
viewerModule.app.resizeCanvas(window.innerWidth, window.innerHeight);
} else {
viewerModule.app.resizeCanvas(
viewerContainerElem.clientWidth,
viewerContainerElem.clientHeight
);
}
}
setMenuContentMaxSize();
});
setTimeout(() => {
saveCurrentState();
document.dispatchEvent(new CustomEvent('toggle-tooltips', { detail: { visible: !!config.showTooltipsDefault } }));
setMenuContentMaxSize();
}, 200);
})();