diff options
Diffstat (limited to 'subprojects/frontend/src/graph/postProcessSVG.ts')
-rw-r--r-- | subprojects/frontend/src/graph/postProcessSVG.ts | 133 |
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 @@ | |||
7 | import { type BBox, parsePolygonBBox, parsePathBBox } from './parseBBox'; | 7 | import { type BBox, parsePolygonBBox, parsePathBBox } from './parseBBox'; |
8 | 8 | ||
9 | const SVG_NS = 'http://www.w3.org/2000/svg'; | 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 | } | ||
10 | 39 | ||
11 | function clipCompartmentBackground(node: SVGGElement) { | 40 | function 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 | ||
34 | function createRect( | 74 | function 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 | ||
114 | function 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 | |||
136 | function 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 | |||
74 | export default function postProcessSvg(svg: SVGSVGElement) { | 167 | export 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 | } |