aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src/graph/postProcessSVG.ts
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/frontend/src/graph/postProcessSVG.ts')
-rw-r--r--subprojects/frontend/src/graph/postProcessSVG.ts133
1 files changed, 119 insertions, 14 deletions
diff --git a/subprojects/frontend/src/graph/postProcessSVG.ts b/subprojects/frontend/src/graph/postProcessSVG.ts
index 59cc15b9..13e4eb29 100644
--- a/subprojects/frontend/src/graph/postProcessSVG.ts
+++ b/subprojects/frontend/src/graph/postProcessSVG.ts
@@ -7,19 +7,48 @@
7import { type BBox, parsePolygonBBox, parsePathBBox } from './parseBBox'; 7import { type BBox, parsePolygonBBox, parsePathBBox } from './parseBBox';
8 8
9const SVG_NS = 'http://www.w3.org/2000/svg'; 9const SVG_NS = 'http://www.w3.org/2000/svg';
10const XLINK_NS = 'http://www.w3.org/1999/xlink';
11
12function modifyAttribute(element: Element, attribute: string, change: number) {
13 const valueString = element.getAttribute(attribute);
14 if (valueString === null) {
15 return;
16 }
17 const value = parseInt(valueString, 10);
18 element.setAttribute(attribute, String(value + change));
19}
20
21function addShadow(
22 node: SVGGElement,
23 container: SVGRectElement,
24 offset: number,
25): void {
26 const shadow = container.cloneNode() as SVGRectElement;
27 // Leave space for 1pt stroke around the original container.
28 const offsetWithStroke = offset - 0.5;
29 modifyAttribute(shadow, 'x', offsetWithStroke);
30 modifyAttribute(shadow, 'y', offsetWithStroke);
31 modifyAttribute(shadow, 'width', 1);
32 modifyAttribute(shadow, 'height', 1);
33 modifyAttribute(shadow, 'rx', 0.5);
34 modifyAttribute(shadow, 'ry', 0.5);
35 shadow.setAttribute('class', 'node-shadow');
36 shadow.id = `${node.id},shadow`;
37 node.insertBefore(shadow, node.firstChild);
38}
10 39
11function clipCompartmentBackground(node: SVGGElement) { 40function clipCompartmentBackground(node: SVGGElement) {
12 // Background rectangle of the node created by the `<table bgcolor="green">` 41 // Background rectangle of the node created by the `<table bgcolor="white">`
13 // HTML element in dot. It was transformed into a rounded rect by `fixNodeBackground`. 42 // HTML element in dot. It was transformed into a rounded rect by `fixNodeBackground`.
14 const container = node.querySelector<SVGRectElement>('rect[fill="green"]'); 43 const container = node.querySelector<SVGRectElement>('rect[fill="white"]');
15 // Background rectangle of the lower compartment created by the `<td bgcolor="white">` 44 // Background rectangle of the lower compartment created by the `<td bgcolor="green">`
16 // HTML element in dot. It was transformed into a rounded rect by `fixNodeBackground`. 45 // HTML element in dot. It was transformed into a rounded rect by `fixNodeBackground`.
17 // Since dot doesn't round the coners of `<td>` background, 46 // Since dot doesn't round the coners of `<td>` background,
18 // we have to clip it ourselves. 47 // we have to clip it ourselves.
19 const compartment = node.querySelector<SVGPolygonElement>( 48 const compartment = node.querySelector<SVGRectElement>('rect[fill="green"]');
20 'polygon[fill="white"]', 49 // Make sure we provide traceability with IDs also for the border.
21 ); 50 const border = node.querySelector<SVGRectElement>('rect[stroke="black"]');
22 if (container === null || compartment === null) { 51 if (container === null || compartment === null || border === null) {
23 return; 52 return;
24 } 53 }
25 const copyOfContainer = container.cloneNode() as SVGRectElement; 54 const copyOfContainer = container.cloneNode() as SVGRectElement;
@@ -29,6 +58,17 @@ function clipCompartmentBackground(node: SVGGElement) {
29 clipPath.appendChild(copyOfContainer); 58 clipPath.appendChild(copyOfContainer);
30 node.appendChild(clipPath); 59 node.appendChild(clipPath);
31 compartment.setAttribute('clip-path', `url(#${clipId})`); 60 compartment.setAttribute('clip-path', `url(#${clipId})`);
61 // Enlarge the compartment to completely cover the background.
62 modifyAttribute(compartment, 'y', -5);
63 modifyAttribute(compartment, 'x', -5);
64 modifyAttribute(compartment, 'width', 10);
65 modifyAttribute(compartment, 'height', 5);
66 if (node.classList.contains('node-equalsSelf-UNKNOWN')) {
67 addShadow(node, container, 6);
68 }
69 container.id = `${node.id},container`;
70 compartment.id = `${node.id},compartment`;
71 border.id = `${node.id},border`;
32} 72}
33 73
34function createRect( 74function createRect(
@@ -51,7 +91,7 @@ function optimizeNodeShapes(node: SVGGElement) {
51 const rect = createRect(bbox, path); 91 const rect = createRect(bbox, path);
52 rect.setAttribute('rx', '12'); 92 rect.setAttribute('rx', '12');
53 rect.setAttribute('ry', '12'); 93 rect.setAttribute('ry', '12');
54 node.replaceChild(rect, path); 94 path.parentNode?.replaceChild(rect, path);
55 }); 95 });
56 node.querySelectorAll('polygon').forEach((polygon) => { 96 node.querySelectorAll('polygon').forEach((polygon) => {
57 const bbox = parsePolygonBBox(polygon); 97 const bbox = parsePolygonBBox(polygon);
@@ -62,18 +102,83 @@ function optimizeNodeShapes(node: SVGGElement) {
62 'points', 102 'points',
63 `${bbox.x},${bbox.y} ${bbox.x + bbox.width},${bbox.y}`, 103 `${bbox.x},${bbox.y} ${bbox.x + bbox.width},${bbox.y}`,
64 ); 104 );
65 node.replaceChild(polyline, polygon); 105 polygon.parentNode?.replaceChild(polyline, polygon);
66 } else { 106 } else {
67 const rect = createRect(bbox, polygon); 107 const rect = createRect(bbox, polygon);
68 node.replaceChild(rect, polygon); 108 polygon.parentNode?.replaceChild(rect, polygon);
69 } 109 }
70 }); 110 });
71 clipCompartmentBackground(node); 111 clipCompartmentBackground(node);
72} 112}
73 113
114function hrefToClass(node: SVGGElement) {
115 node.querySelectorAll<SVGAElement>('a').forEach((a) => {
116 if (a.parentNode === null) {
117 return;
118 }
119 const href = a.getAttribute('href') ?? a.getAttributeNS(XLINK_NS, 'href');
120 if (href === 'undefined' || !href?.startsWith('#')) {
121 return;
122 }
123 while (a.lastChild !== null) {
124 const child = a.lastChild;
125 a.removeChild(child);
126 if (child.nodeType === Node.ELEMENT_NODE) {
127 const element = child as Element;
128 element.classList.add('label', `label-${href.replace('#', '')}`);
129 a.after(child);
130 }
131 }
132 a.parentNode.removeChild(a);
133 });
134}
135
136function replaceImages(node: SVGGElement) {
137 node.querySelectorAll<SVGImageElement>('image').forEach((image) => {
138 const href =
139 image.getAttribute('href') ?? image.getAttributeNS(XLINK_NS, 'href');
140 if (href === 'undefined' || !href?.startsWith('#')) {
141 return;
142 }
143 const width = image.getAttribute('width')?.replace('px', '') ?? '';
144 const height = image.getAttribute('height')?.replace('px', '') ?? '';
145 const foreign = document.createElementNS(SVG_NS, 'foreignObject');
146 foreign.setAttribute('x', image.getAttribute('x') ?? '');
147 foreign.setAttribute('y', image.getAttribute('y') ?? '');
148 foreign.setAttribute('width', width);
149 foreign.setAttribute('height', height);
150 const div = document.createElement('div');
151 div.classList.add('icon', `icon-${href.replace('#', '')}`);
152 foreign.appendChild(div);
153 const sibling = image.nextElementSibling;
154 // Since dot doesn't respect the `id` attribute on table cells with a single image,
155 // compute the ID based on the ID of the next element (the label).
156 if (
157 sibling !== null &&
158 sibling.tagName.toLowerCase() === 'g' &&
159 sibling.id !== ''
160 ) {
161 foreign.id = `${sibling.id},icon`;
162 }
163 image.parentNode?.replaceChild(foreign, image);
164 });
165}
166
74export default function postProcessSvg(svg: SVGSVGElement) { 167export default function postProcessSvg(svg: SVGSVGElement) {
75 svg 168 // svg
76 .querySelectorAll<SVGTitleElement>('title') 169 // .querySelectorAll<SVGTitleElement>('title')
77 .forEach((title) => title.parentNode?.removeChild(title)); 170 // .forEach((title) => title.parentElement?.removeChild(title));
78 svg.querySelectorAll<SVGGElement>('g.node').forEach(optimizeNodeShapes); 171 svg.querySelectorAll<SVGGElement>('g.node').forEach((node) => {
172 optimizeNodeShapes(node);
173 hrefToClass(node);
174 replaceImages(node);
175 });
176 // Increase padding to fit box shadows for multi-objects.
177 const viewBox = [
178 svg.viewBox.baseVal.x - 6,
179 svg.viewBox.baseVal.y - 6,
180 svg.viewBox.baseVal.width + 12,
181 svg.viewBox.baseVal.height + 12,
182 ];
183 svg.setAttribute('viewBox', viewBox.join(' '));
79} 184}