Skip to content

Commit

Permalink
Add svg exports for cfgs (#4424)
Browse files Browse the repository at this point in the history
* Added svg exports for cfgs

* Quick refactor

* Fix background colorfor chrome exports
  • Loading branch information
jeremy-rifkin authored and mattgodbolt committed Jan 24, 2023
1 parent f00511f commit cfaafa7
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 10 deletions.
25 changes: 18 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -21,9 +21,10 @@
},
"dependencies": {
"@flatten-js/interval-tree": "^1.0.18",
"@fortawesome/fontawesome-free": "^5.15.4",
"@fortawesome/fontawesome-free": "^6.2.1",
"@sentry/browser": "^6.16.1",
"@sentry/node": "^6.16.1",
"@types/file-saver": "^2.0.5",
"aws-sdk": "^2.1048.0",
"big-integer": "^1.6.51",
"body-parser": "^1.19.1",
Expand Down
96 changes: 95 additions & 1 deletion static/panes/cfg-view.ts
Expand Up @@ -26,6 +26,7 @@ import {Pane} from './pane';
import * as monaco from 'monaco-editor';
import $ from 'jquery';
import _ from 'underscore';
import * as fileSaver from 'file-saver';

import {CfgState} from './cfg-view.interfaces';
import {Hub} from '../hub';
Expand Down Expand Up @@ -59,13 +60,30 @@ type Coordinate = {
const DZOOM = 0.1;
const MINZOOM = 0.1;

// https://stackoverflow.com/questions/6234773/can-i-escape-html-special-chars-in-javascript
function escapeSVG(text: string) {
return text
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}

function attrs(attributes: Record<string, string | number | null>) {
return Object.entries(attributes)
.map(([k, v]) => `${k}="${v}"`)
.join(' ');
}

export class Cfg extends Pane<CfgState> {
graphDiv: HTMLElement;
svg: SVGElement;
blockContainer: HTMLElement;
graphContainer: HTMLElement;
graphElement: HTMLElement;
infoElement: HTMLElement;
exportButton: JQuery;
currentPosition: Coordinate = {x: 0, y: 0};
dragging = false;
dragStart: Coordinate = {x: 0, y: 0};
Expand Down Expand Up @@ -138,6 +156,7 @@ export class Cfg extends Pane<CfgState> {
this.graphContainer = this.domRoot.find('.graph-container')[0];
this.graphElement = this.domRoot.find('.graph')[0];
this.infoElement = this.domRoot.find('.cfg-info')[0];
this.exportButton = this.domRoot.find('.export').first();
}

override registerCallbacks() {
Expand Down Expand Up @@ -169,7 +188,7 @@ export class Cfg extends Pane<CfgState> {
const prevZoom = this.state.zoom;
this.state.zoom += delta;
if (this.state.zoom >= MINZOOM) {
this.graphElement.style.transform = `scale(${this.state.zoom})${this.extraTransforms}`;
this.zoom(this.state.zoom);
const mouseX = e.clientX - this.graphElement.getBoundingClientRect().x;
const mouseY = e.clientY - this.graphElement.getBoundingClientRect().y;
// Amount that the zoom will offset is mouseX / width before zoom * delta * unzoomed width
Expand All @@ -182,6 +201,13 @@ export class Cfg extends Pane<CfgState> {
this.state.zoom = MINZOOM;
}
});
this.exportButton.on('click', () => {
this.exportSVG();
});
}

exportSVG() {
fileSaver.saveAs(new Blob([this.createSVG()], {type: 'text/plain;charset=utf-8'}), 'cfg.svg');
}

override onCompiler(compilerId: number, compiler: any, options: unknown, editorId: number, treeId: number): void {
Expand Down Expand Up @@ -342,6 +368,74 @@ export class Cfg extends Pane<CfgState> {
}`;
}

zoom(zoom: number) {
this.graphElement.style.transform = `scale(${zoom})${this.extraTransforms}`;
}

createSVG() {
this.zoom(1);
let doc = '';
doc += '<?xml version="1.0"?>';
doc += '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">';
doc += `<svg ${attrs({
xmlns: 'http://www.w3.org/2000/svg',
version: '1.1',
width: this.svg.style.width,
height: this.svg.style.height,
viewBox: this.svg.getAttribute('viewBox'),
})}>`;
doc += '<style>.code{font: 16px Consolas;}</style>';
// insert the background
const pane = this.graphContainer.parentElement;
if (!pane || !pane.classList.contains('lm_content')) {
throw Error('unknown parent');
}
const pane_style = window.getComputedStyle(pane);
doc += `<rect ${attrs({
x: '0',
y: '0',
width: this.svg.style.width,
height: this.svg.style.height,
fill: pane_style.backgroundColor,
})} />`;
// just grab the edges/arrows directly
doc += this.svg.innerHTML;
// the blocks we'll have to copy over
for (const block of this.layout.blocks) {
const block_elem = this.bbMap[block.data.id];
const block_style = window.getComputedStyle(block_elem);
const block_bounding_box = block_elem.getBoundingClientRect();
doc += `<rect ${attrs({
x: block.coordinates.x,
y: block.coordinates.y,
width: block.data.width,
height: block.data.height,
fill: block_style.background,
stroke: block_style.borderColor,
'stroke-width': block_style.borderWidth,
})} />`;
for (const [_, span] of block_elem.querySelectorAll('span[class]').entries()) {
const text = new DOMParser().parseFromString(span.innerHTML, 'text/html').documentElement.textContent;
if (!text || text.trim() === '') {
continue;
}
const span_style = window.getComputedStyle(span);
const span_box = span.getBoundingClientRect();
const top = span_box.top - block_bounding_box.top;
const left = span_box.left - block_bounding_box.left;
doc += `<text ${attrs({
x: block.coordinates.x + left,
y: block.coordinates.y + top + span_box.height / 2 + parseInt(block_style.paddingTop),
class: 'code',
fill: span_style.color,
})}>${escapeSVG(text)}</text>`;
}
}
doc += '</svg>';
this.zoom(this.state.zoom);
return doc;
}

override resize() {
_.defer(() => {
const topBarHeight = utils.updateAndCalcTopBarHeight(this.domRoot, this.topBar, this.hideable);
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.frontend.json
Expand Up @@ -7,7 +7,7 @@
/* Code generation */
"inlineSources": true,
"strictPropertyInitialization": false,
"lib": ["dom", "es5"]
"lib": ["dom", "es5", "dom.iterable"]
},
"exclude": ["out", "test", "etc", "examples", "app.js", "lib", "examples"]
}
4 changes: 4 additions & 0 deletions views/templates/panes/cfg.pug
Expand Up @@ -2,6 +2,10 @@
.top-bar.btn-toolbar.bg-light.cfg-toolbar(role="toolbar")
.btn-group.btn-group-sm(role="group")
select.function-selector
.btn-group.btn-group-sm(role="group" aria-label="Cfg actions")
button.btn.btn-sm.btn-light.export(title="Export graph" aria-label="Export graph as an image")
span.fas.fa-file-arrow-down
span.dp-text.hideable Export
.graph-container
span.cfg-info
.graph
Expand Down

0 comments on commit cfaafa7

Please sign in to comment.