@transitrix/diagrams 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/activities/__tests__/validate.test.js +32 -1
- package/dist/activities/cpm.js +11 -2
- package/dist/activities/gantt.js +6 -3
- package/dist/activities/types.d.ts +1 -0
- package/dist/activities/validate.js +24 -8
- package/dist/activity-card/__tests__/resolver.test.js +22 -1
- package/dist/activity-card/__tests__/validate.test.js +23 -1
- package/dist/activity-card/resolver.js +28 -20
- package/dist/activity-card/validate.js +22 -11
- package/dist/capability-map/index.d.ts +4 -0
- package/dist/capability-map/index.js +2 -0
- package/dist/capability-map/layout-tree.d.ts +42 -0
- package/dist/capability-map/layout-tree.js +65 -0
- package/dist/capability-map/render-capability-tree.d.ts +15 -0
- package/dist/capability-map/render-capability-tree.js +108 -0
- package/dist/capability-map/types.d.ts +2 -0
- package/dist/compliance/__tests__/impact.test.js +198 -0
- package/dist/compliance/html.js +1 -7
- package/dist/compliance/impact.d.ts +24 -1
- package/dist/compliance/impact.js +97 -21
- package/dist/factor/__tests__/validate.test.js +3 -3
- package/dist/factor/types.d.ts +1 -1
- package/dist/factor/validate.js +2 -2
- package/dist/fgca/__tests__/chain-table.test.js +2 -2
- package/dist/fgca/__tests__/layout.test.js +11 -11
- package/dist/fgca/__tests__/preview-layout.test.js +7 -7
- package/dist/fgca/__tests__/resolver.test.js +2 -2
- package/dist/fgca/__tests__/validate.test.js +2 -2
- package/dist/fgca/chain-table.d.ts +1 -1
- package/dist/fgca/chain-table.js +4 -4
- package/dist/fgca/index.d.ts +2 -2
- package/dist/fgca/index.js +1 -1
- package/dist/fgca/layout.d.ts +2 -2
- package/dist/fgca/layout.js +9 -9
- package/dist/fgca/nodeTypes.d.ts +1 -1
- package/dist/fgca/nodeTypes.js +2 -2
- package/dist/fgca/nodes/FGCAFactorNode.d.ts +3 -3
- package/dist/fgca/nodes/FGCAFactorNode.js +5 -5
- package/dist/fgca/parse-canonical.js +24 -13
- package/dist/fgca/preview-layout.d.ts +1 -1
- package/dist/fgca/preview-layout.js +4 -4
- package/dist/fgca/resolver.js +3 -1
- package/dist/fgca/types.d.ts +13 -11
- package/dist/fgca/types.js +2 -2
- package/dist/fgca/validate.d.ts +2 -2
- package/dist/fgca/validate.js +34 -23
- package/dist/process-blueprint/index.d.ts +1 -1
- package/dist/process-blueprint/layout.js +64 -37
- package/dist/process-blueprint/types.d.ts +14 -0
- package/dist/scenarios/index.d.ts +1 -1
- package/dist/scenarios/types.d.ts +4 -4
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.js +1 -1
- package/dist/theme/themes.js +28 -3
- package/dist/theme/tokens.d.ts +47 -1
- package/dist/theme/tokens.js +31 -1
- package/dist/webview/__tests__/entry-notations.test.js +2 -0
- package/dist/webview/__tests__/render-process.test.d.ts +1 -0
- package/dist/webview/__tests__/render-process.test.js +463 -0
- package/dist/webview/__tests__/render-util.test.d.ts +1 -0
- package/dist/webview/__tests__/render-util.test.js +54 -0
- package/dist/webview/entry.d.ts +1 -1
- package/dist/webview/entry.js +9 -5
- package/dist/webview/render-activities.d.ts +21 -1
- package/dist/webview/render-activities.js +91 -56
- package/dist/webview/render-activity-card.d.ts +11 -1
- package/dist/webview/render-activity-card.js +11 -2
- package/dist/webview/render-blocks.d.ts +24 -1
- package/dist/webview/render-blocks.js +58 -24
- package/dist/webview/render-fgca.d.ts +27 -0
- package/dist/webview/render-fgca.js +30 -16
- package/dist/webview/render-goals.d.ts +26 -1
- package/dist/webview/render-goals.js +30 -20
- package/dist/webview/render-process-blueprint.d.ts +11 -1
- package/dist/webview/render-process-blueprint.js +118 -53
- package/dist/webview/render-process.d.ts +95 -0
- package/dist/webview/render-process.js +392 -0
- package/dist/webview/render-scenarios.js +2 -2
- package/dist/webview/render-util.d.ts +19 -6
- package/dist/webview/render-util.js +23 -15
- package/package.json +15 -1
- package/src/activities/__tests__/validate.test.ts +35 -1
- package/src/activities/cpm.ts +14 -2
- package/src/activities/gantt.ts +6 -5
- package/src/activities/types.ts +1 -0
- package/src/activities/validate.ts +24 -8
- package/src/activity-card/__tests__/resolver.test.ts +24 -1
- package/src/activity-card/__tests__/validate.test.ts +25 -1
- package/src/activity-card/resolver.ts +29 -20
- package/src/activity-card/validate.ts +20 -11
- package/src/capability-map/index.ts +11 -0
- package/src/capability-map/layout-tree.ts +114 -0
- package/src/capability-map/render-capability-tree.ts +131 -0
- package/src/capability-map/types.ts +2 -0
- package/src/compliance/__tests__/impact.test.ts +219 -0
- package/src/compliance/html.ts +1 -8
- package/src/compliance/impact.ts +141 -23
- package/src/factor/__tests__/validate.test.ts +3 -3
- package/src/factor/types.ts +1 -1
- package/src/factor/validate.ts +2 -2
- package/src/fgca/__tests__/chain-table.test.ts +2 -2
- package/src/fgca/__tests__/layout.test.ts +13 -13
- package/src/fgca/__tests__/preview-layout.test.ts +7 -7
- package/src/fgca/__tests__/resolver.test.ts +2 -2
- package/src/fgca/__tests__/validate.test.ts +2 -2
- package/src/fgca/chain-table.ts +5 -5
- package/src/fgca/index.ts +3 -3
- package/src/fgca/layout.ts +11 -11
- package/src/fgca/nodeTypes.ts +3 -3
- package/src/fgca/nodes/FGCAFactorNode.tsx +6 -6
- package/src/fgca/parse-canonical.ts +26 -15
- package/src/fgca/preview-layout.ts +5 -5
- package/src/fgca/resolver.ts +3 -1
- package/src/fgca/types.ts +15 -13
- package/src/fgca/validate.ts +32 -21
- package/src/process-blueprint/index.ts +1 -0
- package/src/process-blueprint/layout.ts +71 -42
- package/src/process-blueprint/types.ts +15 -0
- package/src/scenarios/index.ts +2 -2
- package/src/scenarios/types.ts +4 -4
- package/src/scenarios/validate.ts +3 -3
- package/src/theme/index.ts +1 -1
- package/src/theme/themes.ts +29 -2
- package/src/theme/tokens.ts +33 -1
- package/src/webview/__tests__/entry-notations.test.ts +2 -0
- package/src/webview/__tests__/render-process.test.ts +538 -0
- package/src/webview/__tests__/render-util.test.ts +66 -0
- package/src/webview/entry.ts +11 -5
- package/src/webview/render-activities.ts +107 -67
- package/src/webview/render-activity-card.ts +11 -2
- package/src/webview/render-blocks.ts +78 -28
- package/src/webview/render-fgca.ts +53 -29
- package/src/webview/render-goals.ts +45 -23
- package/src/webview/render-process-blueprint.ts +156 -82
- package/src/webview/render-process.ts +507 -0
- package/src/webview/render-scenarios.ts +3 -3
- package/src/webview/render-util.ts +27 -14
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
* Only the DEFAULT network view (Project Schedule Network Diagram) is ported.
|
|
12
12
|
* The Gantt view, the CSS-only tab switcher and the interactive spacing /
|
|
13
13
|
* curvature controls stay in the VS Code preview.
|
|
14
|
+
*
|
|
15
|
+
* Single-emitter unification (review C): the canonical network body lives here
|
|
16
|
+
* in `renderActivitiesNetworkBody` and is shared verbatim with the VS Code
|
|
17
|
+
* preview's `networkSvg`.
|
|
14
18
|
*/
|
|
15
19
|
import { layoutActivities } from '../activities/layout.js';
|
|
16
20
|
import { computeCpm } from '../activities/cpm.js';
|
|
@@ -27,22 +31,110 @@ const N_NODE_W = 200;
|
|
|
27
31
|
const N_NODE_H = 80;
|
|
28
32
|
const N_PAD = 24;
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Network-view diagram CSS (critical path, milestones, edge colours) — the
|
|
36
|
+
* canonical rules shared with the VS Code preview's `ACTIVITIES_DIAGRAM_CSS`
|
|
37
|
+
* network subset. Embedded alongside the shared theme CSS so the SVG is
|
|
38
|
+
* self-contained for the JCEF host.
|
|
39
|
+
*/
|
|
40
|
+
export const ACTIVITIES_NETWORK_CSS = `
|
|
41
|
+
.act-node { fill: var(--ts-layer-activity, #d4edda); stroke: var(--ts-node-stroke, #94a3b8); stroke-width: 1; }
|
|
36
42
|
.critical-node { fill: #fff7ed; stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 2.5; }
|
|
37
43
|
.milestone-node { fill: #ecfeff; stroke: var(--ts-text-muted, #64748b); stroke-dasharray: 4 2; }
|
|
38
44
|
.critical-edge { stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 2; }
|
|
39
45
|
.arrow-fill-critical { fill: var(--ts-brand-orange, #ff4d00); }
|
|
40
46
|
`;
|
|
41
47
|
|
|
48
|
+
/** Arrowhead marker defs shared by both hosts for the network view. */
|
|
49
|
+
export const ACTIVITIES_NETWORK_DEFS = `<defs>
|
|
50
|
+
<marker id="arrow" markerWidth="8" markerHeight="8" refX="8" refY="3" orient="auto">
|
|
51
|
+
<path d="M0,0 L0,6 L8,3 z" class="arrow-fill"/>
|
|
52
|
+
</marker>
|
|
53
|
+
<marker id="arrow-crit" markerWidth="8" markerHeight="8" refX="8" refY="3" orient="auto">
|
|
54
|
+
<path d="M0,0 L0,6 L8,3 z" class="arrow-fill-critical"/>
|
|
55
|
+
</marker>
|
|
56
|
+
</defs>`;
|
|
57
|
+
|
|
42
58
|
function truncate(s: string, maxLen: number): string {
|
|
43
59
|
return s.length > maxLen ? s.slice(0, maxLen - 1) + '…' : s;
|
|
44
60
|
}
|
|
45
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Canonical Network (PSND) body — the node rects and edge paths that go inside
|
|
64
|
+
* the `<svg>`, shared verbatim by the VS Code preview and the host-neutral
|
|
65
|
+
* wrapper below. Excludes the host-specific title block and the (identical)
|
|
66
|
+
* marker defs (`ACTIVITIES_NETWORK_DEFS`).
|
|
67
|
+
*
|
|
68
|
+
* `ox`/`oy` are the canvas offsets (the caller folds in padding + any title
|
|
69
|
+
* height); `curvature`/`entryCurvature` scale the exit/entry edge handles.
|
|
70
|
+
*/
|
|
71
|
+
export function renderActivitiesNetworkBody(
|
|
72
|
+
layout: ActivitiesLayout,
|
|
73
|
+
cpm: ReturnType<typeof computeCpm>,
|
|
74
|
+
ox: number,
|
|
75
|
+
oy: number,
|
|
76
|
+
curvature: number,
|
|
77
|
+
entryCurvature: number | undefined,
|
|
78
|
+
): string {
|
|
79
|
+
const nodeMap = new Map(layout.nodes.map((n) => [n.id, n]));
|
|
80
|
+
|
|
81
|
+
// SVG paints later siblings over earlier ones. Sort non-critical first so the
|
|
82
|
+
// bright orange critical-path edges land on top — a gray edge crossing an
|
|
83
|
+
// orange one shouldn't bury the critical signal.
|
|
84
|
+
const orderedEdges = [...layout.edges].sort(
|
|
85
|
+
(a, b) => Number(Boolean(a.isCritical)) - Number(Boolean(b.isCritical)),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const edgeSvg = orderedEdges
|
|
89
|
+
.map((e) => {
|
|
90
|
+
const s = nodeMap.get(e.sourceId);
|
|
91
|
+
const t = nodeMap.get(e.targetId);
|
|
92
|
+
if (!s || !t) return '';
|
|
93
|
+
const sx = s.x + ox + s.width;
|
|
94
|
+
const sy = s.y + oy + s.height / 2;
|
|
95
|
+
const tx = t.x + ox;
|
|
96
|
+
const ty = t.y + oy + t.height / 2;
|
|
97
|
+
const cls = e.isCritical ? 'diagram-edge critical-edge' : 'diagram-edge';
|
|
98
|
+
const marker = `url(#${e.isCritical ? 'arrow-crit' : 'arrow'})`;
|
|
99
|
+
return `<path d="${horizontalCubicEdgePath(sx, sy, tx, ty, curvature, entryCurvature)}" class="${cls}" marker-end="${marker}"/>`;
|
|
100
|
+
})
|
|
101
|
+
.join('\n');
|
|
102
|
+
|
|
103
|
+
const nodeSvg = layout.nodes
|
|
104
|
+
.map((n) => {
|
|
105
|
+
const x = n.x + ox;
|
|
106
|
+
const y = n.y + oy;
|
|
107
|
+
const isCritical = cpm.get(n.id)?.isCritical ?? false;
|
|
108
|
+
const durVal = n.data.duration;
|
|
109
|
+
const isMilestone = (durVal ?? -1) === 0;
|
|
110
|
+
const cls = `diagram-node act-node ${isCritical ? 'critical-node' : ''} ${isMilestone ? 'milestone-node' : ''}`.trim();
|
|
111
|
+
const idLabel = escXml(n.id);
|
|
112
|
+
const words = n.data.name.split(' ');
|
|
113
|
+
let line1 = '';
|
|
114
|
+
let line2 = '';
|
|
115
|
+
for (const w of words) {
|
|
116
|
+
if ((line1 + ' ' + w).trim().length <= 24) line1 = (line1 + ' ' + w).trim();
|
|
117
|
+
else if ((line2 + ' ' + w).trim().length <= 24) line2 = (line2 + ' ' + w).trim();
|
|
118
|
+
else if (!line2) { line2 = w.slice(0, 22) + '…'; break; }
|
|
119
|
+
}
|
|
120
|
+
const twoLines = line2.length > 0;
|
|
121
|
+
const nameY1 = twoLines ? y + 18 : y + 28;
|
|
122
|
+
const nameY2 = y + 34;
|
|
123
|
+
const idY = twoLines ? y + 54 : y + 50;
|
|
124
|
+
const durLabel = (durVal !== undefined && durVal > 0) ? `${durVal}d` : '';
|
|
125
|
+
return [
|
|
126
|
+
`<rect class="${cls}" x="${x}" y="${y}" width="${N_NODE_W}" height="${N_NODE_H}" rx="8"/>`,
|
|
127
|
+
`<text class="text-primary" x="${x + N_NODE_W / 2}" y="${nameY1}" text-anchor="middle" dominant-baseline="central">${escXml(line1)}</text>`,
|
|
128
|
+
twoLines ? `<text class="text-primary" x="${x + N_NODE_W / 2}" y="${nameY2}" text-anchor="middle" dominant-baseline="central">${escXml(line2)}</text>` : '',
|
|
129
|
+
`<text class="text-id" x="${x + N_NODE_W / 2}" y="${idY}" text-anchor="middle" dominant-baseline="central">${idLabel}</text>`,
|
|
130
|
+
durLabel ? `<text class="text-secondary" x="${x + N_NODE_W - 8}" y="${y + N_NODE_H - 8}" text-anchor="end">${durLabel}</text>` : '',
|
|
131
|
+
].filter(Boolean).join('\n');
|
|
132
|
+
})
|
|
133
|
+
.join('\n');
|
|
134
|
+
|
|
135
|
+
return `${nodeSvg}\n${edgeSvg}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
46
138
|
export interface RenderActivitiesOptions {
|
|
47
139
|
/** Optional heading rendered as a `text-header` above the diagram. */
|
|
48
140
|
title?: string;
|
|
@@ -84,75 +176,23 @@ export function renderActivitiesSvg(doc: ActivityDoc, options: RenderActivitiesO
|
|
|
84
176
|
const ox = -layout.bounds.x + N_PAD;
|
|
85
177
|
const oy = -layout.bounds.y + N_PAD + titleH;
|
|
86
178
|
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
// SVG paints later siblings over earlier ones. Sort non-critical first so the
|
|
90
|
-
// bright orange critical-path edges land on top — a gray edge crossing an
|
|
91
|
-
// orange one shouldn't bury the critical signal.
|
|
92
|
-
const orderedEdges = [...layout.edges].sort(
|
|
93
|
-
(a, b) => Number(Boolean(a.isCritical)) - Number(Boolean(b.isCritical)),
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
// Edge path = a single cubic Bézier with horizontal control handles (shared
|
|
97
|
-
// geometry in `edge-path.js`), so the curve meets the vertical node edge at a
|
|
98
|
-
// right angle and the marker-end arrow reads as perpendicular.
|
|
99
|
-
const edgeSvg = orderedEdges
|
|
100
|
-
.map((e) => {
|
|
101
|
-
const s = nodeMap.get(e.sourceId);
|
|
102
|
-
const t = nodeMap.get(e.targetId);
|
|
103
|
-
if (!s || !t) return '';
|
|
104
|
-
const sx = s.x + ox + s.width;
|
|
105
|
-
const sy = s.y + oy + s.height / 2;
|
|
106
|
-
const tx = t.x + ox;
|
|
107
|
-
const ty = t.y + oy + t.height / 2;
|
|
108
|
-
const cls = e.isCritical ? 'diagram-edge critical-edge' : 'diagram-edge';
|
|
109
|
-
const marker = `url(#${e.isCritical ? 'arrow-crit' : 'arrow'})`;
|
|
110
|
-
return `<path d="${horizontalCubicEdgePath(sx, sy, tx, ty, curvature, entryCurvature)}" class="${cls}" marker-end="${marker}"/>`;
|
|
111
|
-
})
|
|
112
|
-
.join('\n');
|
|
113
|
-
|
|
114
|
-
const nodeSvg = layout.nodes
|
|
115
|
-
.map((n) => {
|
|
116
|
-
const x = n.x + ox;
|
|
117
|
-
const y = n.y + oy;
|
|
118
|
-
const isCritical = cpm.get(n.id)?.isCritical ?? false;
|
|
119
|
-
const isMilestone = (n.data.duration ?? -1) === 0;
|
|
120
|
-
const cls = `diagram-node act-node ${isCritical ? 'critical-node' : ''} ${isMilestone ? 'milestone-node' : ''}`.trim();
|
|
121
|
-
const idLabel = escXml(n.id);
|
|
122
|
-
const nameLabel = escXml(truncate(n.data.name, 24));
|
|
123
|
-
const durLabel = n.data.duration !== undefined ? `${n.data.duration}d` : '—';
|
|
124
|
-
return [
|
|
125
|
-
`<rect class="${cls}" x="${x}" y="${y}" width="${N_NODE_W}" height="${N_NODE_H}" rx="6"/>`,
|
|
126
|
-
`<text class="text-id" x="${x + 8}" y="${y + 18}">${idLabel}</text>`,
|
|
127
|
-
`<text class="text-primary" x="${x + N_NODE_W / 2}" y="${y + N_NODE_H / 2}" text-anchor="middle" dominant-baseline="central">${nameLabel}</text>`,
|
|
128
|
-
`<text class="text-secondary" x="${x + N_NODE_W - 8}" y="${y + N_NODE_H - 10}" text-anchor="end">${escXml(durLabel)}</text>`,
|
|
129
|
-
].join('\n');
|
|
130
|
-
})
|
|
131
|
-
.join('\n');
|
|
179
|
+
const body = renderActivitiesNetworkBody(layout, cpm, ox, oy, curvature, entryCurvature);
|
|
132
180
|
|
|
133
181
|
const titleSvg = title
|
|
134
182
|
? `<text class="text-header" x="${N_PAD}" y="${N_PAD - 6}">${escXml(title)}</text>`
|
|
135
183
|
: '';
|
|
136
184
|
|
|
137
|
-
// Embed the shared theme CSS plus the
|
|
138
|
-
// the
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
const embedCss = generateSvgEmbedCss('transitrix') +
|
|
185
|
+
// Embed the shared theme CSS plus the network diagram CSS inside the SVG so
|
|
186
|
+
// the rendered output is self-contained — the JCEF host page only needs to
|
|
187
|
+
// drop the SVG into the DOM and styling resolves without any cooperation from
|
|
188
|
+
// the host stylesheet. Matches what the VS Code path produces via
|
|
189
|
+
// `prepareSvgForExport`.
|
|
190
|
+
const embedCss = generateSvgEmbedCss('transitrix') + ACTIVITIES_NETWORK_CSS;
|
|
143
191
|
|
|
144
192
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
|
|
145
193
|
<style>${embedCss}</style>
|
|
146
|
-
|
|
147
|
-
<marker id="arrow" markerWidth="8" markerHeight="8" refX="8" refY="3" orient="auto">
|
|
148
|
-
<path d="M0,0 L0,6 L8,3 z" class="arrow-fill"/>
|
|
149
|
-
</marker>
|
|
150
|
-
<marker id="arrow-crit" markerWidth="8" markerHeight="8" refX="8" refY="3" orient="auto">
|
|
151
|
-
<path d="M0,0 L0,6 L8,3 z" class="arrow-fill-critical"/>
|
|
152
|
-
</marker>
|
|
153
|
-
</defs>
|
|
194
|
+
${ACTIVITIES_NETWORK_DEFS}
|
|
154
195
|
${titleSvg}
|
|
155
|
-
${
|
|
156
|
-
${edgeSvg}
|
|
196
|
+
${body}
|
|
157
197
|
</svg>`;
|
|
158
198
|
}
|
|
@@ -75,7 +75,16 @@ function resolveSingleDoc(doc: ActivityCardDoc): ResolvedActivityCard {
|
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
/**
|
|
79
|
+
* The single Activity Card SVG body emitter shared by every host. Takes an
|
|
80
|
+
* already-laid-out {@link ActivityCardLayout} plus the canvas offset and emits
|
|
81
|
+
* the body elements (card, header, badges, dates, chain sections, milestones,
|
|
82
|
+
* child activities, footer) — everything except the host chrome (`<defs>`
|
|
83
|
+
* marker, title, embedded CSS, `<svg>` wrapper). The VS Code preview and the
|
|
84
|
+
* host-neutral {@link renderActivityCardSvg} both build their own chrome around
|
|
85
|
+
* this single source of truth.
|
|
86
|
+
*/
|
|
87
|
+
export function renderActivityCardBody(layout: ActivityCardLayout, ox: number, oy: number): string {
|
|
79
88
|
const parts: string[] = [];
|
|
80
89
|
|
|
81
90
|
// Outer card.
|
|
@@ -245,7 +254,7 @@ export function renderActivityCardSvg(
|
|
|
245
254
|
const ox = PAD;
|
|
246
255
|
const oy = PAD + titleH;
|
|
247
256
|
|
|
248
|
-
const body =
|
|
257
|
+
const body = renderActivityCardBody(layout, ox, oy);
|
|
249
258
|
|
|
250
259
|
const titleSvg = title
|
|
251
260
|
? `<text class="text-header" x="${PAD}" y="${PAD + 14}">${escXml(`Activity Card — ${title}`)}</text>`
|
|
@@ -14,11 +14,16 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { layoutNestedBlocks } from '../blocks/layout.js';
|
|
16
16
|
import type { BlocksFile, BlocksLayout, LaidOutBlock } from '../blocks/types.js';
|
|
17
|
-
import { generateSvgEmbedCss } from '../theme/index.js';
|
|
17
|
+
import { generateSvgEmbedCss, type ThemeId } from '../theme/index.js';
|
|
18
18
|
import { escXml } from './render-util.js';
|
|
19
19
|
|
|
20
20
|
const PAD = 24;
|
|
21
21
|
|
|
22
|
+
// Approximate character width (px) at text-primary size (12px/600).
|
|
23
|
+
const CHAR_W = 7;
|
|
24
|
+
// Approximate character width (px) at text-id size (10px/600) — used for ID suffix.
|
|
25
|
+
const CHAR_W_ID = 6;
|
|
26
|
+
|
|
22
27
|
export interface RenderBlocksOptions {
|
|
23
28
|
title?: string;
|
|
24
29
|
}
|
|
@@ -36,27 +41,89 @@ function levelClassForDepth(depth: number): string {
|
|
|
36
41
|
return `level-${idx}`;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
|
|
44
|
+
/** Truncate at a word boundary so labels never break in the middle of a word. */
|
|
45
|
+
function wordTruncate(text: string, maxChars: number): string {
|
|
40
46
|
if (text.length <= maxChars) return text;
|
|
41
|
-
|
|
47
|
+
const cut = text.slice(0, maxChars - 1);
|
|
48
|
+
const lastSpace = cut.lastIndexOf(' ');
|
|
49
|
+
return (lastSpace > maxChars / 2 ? cut.slice(0, lastSpace) : cut) + '…';
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
function emitBlockSvg(b: LaidOutBlock, ox: number, oy: number, parts: string[]): void {
|
|
45
53
|
const cls = levelClassForDepth(b.depth);
|
|
54
|
+
const cx = b.x + ox + b.width / 2;
|
|
46
55
|
parts.push(
|
|
47
56
|
`<rect class="diagram-node ${cls}" x="${b.x + ox}" y="${b.y + oy}" width="${b.width}" height="${b.height}" rx="6"/>`,
|
|
48
57
|
);
|
|
49
58
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
const isLeaf = b.children.length === 0;
|
|
60
|
+
if (isLeaf) {
|
|
61
|
+
// Leaf block: name on upper line, ID on lower line — both centred.
|
|
62
|
+
const nameY = b.y + oy + Math.round(b.height * 0.38);
|
|
63
|
+
const idY = b.y + oy + Math.round(b.height * 0.66);
|
|
64
|
+
const maxChars = Math.max(4, Math.floor(b.width / CHAR_W));
|
|
65
|
+
parts.push(
|
|
66
|
+
`<text class="text-primary" x="${cx}" y="${nameY}" text-anchor="middle" dominant-baseline="central">${escXml(wordTruncate(b.name, maxChars))}</text>`,
|
|
67
|
+
`<text class="text-id" x="${cx}" y="${idY}" text-anchor="middle" dominant-baseline="central">${escXml(b.id)}</text>`,
|
|
68
|
+
);
|
|
69
|
+
} else {
|
|
70
|
+
// Container block: name + (ID) on the single header line; ID in grey.
|
|
71
|
+
const headerY = b.y + oy + b.headerHeight / 2;
|
|
72
|
+
const idSuffix = ` (${b.id})`;
|
|
73
|
+
const idSuffixW = idSuffix.length * CHAR_W_ID;
|
|
74
|
+
const nameMaxChars = Math.max(4, Math.floor((b.width - idSuffixW) / CHAR_W));
|
|
75
|
+
const nameText = wordTruncate(b.name, nameMaxChars);
|
|
76
|
+
parts.push(
|
|
77
|
+
`<text class="text-primary" x="${cx}" y="${headerY}" text-anchor="middle" dominant-baseline="central">${escXml(nameText)}<tspan fill="var(--ts-text-secondary, #64748b)" font-size="10" font-weight="600">${escXml(idSuffix)}</tspan></text>`,
|
|
78
|
+
);
|
|
79
|
+
for (const c of b.children) emitBlockSvg(c, ox, oy, parts);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
56
82
|
|
|
57
|
-
|
|
83
|
+
export interface RenderBlocksLayoutOptions {
|
|
84
|
+
/** Extra vertical space reserved at the top of the canvas (e.g. for a title block). */
|
|
85
|
+
topInset?: number;
|
|
86
|
+
/** Raw SVG injected immediately after the opening tag — a header line or a full title block. */
|
|
87
|
+
title?: string;
|
|
88
|
+
/** When set, the theme CSS is embedded as `<style>` so the SVG is self-contained. */
|
|
89
|
+
embedCssTheme?: ThemeId;
|
|
58
90
|
}
|
|
59
91
|
|
|
92
|
+
/**
|
|
93
|
+
* The single Nested Blocks SVG emitter shared by every host. Takes an
|
|
94
|
+
* already-computed {@link BlocksLayout} (callers decide the layout options) and
|
|
95
|
+
* produces the `<svg>`. Hosts wrap it with their own chrome:
|
|
96
|
+
* - IntelliJ/UI via {@link renderBlocksSvg} (embedded CSS + simple header);
|
|
97
|
+
* - VS Code's blocks preview (rich title block, no embedded CSS — the webview
|
|
98
|
+
* and the export path own styling).
|
|
99
|
+
*/
|
|
100
|
+
export function renderBlocksLayoutSvg(
|
|
101
|
+
layout: BlocksLayout,
|
|
102
|
+
options: RenderBlocksLayoutOptions = {},
|
|
103
|
+
): string {
|
|
104
|
+
const { topInset = 0, title = '', embedCssTheme } = options;
|
|
105
|
+
|
|
106
|
+
const w = layout.bounds.width + PAD * 2;
|
|
107
|
+
const h = layout.bounds.height + PAD * 2 + topInset;
|
|
108
|
+
const ox = -layout.bounds.x + PAD;
|
|
109
|
+
const oy = -layout.bounds.y + PAD + topInset;
|
|
110
|
+
|
|
111
|
+
const parts: string[] = [];
|
|
112
|
+
for (const top of layout.blocks) emitBlockSvg(top, ox, oy, parts);
|
|
113
|
+
|
|
114
|
+
const styleLine = embedCssTheme ? `\n<style>${generateSvgEmbedCss(embedCssTheme)}</style>` : '';
|
|
115
|
+
|
|
116
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">${styleLine}
|
|
117
|
+
${title}
|
|
118
|
+
${parts.join('\n')}
|
|
119
|
+
</svg>`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Host-neutral blocks renderer (IntelliJ/UI). Lays the doc out with the default
|
|
124
|
+
* spacing, then delegates the actual SVG emission to {@link renderBlocksLayoutSvg}
|
|
125
|
+
* with the shared theme CSS embedded so the output is self-contained.
|
|
126
|
+
*/
|
|
60
127
|
export function renderBlocksSvg(doc: BlocksFile, options: RenderBlocksOptions = {}): string {
|
|
61
128
|
const { title = '' } = options;
|
|
62
129
|
|
|
@@ -66,26 +133,9 @@ export function renderBlocksSvg(doc: BlocksFile, options: RenderBlocksOptions =
|
|
|
66
133
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
|
|
67
134
|
}
|
|
68
135
|
|
|
69
|
-
const w = layout.bounds.width + PAD * 2;
|
|
70
|
-
const h = layout.bounds.height + PAD * 2;
|
|
71
|
-
const ox = -layout.bounds.x + PAD;
|
|
72
|
-
const oy = -layout.bounds.y + PAD;
|
|
73
|
-
|
|
74
|
-
const parts: string[] = [];
|
|
75
|
-
for (const top of layout.blocks) emitBlockSvg(top, ox, oy, parts);
|
|
76
|
-
|
|
77
136
|
const titleSvg = title
|
|
78
137
|
? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(`Nested Blocks — ${title}`)}</text>`
|
|
79
138
|
: '';
|
|
80
139
|
|
|
81
|
-
|
|
82
|
-
// self-contained — the JCEF host page only needs to drop the SVG into the
|
|
83
|
-
// DOM and styling resolves without any cooperation from the host stylesheet.
|
|
84
|
-
const embedCss = generateSvgEmbedCss('transitrix');
|
|
85
|
-
|
|
86
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
|
|
87
|
-
<style>${embedCss}</style>
|
|
88
|
-
${titleSvg}
|
|
89
|
-
${parts.join('\n')}
|
|
90
|
-
</svg>`;
|
|
140
|
+
return renderBlocksLayoutSvg(layout, { title: titleSvg, embedCssTheme: 'transitrix' });
|
|
91
141
|
}
|
|
@@ -26,33 +26,30 @@ import { generateSvgEmbedCss } from '../theme/index.js';
|
|
|
26
26
|
import { escXml } from './render-util.js';
|
|
27
27
|
|
|
28
28
|
const COL_LABELS: Record<FGCAPreviewColumn, string> = {
|
|
29
|
-
|
|
29
|
+
driver: 'Drivers (D)',
|
|
30
30
|
goal: 'Goals (G)',
|
|
31
31
|
change: 'Changes (C)',
|
|
32
|
-
activity: '
|
|
32
|
+
activity: 'Actions (A)',
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
/** `'fga'` hides the Changes column; defaults to `'fgca'`. */
|
|
37
|
-
variant?: 'fgca' | 'fga';
|
|
38
|
-
/** Optional heading rendered as a left-anchored `text-header` line. */
|
|
39
|
-
title?: string;
|
|
40
|
-
/** Exit edge curvature; 1 = default, 0 = straight, higher = stronger arc. */
|
|
41
|
-
curvature?: number;
|
|
42
|
-
/** Entry curvature at the target node; defaults to `curvature` when omitted. */
|
|
43
|
-
entryCurvature?: number;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function renderFgcaSvg(doc: FGCADoc, options: RenderFgcaOptions = {}): string {
|
|
47
|
-
const { variant = 'fgca', title = '', curvature = DEFAULT_EDGE_CURVATURE, entryCurvature } = options;
|
|
48
|
-
const hideChanges = variant === 'fga';
|
|
49
|
-
|
|
50
|
-
const { nodes, edges, columns, width, height } = layoutFGCAPreview(doc, { hideChanges });
|
|
51
|
-
|
|
52
|
-
if (nodes.length === 0) {
|
|
53
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
|
|
54
|
-
}
|
|
35
|
+
type FgcaLayout = ReturnType<typeof layoutFGCAPreview>;
|
|
55
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Canonical FGCA/FGA body — the column headers, node rects and edge paths that
|
|
39
|
+
* go inside the `<svg>`, shared verbatim by the VS Code preview (`buildSvg`)
|
|
40
|
+
* and the host-neutral wrapper below. Excludes the host-specific title block
|
|
41
|
+
* and the (identical) `<defs>` arrow marker. Coordinates are absolute (the
|
|
42
|
+
* layout's own); the VS Code path wraps this in a `translate(0, titleH)` group
|
|
43
|
+
* to make room for its title block, while the host-neutral wrapper renders it
|
|
44
|
+
* untranslated.
|
|
45
|
+
*/
|
|
46
|
+
export function renderFgcaBody(
|
|
47
|
+
columns: FgcaLayout['columns'],
|
|
48
|
+
nodes: FgcaLayout['nodes'],
|
|
49
|
+
edges: FgcaLayout['edges'],
|
|
50
|
+
curvature: number,
|
|
51
|
+
entryCurvature: number | undefined,
|
|
52
|
+
): string {
|
|
56
53
|
const headerSvg = columns
|
|
57
54
|
.map(({ col, x }) =>
|
|
58
55
|
[
|
|
@@ -85,20 +82,49 @@ export function renderFgcaSvg(doc: FGCADoc, options: RenderFgcaOptions = {}): st
|
|
|
85
82
|
}
|
|
86
83
|
}
|
|
87
84
|
const twoLines = line2.length > 0;
|
|
88
|
-
const y1 = twoLines ? n.y +
|
|
89
|
-
const y2 =
|
|
85
|
+
const y1 = twoLines ? n.y + 16 : n.y + 26;
|
|
86
|
+
const y2 = n.y + 32;
|
|
87
|
+
const idY = twoLines ? n.y + 54 : n.y + 50;
|
|
88
|
+
const entityId = n.id.slice(n.id.indexOf('_') + 1);
|
|
90
89
|
return [
|
|
91
90
|
`<rect class="diagram-node layer-${n.col}" x="${n.x}" y="${n.y}" width="${NODE_W}" height="${NODE_H}" rx="8"/>`,
|
|
92
|
-
`<text class="text-primary" x="${n.x + NODE_W / 2}" y="${y1}" text-anchor="middle">${escXml(line1)}</text>`,
|
|
91
|
+
`<text class="text-primary" x="${n.x + NODE_W / 2}" y="${y1}" text-anchor="middle" dominant-baseline="central">${escXml(line1)}</text>`,
|
|
93
92
|
twoLines
|
|
94
|
-
? `<text class="text-secondary" x="${n.x + NODE_W / 2}" y="${y2}" text-anchor="middle">${escXml(line2)}</text>`
|
|
93
|
+
? `<text class="text-secondary" x="${n.x + NODE_W / 2}" y="${y2}" text-anchor="middle" dominant-baseline="central">${escXml(line2)}</text>`
|
|
95
94
|
: '',
|
|
95
|
+
`<text class="text-id" x="${n.x + NODE_W / 2}" y="${idY}" text-anchor="middle" dominant-baseline="central">${escXml(entityId)}</text>`,
|
|
96
96
|
]
|
|
97
97
|
.filter(Boolean)
|
|
98
98
|
.join('\n');
|
|
99
99
|
})
|
|
100
100
|
.join('\n');
|
|
101
101
|
|
|
102
|
+
return `${headerSvg}\n${nodeSvg}\n${edgeSvg}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface RenderFgcaOptions {
|
|
106
|
+
/** `'fga'` hides the Changes column; defaults to `'fgca'`. */
|
|
107
|
+
variant?: 'fgca' | 'fga';
|
|
108
|
+
/** Optional heading rendered as a left-anchored `text-header` line. */
|
|
109
|
+
title?: string;
|
|
110
|
+
/** Exit edge curvature; 1 = default, 0 = straight, higher = stronger arc. */
|
|
111
|
+
curvature?: number;
|
|
112
|
+
/** Entry curvature at the target node; defaults to `curvature` when omitted. */
|
|
113
|
+
entryCurvature?: number;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function renderFgcaSvg(doc: FGCADoc, options: RenderFgcaOptions = {}): string {
|
|
117
|
+
const { variant = 'fgca', title = '', curvature = DEFAULT_EDGE_CURVATURE, entryCurvature } = options;
|
|
118
|
+
const hideChanges = variant === 'fga';
|
|
119
|
+
|
|
120
|
+
const { nodes, edges, columns, width, height } = layoutFGCAPreview(doc, { hideChanges });
|
|
121
|
+
|
|
122
|
+
if (nodes.length === 0) {
|
|
123
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const body = renderFgcaBody(columns, nodes, edges, curvature, entryCurvature);
|
|
127
|
+
|
|
102
128
|
const titleSvg = title
|
|
103
129
|
? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(title)}</text>`
|
|
104
130
|
: '';
|
|
@@ -116,8 +142,6 @@ export function renderFgcaSvg(doc: FGCADoc, options: RenderFgcaOptions = {}): st
|
|
|
116
142
|
</marker>
|
|
117
143
|
</defs>
|
|
118
144
|
${titleSvg}
|
|
119
|
-
${
|
|
120
|
-
${nodeSvg}
|
|
121
|
-
${edgeSvg}
|
|
145
|
+
${body}
|
|
122
146
|
</svg>`;
|
|
123
147
|
}
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
import { layoutGoalTree } from '../goals/layout.js';
|
|
14
14
|
import type { GoalTree, GoalTreeLayout, LaidOutEdge } from '../goals/types.js';
|
|
15
15
|
import { horizontalCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
|
|
16
|
-
import { generateSvgEmbedCss } from '../theme/index.js';
|
|
16
|
+
import { generateSvgEmbedCss, type ThemeId } from '../theme/index.js';
|
|
17
|
+
import { escXml } from './render-util.js';
|
|
17
18
|
|
|
18
19
|
const NODE_W = 250;
|
|
19
20
|
const NODE_H = 60;
|
|
@@ -23,20 +24,17 @@ const PAD = 24;
|
|
|
23
24
|
const LABEL_MAX = 36;
|
|
24
25
|
const LABEL_TRIM = 34;
|
|
25
26
|
|
|
26
|
-
function escXml(s: string): string {
|
|
27
|
-
return s
|
|
28
|
-
.replace(/&/g, '&')
|
|
29
|
-
.replace(/</g, '<')
|
|
30
|
-
.replace(/>/g, '>')
|
|
31
|
-
.replace(/"/g, '"');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
27
|
export interface RenderGoalsOptions {
|
|
35
28
|
treeName?: string;
|
|
36
29
|
curvature?: number;
|
|
37
30
|
entryCurvature?: number;
|
|
38
31
|
}
|
|
39
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Host-neutral goals renderer (IntelliJ/UI). Lays the tree out with the default
|
|
35
|
+
* spacing, then delegates the actual SVG emission to {@link renderGoalsLayoutSvg}
|
|
36
|
+
* with the shared theme CSS embedded so the output is self-contained.
|
|
37
|
+
*/
|
|
40
38
|
export function renderGoalsSvg(tree: GoalTree, options: RenderGoalsOptions = {}): string {
|
|
41
39
|
const { treeName = '', curvature = DEFAULT_EDGE_CURVATURE, entryCurvature } = options;
|
|
42
40
|
|
|
@@ -51,10 +49,43 @@ export function renderGoalsSvg(tree: GoalTree, options: RenderGoalsOptions = {})
|
|
|
51
49
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
|
|
52
50
|
}
|
|
53
51
|
|
|
52
|
+
const title = treeName
|
|
53
|
+
? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(`Goal tree — ${treeName}`)}</text>`
|
|
54
|
+
: '';
|
|
55
|
+
|
|
56
|
+
// Embed the shared theme CSS so the JCEF host page only needs to drop the SVG
|
|
57
|
+
// into the DOM — styling resolves without help from the host stylesheet. The
|
|
58
|
+
// VS Code preview omits this (its webview supplies the CSS) and embeds it only
|
|
59
|
+
// on export via `prepareSvgForExport`.
|
|
60
|
+
return renderGoalsLayoutSvg(layout, { curvature, entryCurvature, title, embedCssTheme: 'transitrix' });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface RenderGoalsLayoutOptions {
|
|
64
|
+
curvature?: number;
|
|
65
|
+
entryCurvature?: number;
|
|
66
|
+
/** Extra vertical space reserved at the top of the canvas (e.g. for a title block). */
|
|
67
|
+
topInset?: number;
|
|
68
|
+
/** Raw SVG injected immediately after `<defs>` — a header line or a full title block. */
|
|
69
|
+
title?: string;
|
|
70
|
+
/** When set, the theme CSS is embedded as `<style>` so the SVG is self-contained. */
|
|
71
|
+
embedCssTheme?: ThemeId;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The single goals SVG emitter shared by every host. Takes an already-computed
|
|
76
|
+
* {@link GoalTreeLayout} (callers decide spacing/scope) and produces the `<svg>`.
|
|
77
|
+
* Hosts wrap it with their own chrome:
|
|
78
|
+
* - IntelliJ/UI via {@link renderGoalsSvg} (embedded CSS + simple header);
|
|
79
|
+
* - VS Code's goals preview (rich title block, no embedded CSS — the webview
|
|
80
|
+
* and the export path own styling).
|
|
81
|
+
*/
|
|
82
|
+
export function renderGoalsLayoutSvg(layout: GoalTreeLayout, options: RenderGoalsLayoutOptions = {}): string {
|
|
83
|
+
const { curvature = DEFAULT_EDGE_CURVATURE, entryCurvature, topInset = 0, title = '', embedCssTheme } = options;
|
|
84
|
+
|
|
54
85
|
const w = layout.bounds.width + PAD * 2;
|
|
55
|
-
const h = layout.bounds.height + PAD * 2;
|
|
86
|
+
const h = layout.bounds.height + PAD * 2 + topInset;
|
|
56
87
|
const ox = -layout.bounds.x + PAD;
|
|
57
|
-
const oy = -layout.bounds.y + PAD;
|
|
88
|
+
const oy = -layout.bounds.y + PAD + topInset;
|
|
58
89
|
|
|
59
90
|
const nodeMap = new Map(layout.nodes.map((n) => [n.id, n]));
|
|
60
91
|
|
|
@@ -87,24 +118,15 @@ export function renderGoalsSvg(tree: GoalTree, options: RenderGoalsOptions = {})
|
|
|
87
118
|
})
|
|
88
119
|
.join('\n');
|
|
89
120
|
|
|
90
|
-
const
|
|
91
|
-
? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(`Goal tree — ${treeName}`)}</text>`
|
|
92
|
-
: '';
|
|
93
|
-
|
|
94
|
-
// Embed the shared theme CSS inside the SVG so the rendered output is
|
|
95
|
-
// self-contained — the JCEF host page only needs to drop the SVG into the
|
|
96
|
-
// DOM and styling resolves without any cooperation from the host stylesheet.
|
|
97
|
-
// Matches what the VS Code path produces via `prepareSvgForExport`.
|
|
98
|
-
const embedCss = generateSvgEmbedCss('transitrix');
|
|
121
|
+
const styleLine = embedCssTheme ? `\n<style>${generateSvgEmbedCss(embedCssTheme)}</style>` : '';
|
|
99
122
|
|
|
100
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}"
|
|
101
|
-
<style>${embedCss}</style>
|
|
123
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">${styleLine}
|
|
102
124
|
<defs>
|
|
103
125
|
<marker id="arrow" markerWidth="8" markerHeight="8" refX="8" refY="3" orient="auto">
|
|
104
126
|
<path d="M0,0 L0,6 L8,3 z" class="arrow-fill"/>
|
|
105
127
|
</marker>
|
|
106
128
|
</defs>
|
|
107
|
-
${
|
|
129
|
+
${title}
|
|
108
130
|
${nodeSvg}
|
|
109
131
|
${edgeSvg}
|
|
110
132
|
</svg>`;
|