td2-visualizer/src/helpers/exportHelper.js
izawartka 6a04b5feec Added export trimming
*Objects in the exported SVG are now removed if they lay outside of the view
2025-08-17 17:49:13 +02:00

138 lines
4.2 KiB
JavaScript

const applyStyles = ['fill', 'stroke', 'stroke-width', 'font-size', 'font-family', 'text-anchor', 'dominant-baseline'];
function processExportNode(svgNode, currentGroupStyles = {}) {
if (!svgNode) return;
// apply export-force-visible attribute
if(svgNode.hasAttribute('export-force-visible')) {
svgNode.removeAttribute('export-force-visible');
if (svgNode.style.display === 'none') {
svgNode.style.display = '';
}
}
// remove empty style attribute
if (svgNode.hasAttribute('style')) {
const style = svgNode.getAttribute('style');
if (!style || style.trim() === '') {
svgNode.removeAttribute('style');
}
}
// apply styles
const computedStyles = getComputedStyle(svgNode);
const newGroupStyles = { ...currentGroupStyles };
for (const style of applyStyles) {
if (computedStyles[style] && currentGroupStyles[style] !== computedStyles[style]) {
svgNode.setAttribute(style, computedStyles[style]);
newGroupStyles[style] = computedStyles[style];
}
}
svgNode.removeAttribute('pointer-events');
if (svgNode.nodeType !== Node.ELEMENT_NODE) return;
if (svgNode.children && svgNode.children.length > 0) {
const nodesToRemove = [];
// process children recursively
for (const child of svgNode.children) {
if (child.hasAttribute('no-export')) {
nodesToRemove.push(child);
continue;
}
processExportNode(child, newGroupStyles);
}
// remove nodes with no-export attribute
for (const node of nodesToRemove) {
svgNode.removeChild(node);
}
}
}
function checkNodeVisible(svgNode, trimClientRect) {
if(!svgNode) return false;
if (svgNode.children?.length > 0) {
const visibleNodes = Array.from(svgNode.children).filter(child => {
return checkNodeVisible(child, trimClientRect);
});
return visibleNodes.length > 0;
}
const clientRect = svgNode.getBoundingClientRect();
if (clientRect.x + clientRect.width < trimClientRect.x) return false;
if (clientRect.x > trimClientRect.x + trimClientRect.width) return false;
if (clientRect.y + clientRect.height < trimClientRect.y) return false;
if (clientRect.y > trimClientRect.y + trimClientRect.height) return false;
if (clientRect.width === 0 || clientRect.height === 0) return false;
return true;
}
function removeNonVisibleObjects(svg) {
const sceneryRoot = svg.querySelector('g.scenery-root');
if (!sceneryRoot) return;
const trimClientRect = svg.getBoundingClientRect();
for (const layer of sceneryRoot.children) {
const objectsToDelete = Array.from(layer.children).filter(child => {
return !checkNodeVisible(child, trimClientRect);
});
for (const node of objectsToDelete) {
layer.removeChild(node);
}
}
}
function serializeAndDownload(svgNode, filename) {
const serializer = new XMLSerializer();
const svgString = serializer.serializeToString(svgNode);
const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
function exportSvg(filename) {
const mapElement = document.getElementsByClassName('map')[0];
if (!mapElement) return false;
const svgOrig = mapElement.querySelector('svg');
if (!svgOrig) return false;
const exportContainer = document.createElement('div');
exportContainer.classList.add('export-container', 'light-mode');
const svg = svgOrig.cloneNode(true);
mapElement.appendChild(exportContainer);
exportContainer.appendChild(svg);
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
svg.setAttribute('version', '1.1');
processExportNode(svg);
removeNonVisibleObjects(svg);
serializeAndDownload(svg, filename);
mapElement.removeChild(exportContainer);
return true;
}
const ExportHelper = {
exportSvg
};
export default ExportHelper;