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}
|
ref={layerRef}
|
||||||
className={`scenery-layer-${name}`}
|
className={`scenery-layer-${name}`}
|
||||||
pointerEvents={pointerEvents ? "all" : "none"}
|
pointerEvents={pointerEvents ? "all" : "none"}
|
||||||
|
export-force-visible="true"
|
||||||
>
|
>
|
||||||
{objects.map((obj) => {
|
{objects.map((obj) => {
|
||||||
if (type !== undefined && type !== obj.type) return null;
|
if (type !== undefined && type !== obj.type) return null;
|
||||||
|
|||||||
@ -56,9 +56,9 @@ function StatelessTrackRenderer(props) {
|
|||||||
|
|
||||||
if(details) {
|
if(details) {
|
||||||
unscaledPathRef.current.style.display = 'none';
|
unscaledPathRef.current.style.display = 'none';
|
||||||
scaledPathRef.current.style.display = 'block';
|
scaledPathRef.current.style.display = '';
|
||||||
} else {
|
} else {
|
||||||
unscaledPathRef.current.style.display = 'block';
|
unscaledPathRef.current.style.display = '';
|
||||||
scaledPathRef.current.style.display = 'none';
|
scaledPathRef.current.style.display = 'none';
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
@ -87,6 +87,7 @@ function StatelessTrackRenderer(props) {
|
|||||||
d={path}
|
d={path}
|
||||||
stroke={color}
|
stroke={color}
|
||||||
className="track-unscaled"
|
className="track-unscaled"
|
||||||
|
no-export="true"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
ref={scaledPathRef}
|
ref={scaledPathRef}
|
||||||
@ -95,7 +96,9 @@ function StatelessTrackRenderer(props) {
|
|||||||
className="track"
|
className="track"
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
onClick={onClick}/>
|
onClick={onClick}
|
||||||
|
export-force-visible="true"
|
||||||
|
/>
|
||||||
</g>
|
</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,
|
.distance-meter-button button,
|
||||||
.scenery-info-button button {
|
.scenery-info-button button,
|
||||||
|
.export-view-button button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import DistanceMeterButton from './DistanceMeterButton';
|
|||||||
import InfoFooter from './InfoFooter';
|
import InfoFooter from './InfoFooter';
|
||||||
import LayerOptionsMenu from './LayerOptionsMenu';
|
import LayerOptionsMenu from './LayerOptionsMenu';
|
||||||
import SideMenuContext from '../../contexts/SideMenuContext';
|
import SideMenuContext from '../../contexts/SideMenuContext';
|
||||||
|
import ExportViewButton from './ExportViewButton';
|
||||||
|
|
||||||
export default function SideMenu() {
|
export default function SideMenu() {
|
||||||
const { sideMenuOpen } = useContext(SideMenuContext);
|
const { sideMenuOpen } = useContext(SideMenuContext);
|
||||||
@ -17,6 +18,7 @@ export default function SideMenu() {
|
|||||||
<div className={sideMenuClass}>
|
<div className={sideMenuClass}>
|
||||||
<SceneryInfoButton />
|
<SceneryInfoButton />
|
||||||
<DistanceMeterButton />
|
<DistanceMeterButton />
|
||||||
|
<ExportViewButton />
|
||||||
<LayersMenu />
|
<LayersMenu />
|
||||||
<LayerOptionsMenu />
|
<LayerOptionsMenu />
|
||||||
<InfoFooter />
|
<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 };
|
trackElevationBounds = { minY: Infinity, maxY: -Infinity };
|
||||||
electrificationResolved = ElectrificationResolutionStatus.NOT_RESOLVED;
|
electrificationResolved = ElectrificationResolutionStatus.NOT_RESOLVED;
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return this.objects.special?.SceneryInfo?.name || null;
|
||||||
|
}
|
||||||
|
|
||||||
getBounds () {
|
getBounds () {
|
||||||
return { ...this.bounds };
|
return { ...this.bounds };
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user