diff options
Diffstat (limited to 'subprojects/frontend/src/graph/postProcessSVG.ts')
-rw-r--r-- | subprojects/frontend/src/graph/postProcessSVG.ts | 186 |
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 | |||
7 | import { type BBox, parsePolygonBBox, parsePathBBox } from './parseBBox'; | ||
8 | |||
9 | const SVG_NS = 'http://www.w3.org/2000/svg'; | ||
10 | const XLINK_NS = 'http://www.w3.org/1999/xlink'; | ||
11 | |||
12 | function 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 | |||
21 | function 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 | |||
40 | function 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 | |||
76 | function 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 | |||
90 | function 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 | |||
116 | function 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 | |||
138 | function 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 | |||
169 | export 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 | } | ||