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.ts186
1 files changed, 186 insertions, 0 deletions
diff --git a/subprojects/frontend/src/graph/postProcessSVG.ts b/subprojects/frontend/src/graph/postProcessSVG.ts
new file mode 100644
index 00000000..a580f5c6
--- /dev/null
+++ b/subprojects/frontend/src/graph/postProcessSVG.ts
@@ -0,0 +1,186 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import { type BBox, parsePolygonBBox, parsePathBBox } from './parseBBox';
8
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}
39
40function clipCompartmentBackground(node: SVGGElement) {
41 // Background rectangle of the node created by the `<table bgcolor="white">`
42 // HTML element in dot. It was transformed into a rounded rect by `fixNodeBackground`.
43 const container = node.querySelector<SVGRectElement>('rect[fill="white"]');
44 // Background rectangle of the lower compartment created by the `<td bgcolor="green">`
45 // HTML element in dot. It was transformed into a rounded rect by `fixNodeBackground`.
46 // Since dot doesn't round the coners of `<td>` background,
47 // we have to clip it ourselves.
48 const compartment = node.querySelector<SVGRectElement>('rect[fill="green"]');
49 // Make sure we provide traceability with IDs also for the border.
50 const border = node.querySelector<SVGRectElement>('rect[stroke="black"]');
51 if (container === null || compartment === null || border === null) {
52 return;
53 }
54 const copyOfContainer = container.cloneNode() as SVGRectElement;
55 const clipPath = document.createElementNS(SVG_NS, 'clipPath');
56 const clipId = `${node.id},,clip`;
57 clipPath.setAttribute('id', clipId);
58 clipPath.appendChild(copyOfContainer);
59 node.appendChild(clipPath);
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 const isEmpty = node.classList.contains('node-empty');
66 // Make sure that empty nodes are fully filled.
67 modifyAttribute(compartment, 'height', isEmpty ? 10 : 5);
68 if (node.classList.contains('node-equalsSelf-UNKNOWN')) {
69 addShadow(node, container, 6);
70 }
71 container.id = `${node.id},container`;
72 compartment.id = `${node.id},compartment`;
73 border.id = `${node.id},border`;
74}
75
76function createRect(
77 { x, y, width, height }: BBox,
78 original: SVGElement,
79): SVGRectElement {
80 const rect = document.createElementNS(SVG_NS, 'rect');
81 rect.setAttribute('fill', original.getAttribute('fill') ?? '');
82 rect.setAttribute('stroke', original.getAttribute('stroke') ?? '');
83 rect.setAttribute('x', String(x));
84 rect.setAttribute('y', String(y));
85 rect.setAttribute('width', String(width));
86 rect.setAttribute('height', String(height));
87 return rect;
88}
89
90function optimizeNodeShapes(node: SVGGElement) {
91 node.querySelectorAll('path').forEach((path) => {
92 const bbox = parsePathBBox(path);
93 const rect = createRect(bbox, path);
94 rect.setAttribute('rx', '12');
95 rect.setAttribute('ry', '12');
96 path.parentNode?.replaceChild(rect, path);
97 });
98 node.querySelectorAll('polygon').forEach((polygon) => {
99 const bbox = parsePolygonBBox(polygon);
100 if (bbox.height === 0) {
101 const polyline = document.createElementNS(SVG_NS, 'polyline');
102 polyline.setAttribute('stroke', polygon.getAttribute('stroke') ?? '');
103 polyline.setAttribute(
104 'points',
105 `${bbox.x},${bbox.y} ${bbox.x + bbox.width},${bbox.y}`,
106 );
107 polygon.parentNode?.replaceChild(polyline, polygon);
108 } else {
109 const rect = createRect(bbox, polygon);
110 polygon.parentNode?.replaceChild(rect, polygon);
111 }
112 });
113 clipCompartmentBackground(node);
114}
115
116function hrefToClass(node: SVGGElement) {
117 node.querySelectorAll<SVGAElement>('a').forEach((a) => {
118 if (a.parentNode === null) {
119 return;
120 }
121 const href = a.getAttribute('href') ?? a.getAttributeNS(XLINK_NS, 'href');
122 if (href === 'undefined' || !href?.startsWith('#')) {
123 return;
124 }
125 while (a.lastChild !== null) {
126 const child = a.lastChild;
127 a.removeChild(child);
128 if (child.nodeType === Node.ELEMENT_NODE) {
129 const element = child as Element;
130 element.classList.add('label', `label-${href.replace('#', '')}`);
131 a.after(child);
132 }
133 }
134 a.parentNode.removeChild(a);
135 });
136}
137
138function replaceImages(node: SVGGElement) {
139 node.querySelectorAll<SVGImageElement>('image').forEach((image) => {
140 const href =
141 image.getAttribute('href') ?? image.getAttributeNS(XLINK_NS, 'href');
142 if (href === 'undefined' || !href?.startsWith('#')) {
143 return;
144 }
145 const width = image.getAttribute('width')?.replace('px', '') ?? '';
146 const height = image.getAttribute('height')?.replace('px', '') ?? '';
147 const foreign = document.createElementNS(SVG_NS, 'foreignObject');
148 foreign.setAttribute('x', image.getAttribute('x') ?? '');
149 foreign.setAttribute('y', image.getAttribute('y') ?? '');
150 foreign.setAttribute('width', width);
151 foreign.setAttribute('height', height);
152 const div = document.createElement('div');
153 div.classList.add('icon', `icon-${href.replace('#', '')}`);
154 foreign.appendChild(div);
155 const sibling = image.nextElementSibling;
156 // Since dot doesn't respect the `id` attribute on table cells with a single image,
157 // compute the ID based on the ID of the next element (the label).
158 if (
159 sibling !== null &&
160 sibling.tagName.toLowerCase() === 'g' &&
161 sibling.id !== ''
162 ) {
163 foreign.id = `${sibling.id},icon`;
164 }
165 image.parentNode?.replaceChild(foreign, image);
166 });
167}
168
169export default function postProcessSvg(svg: SVGSVGElement) {
170 // svg
171 // .querySelectorAll<SVGTitleElement>('title')
172 // .forEach((title) => title.parentElement?.removeChild(title));
173 svg.querySelectorAll<SVGGElement>('g.node').forEach((node) => {
174 optimizeNodeShapes(node);
175 hrefToClass(node);
176 replaceImages(node);
177 });
178 // Increase padding to fit box shadows for multi-objects.
179 const viewBox = [
180 svg.viewBox.baseVal.x - 6,
181 svg.viewBox.baseVal.y - 6,
182 svg.viewBox.baseVal.width + 12,
183 svg.viewBox.baseVal.height + 12,
184 ];
185 svg.setAttribute('viewBox', viewBox.join(' '));
186}