Spaces:
Running
Running
// 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); | |
})(); | |