1243 lines
60 KiB
JavaScript
1243 lines
60 KiB
JavaScript
import THREE from "./init.js";
|
|
|
|
import { core } from "./core.js";
|
|
import { t } from "./i18n-utils.js";
|
|
import { toastHelper } from './viewer-utils.js';
|
|
|
|
export function getEditorToolbarIcon(icon) {
|
|
const icons = {
|
|
moveToolbar: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v18M3 12h18M12 3l-2.5 2.5M12 3l2.5 2.5M12 21l-2.5-2.5M12 21l2.5-2.5M3 12l2.5-2.5M3 12l2.5 2.5M21 12l-2.5-2.5M21 12l-2.5 2.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
orbit: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3a9 9 0 1 0 9 9" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M16.5 2.75 21 3.5l-.75 4.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12" r="2.25" fill="currentColor"/></svg>',
|
|
move: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v18M3 12h18M12 3l-2.5 2.5M12 3l2.5 2.5M12 21l-2.5-2.5M12 21l2.5-2.5M3 12l2.5-2.5M3 12l2.5 2.5M21 12l-2.5-2.5M21 12l-2.5 2.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
rotate: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 6.5A7.5 7.5 0 1 1 5 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M8 3.5v3H5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
scale: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 8h8v8H8zM5 5h4M5 5v4M19 19h-4M19 19v-4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
lightMove: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3 5 13h5l-1 8 7-10h-5z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg>',
|
|
lightTarget: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="7" fill="none" stroke="currentColor" stroke-width="1.8"/><circle cx="12" cy="12" r="2.5" fill="currentColor"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>',
|
|
lights: '<svg viewBox="0 0 24 24" aria-hidden="true"> <circle cx="12" cy="12" r="3" fill="none" stroke="currentColor" stroke-width="1.8"/> <path d="M12 4V7M12 17v3M4 12h3M17 12h3" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/> <path d="M6.5 6.5l2 2M15.5 15.5l2 2M17.5 6.5l-2 2M8.5 15.5l-2 2" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> </svg>',
|
|
materials: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2l8 4v8l-8 4-8-4V6l8-4z" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M12 6l8 4M12 6v8M12 14l-8-4" fill="none" stroke="currentColor" stroke-width="1.8"/></svg>',
|
|
ambientLight: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="6" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M12 2v2M12 20v2M2 12h2M20 12h2" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>',
|
|
cameraLight: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h3l2-2h4l2 2h3v10H5z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><circle cx="12" cy="13" r="2.5" fill="none" stroke="currentColor" stroke-width="1.8"/></svg>',
|
|
environmentMap: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3 19 7v10l-7 4-7-4V7z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M12 3v18M5 7l7 4 7-4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><circle cx="18.25" cy="5.75" r="1.25" fill="currentColor"/></svg>',
|
|
color: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3a5 5 0 0 0-5 5c0 2.8 5 9 5 9s5-6.2 5-9a5 5 0 0 0-5-5Z" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M12 14.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3Z" fill="currentColor"/></svg>',
|
|
intensity: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M12 2v2M12 20v2M2 12h2M20 12h2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>',
|
|
picking: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 3 8 8-4 1 2 5-2.5 1-2-5-3 3Z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg>',
|
|
resetCamera: '<svg viewBox="0 0 24 24" aria-hidden="true"> <path d="M9 4H4v5M15 4h5v5M20 15v5h-5M4 15v5h5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/> <circle cx="12" cy="12" r="3" fill="none" stroke="currentColor" stroke-width="1.8"/> </svg>',
|
|
preview: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h16v12H4z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="m8 14 2.5-3 2.5 2 2-3 3 4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
save: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4h11l3 3v13H5z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M8 4v5h8M9 18h6" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>',
|
|
mainMenu: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m12 3 2 2.2 3-.2.8 2.9 2.6 1.4-1 2.8 1 2.8-2.6 1.4-.8 2.9-3-.2L12 21l-2-2.2-3 .2-.8-2.9-2.6-1.4 1-2.8-1-2.8 2.6-1.4.8-2.9 3 .2Z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/><circle cx="12" cy="12" r="2.5" fill="none" stroke="currentColor" stroke-width="1.8"/></svg>',
|
|
advancedEditor: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h10M4 17h16M14 7h6M4 12h6M12 12h8M8 5v4M16 10v4M10 15v4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
fullScreen: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 4h5M4 4v5M20 4h-5M20 4v5M4 20h5M4 20v-5M20 20h-5M20 20v-5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
displayHelperX: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 8l8 8M16 8 8 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>',
|
|
displayHelperY: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 7 12 13 17 7" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M12 13v4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>',
|
|
displayHelperZ: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 7h10M7 17h10M17 7 7 17" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>',
|
|
visible: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8S1 12 1 12Z" fill="none" stroke="currentColor" stroke-width="1.8"/><circle cx="12" cy="12" r="3" fill="none" stroke="currentColor" stroke-width="1.8"/></svg>',
|
|
clippingPlanes: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 6h10v12H7z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M12 5v14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/><path d="M7 6h5v12H7z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-dasharray="2.5 2.5" stroke-linejoin="round"/></svg>',
|
|
ruler: '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="9" width="16" height="6" rx="1.8" fill="none" stroke="currentColor" stroke-width="1.8"/> <path d="M7 9v2.5 M9.5 9v1.6 M12 9v2.5 M14.5 9v1.6 M17 9v2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>',
|
|
annotate: '<svg viewBox="0 0 24 24" aria-hidden="true"> <path d="M5 5h14v10H9l-4 4z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/> <path d="M9 9h6M9 12h4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/> </svg>',
|
|
annotateAdd: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4h14v11H9l-4 4z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M12 8v5M9.5 10.5h5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/></svg>',
|
|
annotateImport: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4h14v11H9l-4 4z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M12 6.8v7" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/><path d="M8.8 10.8 12 14l3.2-3.2" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
annotateExport: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4h14v11H9l-4 4z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M12 14.2V7.2" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/><path d="M8.8 10.2 12 7l3.2 3.2" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
IIIFexport: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 3h9l3 3v12H6z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M15 3v3h3" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M12 18V9" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/><path d="M8.8 12.2L12 9l3.2 3.2" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/><path d="M5 21h14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/></svg>', hierarchy: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 4h5v5H4zM15 4h5v5h-5zM4 15h5v5H4zM15 15h5v5h-5zM9 6h6M9 17h6" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
loadingLogs: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 12h18M3 6h12M3 18h12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>',
|
|
performance: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3a9 9 0 1 1-9 9" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M12 3a9 9 0 0 1 9 9" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M12 12 15 9" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><circle cx="12" cy="12" r="1.5" fill="currentColor"/></svg>',
|
|
statistics: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 18h16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M7 14v4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M12 10v8" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M17 6v12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>',
|
|
performanceDefault: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3a9 9 0 1 1-9 9" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M12 3a9 9 0 0 1 9 9" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M12 12 15 9" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><circle cx="12" cy="12" r="1.5" fill="currentColor"/></svg>',
|
|
performanceHigh: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3a9 9 0 1 1-9 9" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M12 3a9 9 0 0 1 9 9" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M12 12 15 9" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><circle cx="12" cy="12" r="1.5" fill="#FF4136"/></svg>',
|
|
performanceLow: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3a9 9 0 1 1-9 9" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M12 3a9 9 0 0 1 9 9" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M12 12 15 9" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><circle cx="12" cy="12" r="1.5" fill="#2ECC40"/></svg>',
|
|
expand: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 5l7 7-7 7" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
collapse: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 9l5 5 5-5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
projection: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 8l5-3h7v14h-7l-5-3z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M11 5v14" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M6 8v8" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>',
|
|
wireframe: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3 19 7v10l-7 4-7-4V7z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M12 3v18M5 7l7 4 7-4M5 17l7-4 7 4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>',
|
|
screenshot: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 5H5a2 2 0 0 0-2 2v2M17 5h2a2 2 0 0 1 2 2v2M17 19h2a2 2 0 0 0 2-2v-2M7 19H5a2 2 0 0 1-2-2v-2" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><circle cx="12" cy="12" r="3.2" fill="none" stroke="currentColor" stroke-width="1.8"/></svg>',
|
|
download: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v12M5 12l7 7 7-7M4 19h16a1 1 0 0 1 1 1v2H3v-2a1 1 0 0 1 1-1z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
};
|
|
|
|
return icons[icon] || icons.advancedEditor;
|
|
}
|
|
|
|
export function syncEditorToolbarSecondaryTrayWidth(viewer) {
|
|
if (!viewer.editorToolbarSecondaryTray) return;
|
|
|
|
const width =
|
|
Array.from(
|
|
viewer.editorToolbarSecondaryTray.children
|
|
).reduce(
|
|
(sum, el) => sum + el.getBoundingClientRect().width + 10,
|
|
0
|
|
);
|
|
|
|
viewer.editorToolbarSecondaryTray.style.setProperty(
|
|
"--viewer-toolbar-secondary-width",
|
|
`${Math.ceil(width)}px`
|
|
);
|
|
}
|
|
|
|
export function getEditorToolbarHost(viewer) {
|
|
return core.container || viewer.viewerWrapper || null;
|
|
}
|
|
|
|
function initializeEditorToolbarDrag(handle, viewer, toolbar, host) {
|
|
let dragState = null;
|
|
|
|
// persistent toolbar position
|
|
let currentX = 0;
|
|
let currentY = 0;
|
|
|
|
const getScale = () => {
|
|
const style = getComputedStyle(toolbar);
|
|
const scale = parseFloat(
|
|
style.getPropertyValue("--viewer-toolbar-scale")
|
|
);
|
|
|
|
return Number.isFinite(scale) ? scale : 1;
|
|
};
|
|
|
|
const clampPosition = (x, y) => {
|
|
const hostRect = host.getBoundingClientRect();
|
|
|
|
return {
|
|
x: Math.min(
|
|
Math.max(x, -hostRect.width),
|
|
hostRect.width
|
|
),
|
|
|
|
y: Math.min(
|
|
Math.max(y, -hostRect.height),
|
|
hostRect.height
|
|
),
|
|
};
|
|
};
|
|
|
|
const applyPosition = () => {
|
|
toolbar.style.setProperty("--drag-x", `${currentX}px`);
|
|
toolbar.style.setProperty("--drag-y", `${currentY}px`);
|
|
};
|
|
|
|
const updateToolbarPosition = (event) => {
|
|
if (!dragState) return;
|
|
|
|
const scale = getScale();
|
|
|
|
const dx = (event.clientX - dragState.startX) / scale;
|
|
const dy = (event.clientY - dragState.startY) / scale;
|
|
|
|
const pos = clampPosition(
|
|
dragState.originX + dx,
|
|
dragState.originY + dy
|
|
);
|
|
|
|
currentX = pos.x;
|
|
currentY = pos.y;
|
|
|
|
applyPosition();
|
|
};
|
|
|
|
const stopToolbarDrag = () => {
|
|
if (!dragState) return;
|
|
|
|
dragState = null;
|
|
|
|
toolbar.classList.remove("viewer-editor-toolbar_dragging");
|
|
|
|
document.removeEventListener(
|
|
"pointermove",
|
|
updateToolbarPosition
|
|
);
|
|
|
|
document.removeEventListener(
|
|
"pointerup",
|
|
stopToolbarDrag
|
|
);
|
|
|
|
document.removeEventListener(
|
|
"pointercancel",
|
|
stopToolbarDrag
|
|
);
|
|
|
|
requestAnimationFrame(() => {
|
|
toolbar.style.removeProperty("transition");
|
|
});
|
|
};
|
|
|
|
const startToolbarDrag = (event) => {
|
|
if (event.button !== 0) return;
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
dragState = {
|
|
startX: event.clientX,
|
|
startY: event.clientY,
|
|
originX: currentX,
|
|
originY: currentY,
|
|
};
|
|
|
|
toolbar.classList.add("viewer-editor-toolbar_dragging");
|
|
|
|
toolbar.style.transition = "none";
|
|
|
|
document.addEventListener(
|
|
"pointermove",
|
|
updateToolbarPosition,
|
|
{passive: true}
|
|
);
|
|
|
|
document.addEventListener(
|
|
"pointerup",
|
|
stopToolbarDrag
|
|
);
|
|
|
|
document.addEventListener(
|
|
"pointercancel",
|
|
stopToolbarDrag
|
|
);
|
|
};
|
|
|
|
viewer.bindEventListener(
|
|
handle,
|
|
"pointerdown",
|
|
startToolbarDrag
|
|
);
|
|
|
|
viewer.bindEventListener(handle, "click", (event) => {
|
|
event.stopPropagation();
|
|
});
|
|
|
|
// keep position valid after resize
|
|
const resizeObserver = new ResizeObserver(() => {
|
|
const pos = clampPosition(currentX, currentY);
|
|
|
|
currentX = pos.x;
|
|
currentY = pos.y;
|
|
|
|
applyPosition();
|
|
});
|
|
|
|
resizeObserver.observe(host);
|
|
|
|
applyPosition();
|
|
}
|
|
|
|
export function attachEditorToolbar(viewer) {
|
|
if (!core.editorToolbar || !core.container) return;
|
|
if (getComputedStyle(core.container).position === 'static') {
|
|
core.container.style.position = 'relative';
|
|
}
|
|
const host = getEditorToolbarHost(viewer);
|
|
if (!host || core.editorToolbar.parentElement === host) return;
|
|
host.appendChild(core.editorToolbar);
|
|
}
|
|
|
|
export function toggleToolbarExpanded(viewer) {
|
|
if (!core.editorToolbar) return;
|
|
|
|
syncEditorToolbarSecondaryTrayWidth(viewer);
|
|
viewer.isToolbarExpanded = !viewer.isToolbarExpanded;
|
|
core.editorToolbar.classList.toggle("expanded", viewer.isToolbarExpanded);
|
|
viewer.editorToolbarButtons.expand.classList.toggle("expanded-icon", viewer.isToolbarExpanded);
|
|
viewer.editorToolbarButtons.expand.setAttribute("aria-expanded", viewer.isToolbarExpanded ? "true" : "false");
|
|
const icon = viewer.editorToolbarButtons.expand.querySelector(".viewer-editor-tool_icon");
|
|
if (icon) {
|
|
icon.innerHTML = getEditorToolbarIcon(viewer.isToolbarExpanded ? "collapse" : "expand");
|
|
}
|
|
|
|
viewer.updateEditorToolbarLabels();
|
|
}
|
|
|
|
async function downloadFile(fileName = "model.glb") {
|
|
if (!core.downloadModel) return;
|
|
|
|
const handle = await window.showSaveFilePicker({
|
|
suggestedName: fileName,
|
|
});
|
|
|
|
const writable = await handle.createWritable();
|
|
|
|
await writable.write(core.downloadModel);
|
|
await writable.close();
|
|
|
|
toastHelper("download", "success");
|
|
}
|
|
|
|
export function createEditorToolbar(viewer) {
|
|
if (!core.EDITOR || viewer.urlOptions.hideUi || core.editorToolbar || !core.container) return;
|
|
|
|
const toolbar = document.createElement("div");
|
|
toolbar.id = "viewerEditorToolbar";
|
|
toolbar.setAttribute("role", "toolbar");
|
|
toolbar.setAttribute("aria-label", t("toolbar.editor", "Editor tools"));
|
|
|
|
const tools = [
|
|
{ key: "moveToolbar", icon: "moveToolbar", onClick: () => {}, pressed:true, primary: true },
|
|
{ key: "orbit", icon: "orbit", onClick: () => viewer.setObjectTransformMode(""), primary: true },
|
|
{ key: "move", icon: "move", onClick: () => viewer.toggleObjectTransformMode("translate"), pressed: true, primary: true },
|
|
{ key: "rotate", icon: "rotate", onClick: () => viewer.toggleObjectTransformMode("rotate"), pressed: true, primary: true },
|
|
{ key: "scale", icon: "scale", onClick: () => viewer.toggleObjectTransformMode("scale"), pressed: true, primary: true },
|
|
{ key: "lights", icon: "lights", onClick: () => {}, pressed: false, primary: false },
|
|
{ key: "materials", icon: "materials", onClick: () => viewer.openMaterialsFolder(), pressed: false, primary: false },
|
|
{ key: "picking", icon: "picking", onClick: () => viewer.togglePickingMode(), pressed: true, primary: false },
|
|
{ key: "annotate", icon: "annotate", onClick: () => viewer.openAnnotationDialogWithAutoPicking(), primary: false },
|
|
{ key: "ruler", icon: "ruler", onClick: () => viewer.toggleDistanceMeasurement(), pressed: true, primary: false },
|
|
{ key: "fullScreen", icon: "fullScreen", onClick: () => viewer.toggleFullscreen(), pressed: true, primary: true },
|
|
{ key: "clippingPlanes", icon: "clippingPlanes", onClick: () => viewer.toggleClippingPlanesPanel(), pressed: true, primary: false },
|
|
{ key: "resetCamera", icon: "resetCamera", onClick: () => viewer.resetCamera(), primary: false },
|
|
{ key: "hierarchy", icon: "hierarchy", onClick: () => {}, pressed: true, primary: false },
|
|
{ key: "projection", icon: "projection", onClick: () => viewer.toggleCameraProjection(), pressed: true, primary: false },
|
|
{ key: "wireframe", icon: "wireframe", onClick: () => viewer.toggleWireframeMode(), pressed: true, primary: false },
|
|
{ key: "statistics", icon: "statistics", onClick: () => {}, pressed: false, primary: false },
|
|
|
|
];
|
|
|
|
if (!core.isLightweight || core.isLocalPreview) {
|
|
tools.splice(tools.length - 1, 0,
|
|
{ key: "loadingLogs", icon: "loadingLogs", onClick: () => viewer.toggleLoadingLogs(), pressed: true, primary: false },
|
|
{ key: "download", icon: "download", onClick: () => downloadFile(core.fileObject.filename), pressed: true, primary: false },
|
|
{ key: "preview", icon: "preview", onClick: () => viewer.takeScreenshot(), primary: false },
|
|
{ key: "save", icon: "save", onClick: () => {}, primary: false }
|
|
);
|
|
}
|
|
|
|
viewer.editorToolbarButtons = {};
|
|
viewer.environmentMapPreset = viewer.environmentMapPreset || "neutral";
|
|
|
|
const secondaryTray = document.createElement("div");
|
|
secondaryTray.className = "viewer-editor-toolbar_secondary-tray";
|
|
viewer.editorToolbarSecondaryTray = secondaryTray;
|
|
|
|
tools.forEach((tool) => {
|
|
const button = document.createElement("button");
|
|
button.type = "button";
|
|
button.className = "viewer-editor-tool";
|
|
if (!tool.primary) {
|
|
button.classList.add("viewer-editor-tool-not-primary");
|
|
}
|
|
button.dataset.tool = tool.key;
|
|
button.dataset.pressed = tool.pressed ? "true" : "false";
|
|
button.dataset.primary = tool.primary ? "true" : "false";
|
|
if (tool.key === "materials") {
|
|
const label = t("gui.materials", "Materials");
|
|
button.setAttribute("title", label);
|
|
button.setAttribute("aria-label", label);
|
|
}
|
|
button.innerHTML = `
|
|
<span class="viewer-editor-tool_icon" aria-hidden="true">${getEditorToolbarIcon(tool.icon)}</span>
|
|
<span class="viewer-editor-tool_sr"></span>
|
|
`;
|
|
if (tool.key === "moveToolbar") {
|
|
initializeEditorToolbarDrag(button, viewer, toolbar, getEditorToolbarHost(viewer));
|
|
}
|
|
else if (tool.key === "clippingPlanes") {
|
|
button.classList.add("has-submenu");
|
|
const submenu = document.createElement("div");
|
|
submenu.className = "viewer-editor-tool_submenu";
|
|
const submenuItems = [
|
|
{ key: "displayHelperX", icon: "displayHelperX", label: t("gui.displayHelperX", "Show X helper"), onClick: () => viewer.toggleClippingPlaneHelper("x") },
|
|
{ key: "displayHelperY", icon: "displayHelperY", label: t("gui.displayHelperY", "Show Y helper"), onClick: () => viewer.toggleClippingPlaneHelper("y") },
|
|
{ key: "displayHelperZ", icon: "displayHelperZ", label: t("gui.displayHelperZ", "Show Z helper"), onClick: () => viewer.toggleClippingPlaneHelper("z") },
|
|
{ key: "visible", icon: "visible", label: t("gui.visible", "Visible"), onClick: () => viewer.toggleClippingPlaneVisible() },
|
|
];
|
|
viewer.clippingPlaneSubmenuButtons = {};
|
|
submenuItems.forEach((item) => {
|
|
const subButton = document.createElement("button");
|
|
subButton.type = "button";
|
|
subButton.className = "viewer-editor-tool viewer-editor-tool_submenu-button";
|
|
subButton.dataset.tool = item.key;
|
|
subButton.innerHTML = `
|
|
<span class="viewer-editor-tool_icon" aria-hidden="true">${getEditorToolbarIcon(item.icon)}</span>
|
|
`;
|
|
subButton.setAttribute("title", item.label);
|
|
subButton.setAttribute("aria-label", item.label);
|
|
viewer.bindEventListener(subButton, "click", (event) => {
|
|
event.stopPropagation();
|
|
item.onClick();
|
|
});
|
|
submenu.appendChild(subButton);
|
|
viewer.clippingPlaneSubmenuButtons[item.key] = subButton;
|
|
});
|
|
button.appendChild(submenu);
|
|
} else if (tool.key === "annotate") {
|
|
button.classList.add("has-submenu");
|
|
const submenu = document.createElement("div");
|
|
submenu.className = "viewer-editor-tool_submenu";
|
|
const submenuItems = [
|
|
{ key: "annotateAdd", icon: "annotateAdd", label: t("gui.annotateAdd", "Add Annotation"), onClick: () => viewer.openAnnotationDialogWithAutoPicking() },
|
|
{ key: "annotateImport", icon: "annotateImport", label: t("gui.annotateImport", "Import Annotations"), onClick: () => viewer.triggerAnnotationsXmlImport() },
|
|
{ key: "annotateExport", icon: "annotateExport", label: t("gui.annotateExport", "Export Annotations"), onClick: () => viewer.downloadAnnotationsXmlFile() },
|
|
{ key: "IIIFexport", icon: "IIIFexport", label: t("gui.IIIFexport", "Export to IIIF"), onClick: () => viewer.exportIIIFManifest() },
|
|
];
|
|
viewer.annotateSubmenuButtons = {};
|
|
submenuItems.forEach((item) => {
|
|
const subButton = document.createElement("button");
|
|
subButton.type = "button";
|
|
subButton.className = "viewer-editor-tool viewer-editor-tool_submenu-button";
|
|
subButton.dataset.tool = item.key;
|
|
subButton.innerHTML = `
|
|
<span class="viewer-editor-tool_icon" aria-hidden="true">${getEditorToolbarIcon(item.icon)}</span>
|
|
`;
|
|
subButton.setAttribute("title", item.label);
|
|
subButton.setAttribute("aria-label", item.label);
|
|
viewer.bindEventListener(subButton, "click", (event) => {
|
|
event.stopPropagation();
|
|
item.onClick();
|
|
});
|
|
submenu.appendChild(subButton);
|
|
viewer.annotateSubmenuButtons[item.key] = subButton;
|
|
});
|
|
button.appendChild(submenu);
|
|
} else if (tool.key === "materials") {
|
|
button.classList.add("has-submenu");
|
|
const submenu = document.createElement("div");
|
|
submenu.className = "viewer-editor-tool_submenu viewer-editor-tool_submenu-materials";
|
|
viewer.materialsSubmenu = submenu;
|
|
viewer.refreshMaterialsToolbarMenu();
|
|
button.appendChild(submenu);
|
|
} else if (tool.key === "hierarchy") {
|
|
button.classList.add("has-submenu");
|
|
const submenu = document.createElement("div");
|
|
submenu.className = "viewer-editor-tool_submenu viewer-editor-hierarchy-submenu";
|
|
viewer.hierarchySubmenu = submenu;
|
|
const hierarchyList = document.createElement("div");
|
|
hierarchyList.className = "viewer-editor-hierarchy-submenu-list";
|
|
viewer.hierarchySubmenuList = hierarchyList;
|
|
const clearButton = document.createElement("button");
|
|
clearButton.type = "button";
|
|
clearButton.className = "viewer-editor-tool viewer-editor-tool_submenu-button viewer-editor-hierarchy-clear";
|
|
viewer.bindEventListener(clearButton, "click", (event) => {
|
|
event.stopPropagation();
|
|
viewer.clearHierarchySelection();
|
|
});
|
|
viewer.hierarchyClearButton = clearButton;
|
|
viewer.hierarchySubmenuButtons = {};
|
|
submenu.appendChild(hierarchyList);
|
|
submenu.appendChild(clearButton);
|
|
button.appendChild(submenu);
|
|
} else if (tool.key === "save") {
|
|
button.classList.add("has-submenu");
|
|
const submenu = document.createElement("div");
|
|
submenu.className = "viewer-editor-tool_submenu viewer-editor-save-submenu";
|
|
viewer.bindEventListener(submenu, "click", (event) => {
|
|
event.stopPropagation();
|
|
});
|
|
const submenuItems = [
|
|
{ key: "Position", label: t("gui.position", "Position") },
|
|
{ key: "Rotation", label: t("gui.rotation", "Rotation") },
|
|
{ key: "Scale", label: t("gui.scale", "Scale") },
|
|
{ key: "Camera", label: t("gui.camera", "Camera") },
|
|
{ key: "DirectionalLight", label: t("gui.directionalLight", "Directional Light") },
|
|
{ key: "AmbientLight", label: t("gui.ambientLight", "Ambient Light") },
|
|
{ key: "CameraLight", label: t("gui.cameraLight", "Camera Light") },
|
|
{ key: "BackgroundColor", label: t("gui.backgroundColor", "Background Color") },
|
|
];
|
|
viewer.saveSubmenuCheckboxes = {};
|
|
submenuItems.forEach((item) => {
|
|
const row = document.createElement("label");
|
|
row.className = "viewer-editor-save-option";
|
|
row.setAttribute("title", item.label);
|
|
row.setAttribute("aria-label", item.label);
|
|
|
|
const checkbox = document.createElement("input");
|
|
checkbox.type = "checkbox";
|
|
checkbox.checked = Boolean(viewer.saveProperties[item.key]);
|
|
checkbox.dataset.property = item.key;
|
|
viewer.bindEventListener(checkbox, "click", (event) => {
|
|
event.stopPropagation();
|
|
});
|
|
viewer.bindEventListener(checkbox, "change", (event) => {
|
|
event.stopPropagation();
|
|
viewer.saveProperties[item.key] = event.target.checked;
|
|
});
|
|
|
|
const text = document.createElement("span");
|
|
text.className = "viewer-editor-save-option_label";
|
|
text.textContent = item.label;
|
|
|
|
row.appendChild(checkbox);
|
|
row.appendChild(text);
|
|
submenu.appendChild(row);
|
|
viewer.saveSubmenuCheckboxes[item.key] = { row, checkbox, text };
|
|
});
|
|
|
|
const actions = document.createElement("div");
|
|
actions.className = "viewer-editor-save-actions";
|
|
|
|
const saveButton = document.createElement("button");
|
|
saveButton.type = "button";
|
|
saveButton.className = "viewer-editor-save-apply";
|
|
saveButton.textContent = t("gui.saveSettings", "Save settings");
|
|
viewer.bindEventListener(saveButton, "click", (event) => {
|
|
event.stopPropagation();
|
|
viewer.saveEditorMetadata();
|
|
});
|
|
viewer.saveSubmenuActionButton = saveButton;
|
|
|
|
actions.appendChild(saveButton);
|
|
submenu.appendChild(actions);
|
|
button.appendChild(submenu);
|
|
} else if (tool.key === "statistics") {
|
|
button.classList.add("has-submenu");
|
|
const submenu = document.createElement("div");
|
|
submenu.className = "viewer-editor-tool_submenu";
|
|
viewer.statisticsSubmenuButtons = {};
|
|
|
|
const appendStatisticsSubmenuItems = (items, container) => {
|
|
items.forEach((item) => {
|
|
const subButton = document.createElement("button");
|
|
subButton.type = "button";
|
|
subButton.className = "viewer-editor-tool viewer-editor-tool_submenu-button";
|
|
subButton.dataset.tool = item.key;
|
|
subButton.setAttribute("title", item.label);
|
|
subButton.setAttribute("aria-label", item.label);
|
|
subButton.setAttribute("aria-pressed", item.pressed);
|
|
|
|
const iconSpan = document.createElement("span");
|
|
iconSpan.className = "viewer-editor-tool_icon";
|
|
iconSpan.setAttribute("aria-hidden", "true");
|
|
iconSpan.innerHTML = getEditorToolbarIcon(item.icon);
|
|
subButton.appendChild(iconSpan);
|
|
|
|
const srSpan = document.createElement("span");
|
|
srSpan.className = "viewer-editor-tool_sr";
|
|
srSpan.textContent = item.label;
|
|
subButton.appendChild(srSpan);
|
|
|
|
if (item.onClick) {
|
|
viewer.bindEventListener(subButton, "click", (event) => {
|
|
event.stopPropagation();
|
|
item.onClick();
|
|
});
|
|
}
|
|
|
|
if (item.children) {
|
|
subButton.classList.add("has-submenu");
|
|
const nested = document.createElement("div");
|
|
nested.className = "viewer-editor-tool_submenu";
|
|
appendStatisticsSubmenuItems(item.children, nested);
|
|
subButton.appendChild(nested);
|
|
}
|
|
|
|
viewer.statisticsSubmenuButtons[item.key] = subButton;
|
|
container.appendChild(subButton);
|
|
});
|
|
};
|
|
|
|
appendStatisticsSubmenuItems([
|
|
{
|
|
key: "toggleStats",
|
|
icon: "statistics",
|
|
label: t("gui.statistics", "Statistics"),
|
|
pressed: false,
|
|
onClick: () => viewer.toggleStatsVisibility(),
|
|
},
|
|
{
|
|
key: "performance",
|
|
icon: "performance",
|
|
label: t("gui.performance", "Performance"),
|
|
children: [
|
|
{
|
|
key: "performanceDefault",
|
|
icon: "statistics",
|
|
label: t("gui.default", "Default"),
|
|
onClick: () => viewer.setPerformanceMode("default"),
|
|
pressed: true,
|
|
},
|
|
{
|
|
key: "performanceHigh",
|
|
icon: "performanceHigh",
|
|
label: t("gui.highPerformance", "High-performance"),
|
|
onClick: () => viewer.setPerformanceMode("high-performance"),
|
|
pressed: true,
|
|
},
|
|
{
|
|
key: "performanceLow",
|
|
icon: "performanceLow",
|
|
label: t("gui.lowPower", "Low-power"),
|
|
onClick: () => viewer.setPerformanceMode("low-power"),
|
|
pressed: true,
|
|
},
|
|
],
|
|
},
|
|
], submenu);
|
|
|
|
button.appendChild(submenu);
|
|
} else if (tool.key === "lights") {
|
|
button.classList.add("has-submenu");
|
|
const submenu = document.createElement("div");
|
|
submenu.className = "viewer-editor-tool_submenu";
|
|
viewer.lightsSubmenuButtons = {};
|
|
|
|
const normalizeColorValue = (value) => {
|
|
if (typeof value !== "string") return "#ffffff";
|
|
if (value.startsWith("0x")) {
|
|
return `#${value.slice(2).padStart(6, "0")}`;
|
|
}
|
|
return value.startsWith("#") ? value : `#${value}`;
|
|
};
|
|
|
|
const appendSubmenuItems = (items, container) => {
|
|
items.forEach((item) => {
|
|
const subButton = document.createElement("button");
|
|
subButton.type = "button";
|
|
subButton.className = "viewer-editor-tool viewer-editor-tool_submenu-button ";
|
|
subButton.dataset.tool = item.key;
|
|
subButton.setAttribute("title", item.label);
|
|
subButton.setAttribute("aria-label", item.label);
|
|
|
|
const iconSpan = document.createElement("span");
|
|
iconSpan.className = "viewer-editor-tool_icon";
|
|
iconSpan.setAttribute("aria-hidden", "true");
|
|
iconSpan.innerHTML = item.iconHtml || getEditorToolbarIcon(item.icon);
|
|
subButton.appendChild(iconSpan);
|
|
|
|
if (item.type === "color") {
|
|
subButton.classList.add("viewer-editor-tool_submenu-control");
|
|
|
|
const colorInput = document.createElement("input");
|
|
colorInput.type = "color";
|
|
colorInput.value = normalizeColorValue(item.value());
|
|
colorInput.className = "viewer-editor-tool_submenu-input";
|
|
colorInput.addEventListener("click", (event) => event.stopPropagation());
|
|
colorInput.addEventListener("input", (event) => {
|
|
const value = event.target.value;
|
|
item.onChange(value);
|
|
colorInput.value = normalizeColorValue(value);
|
|
});
|
|
subButton.appendChild(colorInput);
|
|
} else if (item.type === "slider") {
|
|
subButton.classList.add("viewer-editor-tool_submenu-control");
|
|
|
|
const slider = document.createElement("input");
|
|
slider.type = "range";
|
|
slider.min = item.min ?? 0;
|
|
slider.max = item.max ?? 10;
|
|
slider.step = item.step ?? 0.01;
|
|
slider.value = String(item.value());
|
|
slider.className = "viewer-editor-tool_submenu-input";
|
|
slider.addEventListener("click", (event) => event.stopPropagation());
|
|
slider.addEventListener("input", (event) => {
|
|
const value = parseFloat(event.target.value);
|
|
item.onChange(value);
|
|
valueLabel.textContent = value.toFixed(2);
|
|
});
|
|
|
|
const valueLabel = document.createElement("span");
|
|
valueLabel.className = "viewer-editor-tool_submenu-value";
|
|
valueLabel.textContent = Number(item.value()).toFixed(2);
|
|
valueLabel.setAttribute("aria-hidden", "true");
|
|
|
|
subButton.appendChild(slider);
|
|
subButton.appendChild(valueLabel);
|
|
} else if (item.type === "toggle") {
|
|
subButton.classList.add("viewer-editor-tool_submenu-control", "viewer-editor-tool_submenu-toggle");
|
|
subButton.setAttribute("type", "button");
|
|
|
|
const toggleState = document.createElement("span");
|
|
toggleState.className = "viewer-editor-tool_submenu-toggle-state";
|
|
const setToggleState = () => {
|
|
const enabled = Boolean(item.value());
|
|
toggleState.textContent = enabled ? t("gui.on", "ON") : t("gui.off", "OFF");
|
|
subButton.setAttribute("aria-pressed", enabled ? "true" : "false");
|
|
subButton.classList.toggle("is-active", enabled);
|
|
};
|
|
setToggleState();
|
|
|
|
viewer.bindEventListener(subButton, "click", async (event) => {
|
|
event.stopPropagation();
|
|
const nextValue = !Boolean(item.value());
|
|
if (item.onChange) {
|
|
await item.onChange(nextValue);
|
|
}
|
|
setToggleState();
|
|
});
|
|
|
|
subButton.appendChild(toggleState);
|
|
} else if (item.onClick) {
|
|
viewer.bindEventListener(subButton, "click", (event) => {
|
|
event.stopPropagation();
|
|
item.onClick();
|
|
});
|
|
}
|
|
|
|
if (item.children) {
|
|
subButton.classList.add("has-submenu");
|
|
const nested = document.createElement("div");
|
|
nested.className = "viewer-editor-tool_submenu";
|
|
appendSubmenuItems(item.children, nested);
|
|
subButton.appendChild(nested);
|
|
}
|
|
|
|
if (
|
|
item.key === "lightTargetTransformMove" ||
|
|
item.key === "lightTargetTransformTarget" ||
|
|
item.key.startsWith("environmentMap")
|
|
) {
|
|
viewer.lightsSubmenuButtons[item.key] = subButton;
|
|
}
|
|
container.appendChild(subButton);
|
|
});
|
|
};
|
|
|
|
appendSubmenuItems([
|
|
{
|
|
key: "environmentMap",
|
|
icon: "environmentMap",
|
|
label: t("gui.environmentMap", "Environment map"),
|
|
children: [
|
|
{
|
|
key: "environmentMapToggle",
|
|
icon: "environmentMap",
|
|
label: t("gui.environmentMapToggle", "Environment map"),
|
|
type: "toggle",
|
|
value: () => (core.scene?.environmentIntensity ?? 0) > 0,
|
|
onChange: async (value) => {
|
|
if (!core.scene) return;
|
|
core.scene.traverse((child) => {
|
|
const materials = child?.material
|
|
? Array.isArray(child.material)
|
|
? child.material
|
|
: [child.material]
|
|
: [];
|
|
materials.forEach((material) => {
|
|
if (material?.isMeshStandardMaterial || material?.isMeshPhysicalMaterial) {
|
|
material.needsUpdate = true;
|
|
}
|
|
});
|
|
});
|
|
viewer.updateEditorToolbarState();
|
|
},
|
|
},
|
|
{
|
|
key: "environmentMapIntensity",
|
|
icon: "intensity",
|
|
label: t("gui.intensity", "Intensity"),
|
|
type: "slider",
|
|
min: 0,
|
|
max: 1,
|
|
step: 0.01,
|
|
value: () => core.environmentMapIntensity ?? 0.5,
|
|
onChange: (value) => {
|
|
if (!core.scene) return;
|
|
core.scene.environmentIntensity = value;
|
|
core.scene.traverse((child) => {
|
|
const materials = child?.material
|
|
? Array.isArray(child.material)
|
|
? child.material
|
|
: [child.material]
|
|
: [];
|
|
materials.forEach((material) => {
|
|
if (material?.isMeshStandardMaterial || material?.isMeshPhysicalMaterial) {
|
|
material.needsUpdate = true;
|
|
}
|
|
});
|
|
});
|
|
},
|
|
},
|
|
{
|
|
key: "environmentMapStyleNeutral",
|
|
iconHtml: "🌥",
|
|
label: t("gui.environmentMapNeutral", "Neutral"),
|
|
onClick: async () => {
|
|
await viewer.setEnvironmentMapPreset("neutral");
|
|
viewer.updateLightsSubmenuState();
|
|
},
|
|
},
|
|
{
|
|
key: "environmentMapStyleSunny",
|
|
iconHtml: "☀️",
|
|
label: t("gui.environmentMapSunny", "Sunny"),
|
|
onClick: async () => {
|
|
await viewer.setEnvironmentMapPreset("sunny");
|
|
viewer.updateLightsSubmenuState();
|
|
},
|
|
},
|
|
{
|
|
key: "environmentMapStyleStudio",
|
|
iconHtml: "📸",
|
|
label: t("gui.environmentMapStudio", "Studio"),
|
|
onClick: async () => {
|
|
await viewer.setEnvironmentMapPreset("studio");
|
|
viewer.updateLightsSubmenuState();
|
|
},
|
|
},
|
|
{
|
|
key: "environmentMapStyleGoldenHour",
|
|
iconHtml: "🌅",
|
|
label: t("gui.environmentMapGoldenHour", "Golden Hour"),
|
|
onClick: async () => {
|
|
await viewer.setEnvironmentMapPreset("goldenHour");
|
|
viewer.updateLightsSubmenuState();
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
key: "lightTarget",
|
|
icon: "lightTarget",
|
|
label: t("gui.target", "Target"),
|
|
children: [
|
|
{
|
|
key: "lightTargetColor",
|
|
icon: "color",
|
|
label: t("gui.color", "Color"),
|
|
type: "color",
|
|
value: () => viewer.colors.DirectionalLight,
|
|
onChange: (value) => {
|
|
viewer.colors.DirectionalLight = value;
|
|
core.lightObjects[0].color = new THREE.Color(value);
|
|
},
|
|
},
|
|
{
|
|
key: "lightTargetIntensity",
|
|
icon: "intensity",
|
|
label: t("gui.intensity", "Intensity"),
|
|
type: "slider",
|
|
min: 0,
|
|
max: 10,
|
|
step: 0.01,
|
|
value: () => viewer.intensity.startIntensityDir,
|
|
onChange: (value) => {
|
|
viewer.intensity.startIntensityDir = value;
|
|
core.lightObjects[0].intensity = value;
|
|
},
|
|
},
|
|
{
|
|
key: "lightTargetTransform",
|
|
icon: "move",
|
|
label: t("gui.transform", "Transform"),
|
|
children: [
|
|
{ key: "lightTargetTransformMove", icon: "move", label: t("gui.move", "Move"), onClick: () => viewer.toggleLightTransformMode("translate") },
|
|
{ key: "lightTargetTransformTarget", icon: "lightTarget", label: t("gui.target", "Target"), onClick: () => viewer.toggleLightTransformMode("rotate") },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
key: "lightAmbient",
|
|
icon: "ambientLight",
|
|
label: t("gui.ambient", "Ambient"),
|
|
children: [
|
|
{
|
|
key: "lightAmbientColor",
|
|
icon: "color",
|
|
label: t("gui.color", "Color"),
|
|
type: "color",
|
|
value: () => viewer.colors.AmbientLight,
|
|
onChange: (value) => {
|
|
viewer.colors.AmbientLight = value;
|
|
viewer.ambientLight.color = new THREE.Color(value);
|
|
},
|
|
},
|
|
{
|
|
key: "lightAmbientIntensity",
|
|
icon: "intensity",
|
|
label: t("gui.intensity", "Intensity"),
|
|
type: "slider",
|
|
min: 0,
|
|
max: 10,
|
|
step: 0.01,
|
|
value: () => viewer.intensity.startIntensityAmbient,
|
|
onChange: (value) => {
|
|
viewer.intensity.startIntensityAmbient = value;
|
|
viewer.ambientLight.intensity = value;
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
key: "lightCamera",
|
|
icon: "cameraLight",
|
|
label: t("gui.camera", "Camera"),
|
|
children: [
|
|
{
|
|
key: "lightCameraColor",
|
|
icon: "color",
|
|
label: t("gui.color", "Color"),
|
|
type: "color",
|
|
value: () => viewer.colors.CameraLight,
|
|
onChange: (value) => {
|
|
viewer.colors.CameraLight = value;
|
|
viewer.cameraLight.color = new THREE.Color(value);
|
|
},
|
|
},
|
|
{
|
|
key: "lightCameraIntensity",
|
|
icon: "intensity",
|
|
label: t("gui.intensity", "Intensity"),
|
|
type: "slider",
|
|
min: 0,
|
|
max: 10,
|
|
step: 0.01,
|
|
value: () => viewer.intensity.startIntensityCamera,
|
|
onChange: (value) => {
|
|
viewer.intensity.startIntensityCamera = value;
|
|
viewer.cameraLight.intensity = value;
|
|
},
|
|
},
|
|
],
|
|
},
|
|
], submenu);
|
|
button.appendChild(submenu);
|
|
} else if (tool.key === "materials") {
|
|
button.classList.add("has-submenu");
|
|
const submenu = document.createElement("div");
|
|
submenu.className = "viewer-editor-tool_submenu viewer-editor-tool_submenu-materials";
|
|
const submenuItems = [
|
|
{ key: "materialColor", icon: "color", label: t("gui.color", "Color"), onClick: () => viewer.openMaterialsFolder() },
|
|
{ key: "materialIntensity", icon: "intensity", label: t("gui.intensity", "Intensity"), onClick: () => viewer.openMaterialsFolder() },
|
|
];
|
|
submenuItems.forEach((item) => {
|
|
const subButton = document.createElement("button");
|
|
subButton.type = "button";
|
|
subButton.className = "viewer-editor-tool viewer-editor-tool_submenu-button";
|
|
subButton.dataset.tool = item.key;
|
|
subButton.innerHTML = `
|
|
<span class="viewer-editor-tool_icon" aria-hidden="true">${getEditorToolbarIcon(item.icon)}</span>
|
|
`;
|
|
subButton.setAttribute("title", item.label);
|
|
subButton.setAttribute("aria-label", item.label);
|
|
viewer.bindEventListener(subButton, "click", (event) => {
|
|
event.stopPropagation();
|
|
item.onClick();
|
|
});
|
|
submenu.appendChild(subButton);
|
|
});
|
|
button.appendChild(submenu);
|
|
} else if (tool.key === "download") {
|
|
if (!core.isLightweight || core.isLocalPreview) {
|
|
button.href = core.downloadModel;
|
|
button.target = "_blank";
|
|
button.rel = "noopener noreferrer";
|
|
button.download = core.fileObject.filename;
|
|
}
|
|
}
|
|
viewer.bindEventListener(button, "click", () => {
|
|
viewer.stopHandMode();
|
|
if (tool.onClick) {
|
|
tool.onClick();
|
|
}
|
|
});
|
|
if (tool.primary) toolbar.appendChild(button);
|
|
else secondaryTray.appendChild(button);
|
|
viewer.editorToolbarButtons[tool.key] = button;
|
|
if (!tool.primary) viewer.editorSecondaryKeys.push(button);
|
|
});
|
|
|
|
toolbar.appendChild(secondaryTray);
|
|
|
|
const expandButton = document.createElement("button");
|
|
expandButton.type = "button";
|
|
expandButton.className = "viewer-editor-tool viewer-editor-expand";
|
|
expandButton.innerHTML = `<span class="viewer-editor-tool_icon" aria-hidden="true">${getEditorToolbarIcon("expand")}</span>`;
|
|
expandButton.dataset.primary = "true";
|
|
expandButton.setAttribute("aria-expanded", "false");
|
|
expandButton.setAttribute("title", t("gui.expand", "Expand toolbar"));
|
|
expandButton.setAttribute("aria-label", t("gui.expand", "Expand toolbar"));
|
|
viewer.bindEventListener(expandButton, "click", () => toggleToolbarExpanded(viewer));
|
|
toolbar.appendChild(expandButton);
|
|
viewer.editorToolbarButtons.expand = expandButton;
|
|
|
|
if (viewer.actionMenu) {
|
|
viewer.actionMenu.classList.add("viewer-action-menu_in-toolbar");
|
|
toolbar.appendChild(viewer.actionMenu);
|
|
}
|
|
|
|
getEditorToolbarHost(viewer)?.appendChild(toolbar);
|
|
core.editorToolbar = toolbar;
|
|
core.editorToolbar.classList.add("editorToolbar-hidden");
|
|
core.editorToolbar.classList.add("collapsed");
|
|
viewer.updateFullscreenButtonIcon();
|
|
viewer.updateEditorToolbarLabels();
|
|
viewer.updateEditorToolbarState();
|
|
syncEditorToolbarSecondaryTrayWidth(viewer);
|
|
viewer.bindEventListener(window, "resize", () => syncEditorToolbarSecondaryTrayWidth(viewer));
|
|
}
|
|
|
|
export function updateHierarchySubmenuState(viewer) {
|
|
if (!viewer.hierarchySubmenuButtons) return;
|
|
|
|
const selectedIds = new Set(
|
|
(core.selectedObjects || [])
|
|
.filter((item) => item?.selected === true)
|
|
.map((item) => String(item.id))
|
|
);
|
|
|
|
Object.entries(viewer.hierarchySubmenuButtons).forEach(([key, button]) => {
|
|
const isActive = selectedIds.has(String(key));
|
|
button.classList.toggle("is-active", isActive);
|
|
button.setAttribute("aria-pressed", isActive ? "true" : "false");
|
|
});
|
|
|
|
viewer.hierarchyClearButton?.toggleAttribute("disabled", selectedIds.size === 0);
|
|
}
|
|
|
|
export function updateStatisticsSubmenuState(viewer) {
|
|
if (!viewer.statisticsSubmenuButtons) return;
|
|
const isVisible = typeof core.stats !== "undefined" && core.stats?.dom?.style?.visibility !== "hidden";
|
|
viewer.statisticsSubmenuButtons.toggleStats?.classList.toggle("is-active", isVisible);
|
|
viewer.statisticsSubmenuButtons.toggleStats?.setAttribute("aria-pressed", isVisible ? "true" : "false");
|
|
|
|
const currentMode = core.renderer?.powerPreference || core.CONFIG.viewer?.performanceMode || "default";
|
|
const performanceMap = {
|
|
performanceHigh: "high-performance",
|
|
performanceLow: "low-power",
|
|
performanceDefault: "default",
|
|
};
|
|
Object.entries(performanceMap).forEach(([key, value]) => {
|
|
const isActive = currentMode === value;
|
|
viewer.statisticsSubmenuButtons[key]?.classList.toggle("is-active", isActive);
|
|
viewer.statisticsSubmenuButtons[key]?.setAttribute("aria-pressed", isActive ? "true" : "false");
|
|
});
|
|
}
|
|
|
|
export function updateClippingPlanesSubmenuState(viewer) {
|
|
if (!viewer.clippingPlaneSubmenuButtons) return;
|
|
const clippingMode = core.planeParams?.clippingMode || {};
|
|
|
|
viewer.clippingPlaneSubmenuButtons.displayHelperX?.classList.toggle(
|
|
"is-active",
|
|
Boolean(clippingMode.x)
|
|
);
|
|
viewer.clippingPlaneSubmenuButtons.displayHelperY?.classList.toggle(
|
|
"is-active",
|
|
Boolean(clippingMode.y)
|
|
);
|
|
viewer.clippingPlaneSubmenuButtons.displayHelperZ?.classList.toggle(
|
|
"is-active",
|
|
Boolean(clippingMode.z)
|
|
);
|
|
viewer.clippingPlaneSubmenuButtons.visible?.classList.toggle(
|
|
"is-active",
|
|
Boolean(core.planeParams?.outline?.visible)
|
|
);
|
|
}
|
|
|
|
export function updateLightsSubmenuState(viewer) {
|
|
if (!viewer.lightsSubmenuButtons) return;
|
|
const activeMode = viewer.transformText["Transform Light"];
|
|
viewer.lightsSubmenuButtons.environmentMap?.classList.toggle(
|
|
"is-active",
|
|
viewer.environmentMapEnabled !== false
|
|
);
|
|
viewer.lightsSubmenuButtons.environmentMap?.setAttribute(
|
|
"aria-pressed",
|
|
viewer.environmentMapEnabled !== false ? "true" : "false"
|
|
);
|
|
|
|
const environmentMapToggle = viewer.lightsSubmenuButtons.environmentMapToggle;
|
|
if (environmentMapToggle) {
|
|
const toggleLabel = environmentMapToggle.querySelector('.viewer-editor-tool_submenu-toggle-state');
|
|
const isEnabled = (core.scene?.environmentIntensity ?? 0) > 0;
|
|
if (toggleLabel) toggleLabel.textContent = isEnabled ? t("gui.on", "ON") : t("gui.off", "OFF");
|
|
environmentMapToggle.setAttribute("aria-pressed", isEnabled ? "true" : "false");
|
|
environmentMapToggle.classList.toggle("is-active", isEnabled);
|
|
}
|
|
|
|
viewer.lightsSubmenuButtons.lightTargetTransformMove?.classList.toggle(
|
|
"is-active",
|
|
activeMode === "translate"
|
|
);
|
|
viewer.lightsSubmenuButtons.lightTargetTransformTarget?.classList.toggle(
|
|
"is-active",
|
|
activeMode === "rotate"
|
|
);
|
|
|
|
const environmentMapPreset = viewer.environmentMapPreset || "neutral";
|
|
const environmentMapPresetStates = {
|
|
environmentMapStyleNeutral: "neutral",
|
|
environmentMapStyleSunny: "sunny",
|
|
environmentMapStyleStudio: "studio",
|
|
environmentMapStyleGoldenHour: "goldenHour",
|
|
};
|
|
|
|
Object.entries(environmentMapPresetStates).forEach(([key, value]) => {
|
|
const isActive = environmentMapPreset === value;
|
|
viewer.lightsSubmenuButtons[key]?.classList.toggle("is-active", isActive);
|
|
viewer.lightsSubmenuButtons[key]?.setAttribute("aria-pressed", isActive ? "true" : "false");
|
|
});
|
|
}
|
|
|
|
export function updateEditorToolbarLabels(viewer) {
|
|
if (!viewer.editorToolbarButtons) return;
|
|
|
|
const labels = {
|
|
moveToolbar: t("gui.moveToolbar", "Move toolbar"),
|
|
orbit: t("gui.orbit", "Navigation mode"),
|
|
move: t("gui.move", "Move"),
|
|
rotate: t("gui.rotate", "Rotate"),
|
|
scale: t("gui.scale", "Scale"),
|
|
lights: t("gui.lights", "Lights"),
|
|
picking: viewer.pickingMode
|
|
? t("controls.disablePickingMode", "Disable picking mode")
|
|
: t("controls.enablePickingMode", "Enable picking mode"),
|
|
annotate: t("gui.addAnnotations", "Add annotations"),
|
|
ruler: viewer.RULER_MODE
|
|
? t("controls.disableDistanceMeasurement", "Disable distance measurement")
|
|
: t("controls.enableDistanceMeasurement", "Enable distance measurement"),
|
|
resetCamera: t("gui.resetCameraPosition", "Reset camera position"),
|
|
preview: t("gui.renderPreview", "Render preview"),
|
|
save: t("gui.saveSettings", "Save settings"),
|
|
advancedEditor: viewer.isEditorAdvancedPanelVisible()
|
|
? t("gui.hideAdvancedEditor", "Hide advanced editor")
|
|
: t("gui.showAdvancedEditor", "Show advanced editor"),
|
|
fullScreen: viewer.FULLSCREEN
|
|
? t("fullscreen.exit", "Exit fullscreen")
|
|
: t("fullscreen.enter", "Enter fullscreen"),
|
|
clippingPlanes: viewer.clippingMode
|
|
? t("gui.disableClippingPlanesMode", "Disable clipping planes mode")
|
|
: t("gui.enableClippingPlanesMode", "Enable clipping planes mode"),
|
|
projection: core.camera && core.camera.isPerspectiveCamera
|
|
? t("gui.orthographicProjection", "Switch to orthographic projection")
|
|
: t("gui.perspectiveProjection", "Switch to perspective projection"),
|
|
wireframe: viewer.wireframeMode
|
|
? t("gui.disableWireframeMode", "Disable wireframe mode")
|
|
: t("gui.enableWireframeMode", "Enable wireframe mode"),
|
|
loadingLogs: viewer.showLoadingLogs
|
|
? t("gui.hideLoadingLogs", "Hide loading logs")
|
|
: t("gui.showLoadingLogs", "Show loading logs"),
|
|
hierarchy: t("gui.hierarchy", "Hierarchy"),
|
|
materials: t("gui.materials", "Materials"),
|
|
statistics: t("gui.statistics", "Statistics"),
|
|
expand: viewer.isToolbarExpanded
|
|
? t("gui.collapse", "Collapse toolbar")
|
|
: t("gui.expand", "Expand toolbar"),
|
|
download: t("gui.download", "Download model"),
|
|
};
|
|
|
|
Object.entries(viewer.editorToolbarButtons).forEach(([key, button]) => {
|
|
const label = labels[key] || key;
|
|
button.setAttribute("title", label);
|
|
button.setAttribute("aria-label", label);
|
|
const sr = button.querySelector(".viewer-editor-tool_sr");
|
|
if (sr) sr.textContent = label;
|
|
});
|
|
|
|
if (viewer.clippingPlaneSubmenuButtons) {
|
|
const clippingPlaneSubmenuLabels = {
|
|
displayHelperX: t("gui.displayHelperX", "Show X helper"),
|
|
displayHelperY: t("gui.displayHelperY", "Show Y helper"),
|
|
displayHelperZ: t("gui.displayHelperZ", "Show Z helper"),
|
|
visible: t("gui.visible", "Visible"),
|
|
};
|
|
Object.entries(viewer.clippingPlaneSubmenuButtons).forEach(([key, button]) => {
|
|
const label = clippingPlaneSubmenuLabels[key] || key;
|
|
button.setAttribute("title", label);
|
|
button.setAttribute("aria-label", label);
|
|
});
|
|
}
|
|
|
|
if (viewer.annotateSubmenuButtons) {
|
|
const annotateSubmenuLabels = {
|
|
annotateAdd: t("gui.addAnnotations", "Add Annotation"),
|
|
annotateImport: t("gui.importAnnotationsXml", "Import Annotations"),
|
|
annotateExport: t("gui.exportAnnotationsXml", "Export Annotations"),
|
|
IIIFexport: t("gui.IIIFexport", "Export to IIIF"),
|
|
};
|
|
Object.entries(viewer.annotateSubmenuButtons).forEach(([key, button]) => {
|
|
const label = annotateSubmenuLabels[key] || key;
|
|
button.setAttribute("title", label);
|
|
button.setAttribute("aria-label", label);
|
|
});
|
|
}
|
|
|
|
if (viewer.statisticsSubmenuButtons) {
|
|
const statisticsSubmenuLabels = {
|
|
toggleStats: t("gui.statistics", "Statistics"),
|
|
performance: t("gui.performance", "Performance"),
|
|
performanceDefault: t("gui.default", "Default"),
|
|
performanceHigh: t("gui.highPerformance", "High-performance"),
|
|
performanceLow: t("gui.lowPower", "Low-power"),
|
|
};
|
|
Object.entries(viewer.statisticsSubmenuButtons).forEach(([key, button]) => {
|
|
const label = statisticsSubmenuLabels[key] || key;
|
|
button.setAttribute("title", label);
|
|
button.setAttribute("aria-label", label);
|
|
});
|
|
}
|
|
|
|
if (viewer.lightsSubmenuButtons) {
|
|
const lightsSubmenuLabels = {
|
|
environmentMap: t("gui.environmentMap", "Environment map"),
|
|
lightTargetTransformMove: t("gui.move", "Move"),
|
|
lightTargetTransformTarget: t("gui.target", "Target"),
|
|
environmentMapToggle: t("gui.environmentMapToggle", "Environment map"),
|
|
environmentMapIntensity: t("gui.intensity", "Intensity"),
|
|
environmentMapStyleNeutral: t("gui.environmentMapNeutral", "Neutral"),
|
|
environmentMapStyleSunny: t("gui.environmentMapSunny", "Sunny"),
|
|
environmentMapStyleStudio: t("gui.environmentMapStudio", "Studio"),
|
|
environmentMapStyleGoldenHour: t("gui.environmentMapGoldenHour", "Golden Hour"),
|
|
};
|
|
Object.entries(viewer.lightsSubmenuButtons).forEach(([key, button]) => {
|
|
const label = lightsSubmenuLabels[key] || key;
|
|
button.setAttribute("title", label);
|
|
button.setAttribute("aria-label", label);
|
|
});
|
|
}
|
|
|
|
if (viewer.hierarchyClearButton) {
|
|
const label = t("gui.clearSelectedHierarchy", "Clear selected objects");
|
|
viewer.hierarchyClearButton.setAttribute("title", label);
|
|
viewer.hierarchyClearButton.setAttribute("aria-label", label);
|
|
viewer.hierarchyClearButton.textContent = label;
|
|
}
|
|
|
|
if (viewer.saveSubmenuCheckboxes) {
|
|
const saveSubmenuLabels = {
|
|
Position: t("gui.position", "Position"),
|
|
Rotation: t("gui.rotation", "Rotation"),
|
|
Scale: t("gui.scale", "Scale"),
|
|
Camera: t("gui.camera", "Camera"),
|
|
DirectionalLight: t("gui.directionalLight", "Directional Light"),
|
|
AmbientLight: t("gui.ambientLight", "Ambient Light"),
|
|
CameraLight: t("gui.cameraLight", "Camera Light"),
|
|
BackgroundColor: t("gui.backgroundColor", "Background Color"),
|
|
};
|
|
Object.entries(viewer.saveSubmenuCheckboxes).forEach(([key, elements]) => {
|
|
const label = saveSubmenuLabels[key] || key;
|
|
elements.row.setAttribute("title", label);
|
|
elements.row.setAttribute("aria-label", label);
|
|
elements.text.textContent = label;
|
|
elements.checkbox.checked = Boolean(viewer.saveProperties[key]);
|
|
});
|
|
}
|
|
|
|
if (viewer.saveSubmenuActionButton) {
|
|
viewer.saveSubmenuActionButton.textContent = t("gui.saveSettings", "Save settings");
|
|
}
|
|
|
|
core.editorToolbar?.setAttribute("aria-label", t("toolbar.editor", "Editor tools"));
|
|
viewer.editorToolbarButtons.expand?.setAttribute("aria-expanded", viewer.isToolbarExpanded ? "true" : "false");
|
|
}
|
|
|
|
export function updateEditorToolbarState(viewer) {
|
|
if (!viewer.editorToolbarButtons) return;
|
|
|
|
const activeMap = {
|
|
moveToolbar: viewer.transformText["Transform 3D Object"] === "translate" || viewer.transformText["Transform 3D Object"] === "rotate" || viewer.transformText["Transform 3D Object"] === "scale",
|
|
orbit: viewer.transformText["Transform 3D Object"] === "",
|
|
move: viewer.transformText["Transform 3D Object"] === "translate",
|
|
rotate: viewer.transformText["Transform 3D Object"] === "rotate",
|
|
scale: viewer.transformText["Transform 3D Object"] === "scale",
|
|
picking: viewer.pickingMode === true,
|
|
ruler: viewer.RULER_MODE === true,
|
|
clippingPlanes: viewer.clippingMode === true,
|
|
advancedEditor: viewer.isEditorAdvancedPanelVisible(),
|
|
fullScreen: viewer.FULLSCREEN === true,
|
|
loadingLogs: viewer.showLoadingLogs === true,
|
|
wireframe: viewer.wireframeMode === true,
|
|
download: false,
|
|
};
|
|
|
|
Object.entries(viewer.editorToolbarButtons).forEach(([key, button]) => {
|
|
const isActive = activeMap[key] === true;
|
|
button.classList.toggle("is-active", isActive);
|
|
if (button.dataset.pressed === "true") {
|
|
button.setAttribute("aria-pressed", isActive ? "true" : "false");
|
|
} else {
|
|
button.removeAttribute("aria-pressed");
|
|
}
|
|
});
|
|
|
|
updateHierarchySubmenuState(viewer);
|
|
updateClippingPlanesSubmenuState(viewer);
|
|
updateLightsSubmenuState(viewer);
|
|
updateStatisticsSubmenuState(viewer);
|
|
}
|