Added SVG export
+Added basic SVG export option
This commit is contained in:
parent
e80a28e9c0
commit
4c849d96bb
@ -85,6 +85,7 @@ function StatelessSceneryLayer({ name, Renderer, objects, type, types, trackSour
|
||||
ref={layerRef}
|
||||
className={`scenery-layer-${name}`}
|
||||
pointerEvents={pointerEvents ? "all" : "none"}
|
||||
export-force-visible="true"
|
||||
>
|
||||
{objects.map((obj) => {
|
||||
if (type !== undefined && type !== obj.type) return null;
|
||||
|
||||
@ -56,9 +56,9 @@ function StatelessTrackRenderer(props) {
|
||||
|
||||
if(details) {
|
||||
unscaledPathRef.current.style.display = 'none';
|
||||
scaledPathRef.current.style.display = 'block';
|
||||
scaledPathRef.current.style.display = '';
|
||||
} else {
|
||||
unscaledPathRef.current.style.display = 'block';
|
||||
unscaledPathRef.current.style.display = '';
|
||||
scaledPathRef.current.style.display = 'none';
|
||||
}
|
||||
}, []);
|
||||
@ -87,6 +87,7 @@ function StatelessTrackRenderer(props) {
|
||||
d={path}
|
||||
stroke={color}
|
||||
className="track-unscaled"
|
||||
no-export="true"
|
||||
/>
|
||||
<path
|
||||
ref={scaledPathRef}
|
||||
@ -95,7 +96,9 @@ function StatelessTrackRenderer(props) {
|
||||
className="track"
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onClick={onClick}/>
|
||||
onClick={onClick}
|
||||
export-force-visible="true"
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
23
src/components/sidemenu/ExportViewButton.js
Normal file
23
src/components/sidemenu/ExportViewButton.js
Normal file
@ -0,0 +1,23 @@
|
||||
import { useContext } from "react";
|
||||
import SceneryContext from "../../contexts/SceneryContext";
|
||||
import ExportHelper from "../../helpers/exportHelper";
|
||||
|
||||
export default function ExportViewButton() {
|
||||
const { scenery } = useContext(SceneryContext);
|
||||
|
||||
const onButtonClick = () => {
|
||||
const filename = (scenery.getName() || 'export') + '.svg';
|
||||
ExportHelper.exportSvg(filename);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="export-view-button">
|
||||
<button
|
||||
onClick={onButtonClick}
|
||||
disabled={!scenery}
|
||||
>
|
||||
Export current view
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -66,7 +66,8 @@
|
||||
}
|
||||
|
||||
.distance-meter-button button,
|
||||
.scenery-info-button button {
|
||||
.scenery-info-button button,
|
||||
.export-view-button button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import DistanceMeterButton from './DistanceMeterButton';
|
||||
import InfoFooter from './InfoFooter';
|
||||
import LayerOptionsMenu from './LayerOptionsMenu';
|
||||
import SideMenuContext from '../../contexts/SideMenuContext';
|
||||
import ExportViewButton from './ExportViewButton';
|
||||
|
||||
export default function SideMenu() {
|
||||
const { sideMenuOpen } = useContext(SideMenuContext);
|
||||
@ -17,6 +18,7 @@ export default function SideMenu() {
|
||||
<div className={sideMenuClass}>
|
||||
<SceneryInfoButton />
|
||||
<DistanceMeterButton />
|
||||
<ExportViewButton />
|
||||
<LayersMenu />
|
||||
<LayerOptionsMenu />
|
||||
<InfoFooter />
|
||||
|
||||
95
src/helpers/exportHelper.js
Normal file
95
src/helpers/exportHelper.js
Normal file
@ -0,0 +1,95 @@
|
||||
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 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 svg = svgOrig.cloneNode(true);
|
||||
mapElement.appendChild(svg);
|
||||
|
||||
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
||||
svg.setAttribute('version', '1.1');
|
||||
svg.classList.add('light-mode');
|
||||
processExportNode(svg);
|
||||
|
||||
serializeAndDownload(svg, filename);
|
||||
|
||||
mapElement.removeChild(svg);
|
||||
return true;
|
||||
}
|
||||
|
||||
const ExportHelper = {
|
||||
exportSvg
|
||||
};
|
||||
|
||||
export default ExportHelper;
|
||||
@ -9,6 +9,10 @@ export default class Scenery
|
||||
trackElevationBounds = { minY: Infinity, maxY: -Infinity };
|
||||
electrificationResolved = ElectrificationResolutionStatus.NOT_RESOLVED;
|
||||
|
||||
getName() {
|
||||
return this.objects.special?.SceneryInfo?.name || null;
|
||||
}
|
||||
|
||||
getBounds () {
|
||||
return { ...this.bounds };
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user