@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';
|
|
@@ -20,54 +24,46 @@ import { escXml } from './render-util.js';
|
|
|
20
24
|
const N_NODE_W = 200;
|
|
21
25
|
const N_NODE_H = 80;
|
|
22
26
|
const N_PAD = 24;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Network-view diagram CSS (critical path, milestones, edge colours) — the
|
|
29
|
+
* canonical rules shared with the VS Code preview's `ACTIVITIES_DIAGRAM_CSS`
|
|
30
|
+
* network subset. Embedded alongside the shared theme CSS so the SVG is
|
|
31
|
+
* self-contained for the JCEF host.
|
|
32
|
+
*/
|
|
33
|
+
export const ACTIVITIES_NETWORK_CSS = `
|
|
34
|
+
.act-node { fill: var(--ts-layer-activity, #d4edda); stroke: var(--ts-node-stroke, #94a3b8); stroke-width: 1; }
|
|
29
35
|
.critical-node { fill: #fff7ed; stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 2.5; }
|
|
30
36
|
.milestone-node { fill: #ecfeff; stroke: var(--ts-text-muted, #64748b); stroke-dasharray: 4 2; }
|
|
31
37
|
.critical-edge { stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 2; }
|
|
32
38
|
.arrow-fill-critical { fill: var(--ts-brand-orange, #ff4d00); }
|
|
33
39
|
`;
|
|
40
|
+
/** Arrowhead marker defs shared by both hosts for the network view. */
|
|
41
|
+
export const ACTIVITIES_NETWORK_DEFS = `<defs>
|
|
42
|
+
<marker id="arrow" markerWidth="8" markerHeight="8" refX="8" refY="3" orient="auto">
|
|
43
|
+
<path d="M0,0 L0,6 L8,3 z" class="arrow-fill"/>
|
|
44
|
+
</marker>
|
|
45
|
+
<marker id="arrow-crit" markerWidth="8" markerHeight="8" refX="8" refY="3" orient="auto">
|
|
46
|
+
<path d="M0,0 L0,6 L8,3 z" class="arrow-fill-critical"/>
|
|
47
|
+
</marker>
|
|
48
|
+
</defs>`;
|
|
34
49
|
function truncate(s, maxLen) {
|
|
35
50
|
return s.length > maxLen ? s.slice(0, maxLen - 1) + '…' : s;
|
|
36
51
|
}
|
|
37
52
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* `validateActivities` returns valid (the activities module has no parsed
|
|
43
|
-
* field, so the dispatcher does `doc as ActivityDoc`).
|
|
53
|
+
* Canonical Network (PSND) body — the node rects and edge paths that go inside
|
|
54
|
+
* the `<svg>`, shared verbatim by the VS Code preview and the host-neutral
|
|
55
|
+
* wrapper below. Excludes the host-specific title block and the (identical)
|
|
56
|
+
* marker defs (`ACTIVITIES_NETWORK_DEFS`).
|
|
44
57
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* critical-path computation, which `computeCpm` backfills with neutral CPM
|
|
48
|
-
* values), so this renderer simply renders whatever the layout returns rather
|
|
49
|
-
* than short-circuiting.
|
|
58
|
+
* `ox`/`oy` are the canvas offsets (the caller folds in padding + any title
|
|
59
|
+
* height); `curvature`/`entryCurvature` scale the exit/entry edge handles.
|
|
50
60
|
*/
|
|
51
|
-
export function
|
|
52
|
-
const { title = '', gaps = {}, curvature = DEFAULT_EDGE_CURVATURE, entryCurvature } = options;
|
|
53
|
-
const layout = layoutActivities(doc, gaps);
|
|
54
|
-
if (layout.nodes.length === 0) {
|
|
55
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
|
|
56
|
-
}
|
|
57
|
-
const cpm = computeCpm(doc.activities ?? []);
|
|
58
|
-
const titleH = title ? 24 : 0;
|
|
59
|
-
const w = layout.bounds.width + N_PAD * 2;
|
|
60
|
-
const h = layout.bounds.height + N_PAD * 2 + titleH;
|
|
61
|
-
const ox = -layout.bounds.x + N_PAD;
|
|
62
|
-
const oy = -layout.bounds.y + N_PAD + titleH;
|
|
61
|
+
export function renderActivitiesNetworkBody(layout, cpm, ox, oy, curvature, entryCurvature) {
|
|
63
62
|
const nodeMap = new Map(layout.nodes.map((n) => [n.id, n]));
|
|
64
63
|
// SVG paints later siblings over earlier ones. Sort non-critical first so the
|
|
65
64
|
// bright orange critical-path edges land on top — a gray edge crossing an
|
|
66
65
|
// orange one shouldn't bury the critical signal.
|
|
67
66
|
const orderedEdges = [...layout.edges].sort((a, b) => Number(Boolean(a.isCritical)) - Number(Boolean(b.isCritical)));
|
|
68
|
-
// Edge path = a single cubic Bézier with horizontal control handles (shared
|
|
69
|
-
// geometry in `edge-path.js`), so the curve meets the vertical node edge at a
|
|
70
|
-
// right angle and the marker-end arrow reads as perpendicular.
|
|
71
67
|
const edgeSvg = orderedEdges
|
|
72
68
|
.map((e) => {
|
|
73
69
|
const s = nodeMap.get(e.sourceId);
|
|
@@ -88,40 +84,79 @@ export function renderActivitiesSvg(doc, options = {}) {
|
|
|
88
84
|
const x = n.x + ox;
|
|
89
85
|
const y = n.y + oy;
|
|
90
86
|
const isCritical = cpm.get(n.id)?.isCritical ?? false;
|
|
91
|
-
const
|
|
87
|
+
const durVal = n.data.duration;
|
|
88
|
+
const isMilestone = (durVal ?? -1) === 0;
|
|
92
89
|
const cls = `diagram-node act-node ${isCritical ? 'critical-node' : ''} ${isMilestone ? 'milestone-node' : ''}`.trim();
|
|
93
90
|
const idLabel = escXml(n.id);
|
|
94
|
-
const
|
|
95
|
-
|
|
91
|
+
const words = n.data.name.split(' ');
|
|
92
|
+
let line1 = '';
|
|
93
|
+
let line2 = '';
|
|
94
|
+
for (const w of words) {
|
|
95
|
+
if ((line1 + ' ' + w).trim().length <= 24)
|
|
96
|
+
line1 = (line1 + ' ' + w).trim();
|
|
97
|
+
else if ((line2 + ' ' + w).trim().length <= 24)
|
|
98
|
+
line2 = (line2 + ' ' + w).trim();
|
|
99
|
+
else if (!line2) {
|
|
100
|
+
line2 = w.slice(0, 22) + '…';
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const twoLines = line2.length > 0;
|
|
105
|
+
const nameY1 = twoLines ? y + 18 : y + 28;
|
|
106
|
+
const nameY2 = y + 34;
|
|
107
|
+
const idY = twoLines ? y + 54 : y + 50;
|
|
108
|
+
const durLabel = (durVal !== undefined && durVal > 0) ? `${durVal}d` : '';
|
|
96
109
|
return [
|
|
97
|
-
`<rect class="${cls}" x="${x}" y="${y}" width="${N_NODE_W}" height="${N_NODE_H}" rx="
|
|
98
|
-
`<text class="text-
|
|
99
|
-
`<text class="text-primary" x="${x + N_NODE_W / 2}" y="${
|
|
100
|
-
`<text class="text-
|
|
101
|
-
|
|
110
|
+
`<rect class="${cls}" x="${x}" y="${y}" width="${N_NODE_W}" height="${N_NODE_H}" rx="8"/>`,
|
|
111
|
+
`<text class="text-primary" x="${x + N_NODE_W / 2}" y="${nameY1}" text-anchor="middle" dominant-baseline="central">${escXml(line1)}</text>`,
|
|
112
|
+
twoLines ? `<text class="text-primary" x="${x + N_NODE_W / 2}" y="${nameY2}" text-anchor="middle" dominant-baseline="central">${escXml(line2)}</text>` : '',
|
|
113
|
+
`<text class="text-id" x="${x + N_NODE_W / 2}" y="${idY}" text-anchor="middle" dominant-baseline="central">${idLabel}</text>`,
|
|
114
|
+
durLabel ? `<text class="text-secondary" x="${x + N_NODE_W - 8}" y="${y + N_NODE_H - 8}" text-anchor="end">${durLabel}</text>` : '',
|
|
115
|
+
].filter(Boolean).join('\n');
|
|
102
116
|
})
|
|
103
117
|
.join('\n');
|
|
118
|
+
return `${nodeSvg}\n${edgeSvg}`;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Render the network (PSND) view of an already-validated ActivityDoc to a
|
|
122
|
+
* self-contained SVG string.
|
|
123
|
+
*
|
|
124
|
+
* The caller passes a doc cast from the yaml-parsed `unknown` after
|
|
125
|
+
* `validateActivities` returns valid (the activities module has no parsed
|
|
126
|
+
* field, so the dispatcher does `doc as ActivityDoc`).
|
|
127
|
+
*
|
|
128
|
+
* Cyclic graphs degrade gracefully: `layoutActivities` / `computeCpm` defend
|
|
129
|
+
* against cycles internally (Kahn's topo-order omits cyclic nodes from the
|
|
130
|
+
* critical-path computation, which `computeCpm` backfills with neutral CPM
|
|
131
|
+
* values), so this renderer simply renders whatever the layout returns rather
|
|
132
|
+
* than short-circuiting.
|
|
133
|
+
*/
|
|
134
|
+
export function renderActivitiesSvg(doc, options = {}) {
|
|
135
|
+
const { title = '', gaps = {}, curvature = DEFAULT_EDGE_CURVATURE, entryCurvature } = options;
|
|
136
|
+
const layout = layoutActivities(doc, gaps);
|
|
137
|
+
if (layout.nodes.length === 0) {
|
|
138
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
|
|
139
|
+
}
|
|
140
|
+
const cpm = computeCpm(doc.activities ?? []);
|
|
141
|
+
const titleH = title ? 24 : 0;
|
|
142
|
+
const w = layout.bounds.width + N_PAD * 2;
|
|
143
|
+
const h = layout.bounds.height + N_PAD * 2 + titleH;
|
|
144
|
+
const ox = -layout.bounds.x + N_PAD;
|
|
145
|
+
const oy = -layout.bounds.y + N_PAD + titleH;
|
|
146
|
+
const body = renderActivitiesNetworkBody(layout, cpm, ox, oy, curvature, entryCurvature);
|
|
104
147
|
const titleSvg = title
|
|
105
148
|
? `<text class="text-header" x="${N_PAD}" y="${N_PAD - 6}">${escXml(title)}</text>`
|
|
106
149
|
: '';
|
|
107
|
-
// Embed the shared theme CSS plus the
|
|
108
|
-
// the
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
const embedCss = generateSvgEmbedCss('transitrix') +
|
|
150
|
+
// Embed the shared theme CSS plus the network diagram CSS inside the SVG so
|
|
151
|
+
// the rendered output is self-contained — the JCEF host page only needs to
|
|
152
|
+
// drop the SVG into the DOM and styling resolves without any cooperation from
|
|
153
|
+
// the host stylesheet. Matches what the VS Code path produces via
|
|
154
|
+
// `prepareSvgForExport`.
|
|
155
|
+
const embedCss = generateSvgEmbedCss('transitrix') + ACTIVITIES_NETWORK_CSS;
|
|
113
156
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
|
|
114
157
|
<style>${embedCss}</style>
|
|
115
|
-
|
|
116
|
-
<marker id="arrow" markerWidth="8" markerHeight="8" refX="8" refY="3" orient="auto">
|
|
117
|
-
<path d="M0,0 L0,6 L8,3 z" class="arrow-fill"/>
|
|
118
|
-
</marker>
|
|
119
|
-
<marker id="arrow-crit" markerWidth="8" markerHeight="8" refX="8" refY="3" orient="auto">
|
|
120
|
-
<path d="M0,0 L0,6 L8,3 z" class="arrow-fill-critical"/>
|
|
121
|
-
</marker>
|
|
122
|
-
</defs>
|
|
158
|
+
${ACTIVITIES_NETWORK_DEFS}
|
|
123
159
|
${titleSvg}
|
|
124
|
-
${
|
|
125
|
-
${edgeSvg}
|
|
160
|
+
${body}
|
|
126
161
|
</svg>`;
|
|
127
162
|
}
|
|
@@ -1,5 +1,15 @@
|
|
|
1
|
-
import type { ActivityCardDoc } from '../activity-card/types.js';
|
|
1
|
+
import type { ActivityCardDoc, ActivityCardLayout } from '../activity-card/types.js';
|
|
2
2
|
export interface RenderActivityCardOptions {
|
|
3
3
|
title?: string;
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* The single Activity Card SVG body emitter shared by every host. Takes an
|
|
7
|
+
* already-laid-out {@link ActivityCardLayout} plus the canvas offset and emits
|
|
8
|
+
* the body elements (card, header, badges, dates, chain sections, milestones,
|
|
9
|
+
* child activities, footer) — everything except the host chrome (`<defs>`
|
|
10
|
+
* marker, title, embedded CSS, `<svg>` wrapper). The VS Code preview and the
|
|
11
|
+
* host-neutral {@link renderActivityCardSvg} both build their own chrome around
|
|
12
|
+
* this single source of truth.
|
|
13
|
+
*/
|
|
14
|
+
export declare function renderActivityCardBody(layout: ActivityCardLayout, ox: number, oy: number): string;
|
|
5
15
|
export declare function renderActivityCardSvg(doc: ActivityCardDoc, options?: RenderActivityCardOptions): string;
|
|
@@ -60,7 +60,16 @@ function resolveSingleDoc(doc) {
|
|
|
60
60
|
notes: card?.notes,
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
-
|
|
63
|
+
/**
|
|
64
|
+
* The single Activity Card SVG body emitter shared by every host. Takes an
|
|
65
|
+
* already-laid-out {@link ActivityCardLayout} plus the canvas offset and emits
|
|
66
|
+
* the body elements (card, header, badges, dates, chain sections, milestones,
|
|
67
|
+
* child activities, footer) — everything except the host chrome (`<defs>`
|
|
68
|
+
* marker, title, embedded CSS, `<svg>` wrapper). The VS Code preview and the
|
|
69
|
+
* host-neutral {@link renderActivityCardSvg} both build their own chrome around
|
|
70
|
+
* this single source of truth.
|
|
71
|
+
*/
|
|
72
|
+
export function renderActivityCardBody(layout, ox, oy) {
|
|
64
73
|
const parts = [];
|
|
65
74
|
// Outer card.
|
|
66
75
|
parts.push(`<rect class="diagram-node level-0" x="${ox}" y="${oy}" width="${layout.bounds.width}" height="${layout.bounds.height}" rx="8"/>`);
|
|
@@ -177,7 +186,7 @@ export function renderActivityCardSvg(doc, options = {}) {
|
|
|
177
186
|
const h = layout.bounds.height + PAD * 2 + titleH;
|
|
178
187
|
const ox = PAD;
|
|
179
188
|
const oy = PAD + titleH;
|
|
180
|
-
const body =
|
|
189
|
+
const body = renderActivityCardBody(layout, ox, oy);
|
|
181
190
|
const titleSvg = title
|
|
182
191
|
? `<text class="text-header" x="${PAD}" y="${PAD + 14}">${escXml(`Activity Card — ${title}`)}</text>`
|
|
183
192
|
: '';
|
|
@@ -1,5 +1,28 @@
|
|
|
1
|
-
import type { BlocksFile } from '../blocks/types.js';
|
|
1
|
+
import type { BlocksFile, BlocksLayout } from '../blocks/types.js';
|
|
2
|
+
import { type ThemeId } from '../theme/index.js';
|
|
2
3
|
export interface RenderBlocksOptions {
|
|
3
4
|
title?: string;
|
|
4
5
|
}
|
|
6
|
+
export interface RenderBlocksLayoutOptions {
|
|
7
|
+
/** Extra vertical space reserved at the top of the canvas (e.g. for a title block). */
|
|
8
|
+
topInset?: number;
|
|
9
|
+
/** Raw SVG injected immediately after the opening tag — a header line or a full title block. */
|
|
10
|
+
title?: string;
|
|
11
|
+
/** When set, the theme CSS is embedded as `<style>` so the SVG is self-contained. */
|
|
12
|
+
embedCssTheme?: ThemeId;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The single Nested Blocks SVG emitter shared by every host. Takes an
|
|
16
|
+
* already-computed {@link BlocksLayout} (callers decide the layout options) and
|
|
17
|
+
* produces the `<svg>`. Hosts wrap it with their own chrome:
|
|
18
|
+
* - IntelliJ/UI via {@link renderBlocksSvg} (embedded CSS + simple header);
|
|
19
|
+
* - VS Code's blocks preview (rich title block, no embedded CSS — the webview
|
|
20
|
+
* and the export path own styling).
|
|
21
|
+
*/
|
|
22
|
+
export declare function renderBlocksLayoutSvg(layout: BlocksLayout, options?: RenderBlocksLayoutOptions): string;
|
|
23
|
+
/**
|
|
24
|
+
* Host-neutral blocks renderer (IntelliJ/UI). Lays the doc out with the default
|
|
25
|
+
* spacing, then delegates the actual SVG emission to {@link renderBlocksLayoutSvg}
|
|
26
|
+
* with the shared theme CSS embedded so the output is self-contained.
|
|
27
|
+
*/
|
|
5
28
|
export declare function renderBlocksSvg(doc: BlocksFile, options?: RenderBlocksOptions): string;
|
|
@@ -16,6 +16,10 @@ import { layoutNestedBlocks } from '../blocks/layout.js';
|
|
|
16
16
|
import { generateSvgEmbedCss } from '../theme/index.js';
|
|
17
17
|
import { escXml } from './render-util.js';
|
|
18
18
|
const PAD = 24;
|
|
19
|
+
// Approximate character width (px) at text-primary size (12px/600).
|
|
20
|
+
const CHAR_W = 7;
|
|
21
|
+
// Approximate character width (px) at text-id size (10px/600) — used for ID suffix.
|
|
22
|
+
const CHAR_W_ID = 6;
|
|
19
23
|
/**
|
|
20
24
|
* Pick the diagram-frame level class for a block at the given depth.
|
|
21
25
|
*
|
|
@@ -28,44 +32,74 @@ function levelClassForDepth(depth) {
|
|
|
28
32
|
const idx = Math.min(Math.max(depth - 1, 0), 6);
|
|
29
33
|
return `level-${idx}`;
|
|
30
34
|
}
|
|
31
|
-
|
|
35
|
+
/** Truncate at a word boundary so labels never break in the middle of a word. */
|
|
36
|
+
function wordTruncate(text, maxChars) {
|
|
32
37
|
if (text.length <= maxChars)
|
|
33
38
|
return text;
|
|
34
|
-
|
|
39
|
+
const cut = text.slice(0, maxChars - 1);
|
|
40
|
+
const lastSpace = cut.lastIndexOf(' ');
|
|
41
|
+
return (lastSpace > maxChars / 2 ? cut.slice(0, lastSpace) : cut) + '…';
|
|
35
42
|
}
|
|
36
43
|
function emitBlockSvg(b, ox, oy, parts) {
|
|
37
44
|
const cls = levelClassForDepth(b.depth);
|
|
45
|
+
const cx = b.x + ox + b.width / 2;
|
|
38
46
|
parts.push(`<rect class="diagram-node ${cls}" x="${b.x + ox}" y="${b.y + oy}" width="${b.width}" height="${b.height}" rx="6"/>`);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
const isLeaf = b.children.length === 0;
|
|
48
|
+
if (isLeaf) {
|
|
49
|
+
// Leaf block: name on upper line, ID on lower line — both centred.
|
|
50
|
+
const nameY = b.y + oy + Math.round(b.height * 0.38);
|
|
51
|
+
const idY = b.y + oy + Math.round(b.height * 0.66);
|
|
52
|
+
const maxChars = Math.max(4, Math.floor(b.width / CHAR_W));
|
|
53
|
+
parts.push(`<text class="text-primary" x="${cx}" y="${nameY}" text-anchor="middle" dominant-baseline="central">${escXml(wordTruncate(b.name, maxChars))}</text>`, `<text class="text-id" x="${cx}" y="${idY}" text-anchor="middle" dominant-baseline="central">${escXml(b.id)}</text>`);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
// Container block: name + (ID) on the single header line; ID in grey.
|
|
57
|
+
const headerY = b.y + oy + b.headerHeight / 2;
|
|
58
|
+
const idSuffix = ` (${b.id})`;
|
|
59
|
+
const idSuffixW = idSuffix.length * CHAR_W_ID;
|
|
60
|
+
const nameMaxChars = Math.max(4, Math.floor((b.width - idSuffixW) / CHAR_W));
|
|
61
|
+
const nameText = wordTruncate(b.name, nameMaxChars);
|
|
62
|
+
parts.push(`<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>`);
|
|
63
|
+
for (const c of b.children)
|
|
64
|
+
emitBlockSvg(c, ox, oy, parts);
|
|
65
|
+
}
|
|
45
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* The single Nested Blocks SVG emitter shared by every host. Takes an
|
|
69
|
+
* already-computed {@link BlocksLayout} (callers decide the layout options) and
|
|
70
|
+
* produces the `<svg>`. Hosts wrap it with their own chrome:
|
|
71
|
+
* - IntelliJ/UI via {@link renderBlocksSvg} (embedded CSS + simple header);
|
|
72
|
+
* - VS Code's blocks preview (rich title block, no embedded CSS — the webview
|
|
73
|
+
* and the export path own styling).
|
|
74
|
+
*/
|
|
75
|
+
export function renderBlocksLayoutSvg(layout, options = {}) {
|
|
76
|
+
const { topInset = 0, title = '', embedCssTheme } = options;
|
|
77
|
+
const w = layout.bounds.width + PAD * 2;
|
|
78
|
+
const h = layout.bounds.height + PAD * 2 + topInset;
|
|
79
|
+
const ox = -layout.bounds.x + PAD;
|
|
80
|
+
const oy = -layout.bounds.y + PAD + topInset;
|
|
81
|
+
const parts = [];
|
|
82
|
+
for (const top of layout.blocks)
|
|
83
|
+
emitBlockSvg(top, ox, oy, parts);
|
|
84
|
+
const styleLine = embedCssTheme ? `\n<style>${generateSvgEmbedCss(embedCssTheme)}</style>` : '';
|
|
85
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">${styleLine}
|
|
86
|
+
${title}
|
|
87
|
+
${parts.join('\n')}
|
|
88
|
+
</svg>`;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Host-neutral blocks renderer (IntelliJ/UI). Lays the doc out with the default
|
|
92
|
+
* spacing, then delegates the actual SVG emission to {@link renderBlocksLayoutSvg}
|
|
93
|
+
* with the shared theme CSS embedded so the output is self-contained.
|
|
94
|
+
*/
|
|
46
95
|
export function renderBlocksSvg(doc, options = {}) {
|
|
47
96
|
const { title = '' } = options;
|
|
48
97
|
const layout = layoutNestedBlocks(doc);
|
|
49
98
|
if (layout.blocks.length === 0) {
|
|
50
99
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
|
|
51
100
|
}
|
|
52
|
-
const w = layout.bounds.width + PAD * 2;
|
|
53
|
-
const h = layout.bounds.height + PAD * 2;
|
|
54
|
-
const ox = -layout.bounds.x + PAD;
|
|
55
|
-
const oy = -layout.bounds.y + PAD;
|
|
56
|
-
const parts = [];
|
|
57
|
-
for (const top of layout.blocks)
|
|
58
|
-
emitBlockSvg(top, ox, oy, parts);
|
|
59
101
|
const titleSvg = title
|
|
60
102
|
? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(`Nested Blocks — ${title}`)}</text>`
|
|
61
103
|
: '';
|
|
62
|
-
|
|
63
|
-
// self-contained — the JCEF host page only needs to drop the SVG into the
|
|
64
|
-
// DOM and styling resolves without any cooperation from the host stylesheet.
|
|
65
|
-
const embedCss = generateSvgEmbedCss('transitrix');
|
|
66
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
|
|
67
|
-
<style>${embedCss}</style>
|
|
68
|
-
${titleSvg}
|
|
69
|
-
${parts.join('\n')}
|
|
70
|
-
</svg>`;
|
|
104
|
+
return renderBlocksLayoutSvg(layout, { title: titleSvg, embedCssTheme: 'transitrix' });
|
|
71
105
|
}
|
|
@@ -1,4 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe SVG renderer for the canonical FGCA and FGA notations.
|
|
3
|
+
*
|
|
4
|
+
* Step 4 of the IntelliJ epic (ADR 0001): the webview bundle must turn a
|
|
5
|
+
* validated FGCA/FGA document into renderable SVG so JCEF can drop it into the
|
|
6
|
+
* preview panel. The VS Code path lives in `extension/src/fgca-preview.ts` and
|
|
7
|
+
* pulls in VS Code-specific concerns (themes, title block, save dialogs,
|
|
8
|
+
* spacing/scope controls); this module is the host-neutral subset — pure
|
|
9
|
+
* column layout → SVG with no VS Code APIs, no `node:fs`, no `node:path`.
|
|
10
|
+
*
|
|
11
|
+
* FGCA and FGA share the same renderer; FGA only differs by hiding the
|
|
12
|
+
* `change` column (`hideChanges` / the layout's Goal → Activity collapse),
|
|
13
|
+
* exactly as the extension preview does.
|
|
14
|
+
*/
|
|
15
|
+
import { layoutFGCAPreview } from '../fgca/preview-layout.js';
|
|
1
16
|
import type { FGCADoc } from '../fgca/validate.js';
|
|
17
|
+
type FgcaLayout = ReturnType<typeof layoutFGCAPreview>;
|
|
18
|
+
/**
|
|
19
|
+
* Canonical FGCA/FGA body — the column headers, node rects and edge paths that
|
|
20
|
+
* go inside the `<svg>`, shared verbatim by the VS Code preview (`buildSvg`)
|
|
21
|
+
* and the host-neutral wrapper below. Excludes the host-specific title block
|
|
22
|
+
* and the (identical) `<defs>` arrow marker. Coordinates are absolute (the
|
|
23
|
+
* layout's own); the VS Code path wraps this in a `translate(0, titleH)` group
|
|
24
|
+
* to make room for its title block, while the host-neutral wrapper renders it
|
|
25
|
+
* untranslated.
|
|
26
|
+
*/
|
|
27
|
+
export declare function renderFgcaBody(columns: FgcaLayout['columns'], nodes: FgcaLayout['nodes'], edges: FgcaLayout['edges'], curvature: number, entryCurvature: number | undefined): string;
|
|
2
28
|
export interface RenderFgcaOptions {
|
|
3
29
|
/** `'fga'` hides the Changes column; defaults to `'fgca'`. */
|
|
4
30
|
variant?: 'fgca' | 'fga';
|
|
@@ -10,3 +36,4 @@ export interface RenderFgcaOptions {
|
|
|
10
36
|
entryCurvature?: number;
|
|
11
37
|
}
|
|
12
38
|
export declare function renderFgcaSvg(doc: FGCADoc, options?: RenderFgcaOptions): string;
|
|
39
|
+
export {};
|
|
@@ -17,18 +17,21 @@ import { horizontalCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js
|
|
|
17
17
|
import { generateSvgEmbedCss } from '../theme/index.js';
|
|
18
18
|
import { escXml } from './render-util.js';
|
|
19
19
|
const COL_LABELS = {
|
|
20
|
-
|
|
20
|
+
driver: 'Drivers (D)',
|
|
21
21
|
goal: 'Goals (G)',
|
|
22
22
|
change: 'Changes (C)',
|
|
23
|
-
activity: '
|
|
23
|
+
activity: 'Actions (A)',
|
|
24
24
|
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Canonical FGCA/FGA body — the column headers, node rects and edge paths that
|
|
27
|
+
* go inside the `<svg>`, shared verbatim by the VS Code preview (`buildSvg`)
|
|
28
|
+
* and the host-neutral wrapper below. Excludes the host-specific title block
|
|
29
|
+
* and the (identical) `<defs>` arrow marker. Coordinates are absolute (the
|
|
30
|
+
* layout's own); the VS Code path wraps this in a `translate(0, titleH)` group
|
|
31
|
+
* to make room for its title block, while the host-neutral wrapper renders it
|
|
32
|
+
* untranslated.
|
|
33
|
+
*/
|
|
34
|
+
export function renderFgcaBody(columns, nodes, edges, curvature, entryCurvature) {
|
|
32
35
|
const headerSvg = columns
|
|
33
36
|
.map(({ col, x }) => [
|
|
34
37
|
`<rect class="diagram-node layer-${col}" x="${x}" y="${PAD}" width="${NODE_W}" height="${HEADER_H}" rx="6"/>`,
|
|
@@ -56,19 +59,32 @@ export function renderFgcaSvg(doc, options = {}) {
|
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
const twoLines = line2.length > 0;
|
|
59
|
-
const y1 = twoLines ? n.y +
|
|
60
|
-
const y2 =
|
|
62
|
+
const y1 = twoLines ? n.y + 16 : n.y + 26;
|
|
63
|
+
const y2 = n.y + 32;
|
|
64
|
+
const idY = twoLines ? n.y + 54 : n.y + 50;
|
|
65
|
+
const entityId = n.id.slice(n.id.indexOf('_') + 1);
|
|
61
66
|
return [
|
|
62
67
|
`<rect class="diagram-node layer-${n.col}" x="${n.x}" y="${n.y}" width="${NODE_W}" height="${NODE_H}" rx="8"/>`,
|
|
63
|
-
`<text class="text-primary" x="${n.x + NODE_W / 2}" y="${y1}" text-anchor="middle">${escXml(line1)}</text>`,
|
|
68
|
+
`<text class="text-primary" x="${n.x + NODE_W / 2}" y="${y1}" text-anchor="middle" dominant-baseline="central">${escXml(line1)}</text>`,
|
|
64
69
|
twoLines
|
|
65
|
-
? `<text class="text-secondary" x="${n.x + NODE_W / 2}" y="${y2}" text-anchor="middle">${escXml(line2)}</text>`
|
|
70
|
+
? `<text class="text-secondary" x="${n.x + NODE_W / 2}" y="${y2}" text-anchor="middle" dominant-baseline="central">${escXml(line2)}</text>`
|
|
66
71
|
: '',
|
|
72
|
+
`<text class="text-id" x="${n.x + NODE_W / 2}" y="${idY}" text-anchor="middle" dominant-baseline="central">${escXml(entityId)}</text>`,
|
|
67
73
|
]
|
|
68
74
|
.filter(Boolean)
|
|
69
75
|
.join('\n');
|
|
70
76
|
})
|
|
71
77
|
.join('\n');
|
|
78
|
+
return `${headerSvg}\n${nodeSvg}\n${edgeSvg}`;
|
|
79
|
+
}
|
|
80
|
+
export function renderFgcaSvg(doc, options = {}) {
|
|
81
|
+
const { variant = 'fgca', title = '', curvature = DEFAULT_EDGE_CURVATURE, entryCurvature } = options;
|
|
82
|
+
const hideChanges = variant === 'fga';
|
|
83
|
+
const { nodes, edges, columns, width, height } = layoutFGCAPreview(doc, { hideChanges });
|
|
84
|
+
if (nodes.length === 0) {
|
|
85
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
|
|
86
|
+
}
|
|
87
|
+
const body = renderFgcaBody(columns, nodes, edges, curvature, entryCurvature);
|
|
72
88
|
const titleSvg = title
|
|
73
89
|
? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(title)}</text>`
|
|
74
90
|
: '';
|
|
@@ -84,8 +100,6 @@ export function renderFgcaSvg(doc, options = {}) {
|
|
|
84
100
|
</marker>
|
|
85
101
|
</defs>
|
|
86
102
|
${titleSvg}
|
|
87
|
-
${
|
|
88
|
-
${nodeSvg}
|
|
89
|
-
${edgeSvg}
|
|
103
|
+
${body}
|
|
90
104
|
</svg>`;
|
|
91
105
|
}
|
|
@@ -1,7 +1,32 @@
|
|
|
1
|
-
import type { GoalTree } from '../goals/types.js';
|
|
1
|
+
import type { GoalTree, GoalTreeLayout } from '../goals/types.js';
|
|
2
|
+
import { type ThemeId } from '../theme/index.js';
|
|
2
3
|
export interface RenderGoalsOptions {
|
|
3
4
|
treeName?: string;
|
|
4
5
|
curvature?: number;
|
|
5
6
|
entryCurvature?: number;
|
|
6
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Host-neutral goals renderer (IntelliJ/UI). Lays the tree out with the default
|
|
10
|
+
* spacing, then delegates the actual SVG emission to {@link renderGoalsLayoutSvg}
|
|
11
|
+
* with the shared theme CSS embedded so the output is self-contained.
|
|
12
|
+
*/
|
|
7
13
|
export declare function renderGoalsSvg(tree: GoalTree, options?: RenderGoalsOptions): string;
|
|
14
|
+
export interface RenderGoalsLayoutOptions {
|
|
15
|
+
curvature?: number;
|
|
16
|
+
entryCurvature?: number;
|
|
17
|
+
/** Extra vertical space reserved at the top of the canvas (e.g. for a title block). */
|
|
18
|
+
topInset?: number;
|
|
19
|
+
/** Raw SVG injected immediately after `<defs>` — a header line or a full title block. */
|
|
20
|
+
title?: string;
|
|
21
|
+
/** When set, the theme CSS is embedded as `<style>` so the SVG is self-contained. */
|
|
22
|
+
embedCssTheme?: ThemeId;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The single goals SVG emitter shared by every host. Takes an already-computed
|
|
26
|
+
* {@link GoalTreeLayout} (callers decide spacing/scope) and produces the `<svg>`.
|
|
27
|
+
* Hosts wrap it with their own chrome:
|
|
28
|
+
* - IntelliJ/UI via {@link renderGoalsSvg} (embedded CSS + simple header);
|
|
29
|
+
* - VS Code's goals preview (rich title block, no embedded CSS — the webview
|
|
30
|
+
* and the export path own styling).
|
|
31
|
+
*/
|
|
32
|
+
export declare function renderGoalsLayoutSvg(layout: GoalTreeLayout, options?: RenderGoalsLayoutOptions): string;
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { layoutGoalTree } from '../goals/layout.js';
|
|
14
14
|
import { horizontalCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
|
|
15
15
|
import { generateSvgEmbedCss } from '../theme/index.js';
|
|
16
|
+
import { escXml } from './render-util.js';
|
|
16
17
|
const NODE_W = 250;
|
|
17
18
|
const NODE_H = 60;
|
|
18
19
|
const RANK_SEP = 100;
|
|
@@ -20,13 +21,11 @@ const NODE_SEP = 24;
|
|
|
20
21
|
const PAD = 24;
|
|
21
22
|
const LABEL_MAX = 36;
|
|
22
23
|
const LABEL_TRIM = 34;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.replace(/"/g, '"');
|
|
29
|
-
}
|
|
24
|
+
/**
|
|
25
|
+
* Host-neutral goals renderer (IntelliJ/UI). Lays the tree out with the default
|
|
26
|
+
* spacing, then delegates the actual SVG emission to {@link renderGoalsLayoutSvg}
|
|
27
|
+
* with the shared theme CSS embedded so the output is self-contained.
|
|
28
|
+
*/
|
|
30
29
|
export function renderGoalsSvg(tree, options = {}) {
|
|
31
30
|
const { treeName = '', curvature = DEFAULT_EDGE_CURVATURE, entryCurvature } = options;
|
|
32
31
|
const layout = layoutGoalTree(tree, {
|
|
@@ -38,10 +37,29 @@ export function renderGoalsSvg(tree, options = {}) {
|
|
|
38
37
|
if (layout.nodes.length === 0) {
|
|
39
38
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
|
|
40
39
|
}
|
|
40
|
+
const title = treeName
|
|
41
|
+
? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(`Goal tree — ${treeName}`)}</text>`
|
|
42
|
+
: '';
|
|
43
|
+
// Embed the shared theme CSS so the JCEF host page only needs to drop the SVG
|
|
44
|
+
// into the DOM — styling resolves without help from the host stylesheet. The
|
|
45
|
+
// VS Code preview omits this (its webview supplies the CSS) and embeds it only
|
|
46
|
+
// on export via `prepareSvgForExport`.
|
|
47
|
+
return renderGoalsLayoutSvg(layout, { curvature, entryCurvature, title, embedCssTheme: 'transitrix' });
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The single goals SVG emitter shared by every host. Takes an already-computed
|
|
51
|
+
* {@link GoalTreeLayout} (callers decide spacing/scope) and produces the `<svg>`.
|
|
52
|
+
* Hosts wrap it with their own chrome:
|
|
53
|
+
* - IntelliJ/UI via {@link renderGoalsSvg} (embedded CSS + simple header);
|
|
54
|
+
* - VS Code's goals preview (rich title block, no embedded CSS — the webview
|
|
55
|
+
* and the export path own styling).
|
|
56
|
+
*/
|
|
57
|
+
export function renderGoalsLayoutSvg(layout, options = {}) {
|
|
58
|
+
const { curvature = DEFAULT_EDGE_CURVATURE, entryCurvature, topInset = 0, title = '', embedCssTheme } = options;
|
|
41
59
|
const w = layout.bounds.width + PAD * 2;
|
|
42
|
-
const h = layout.bounds.height + PAD * 2;
|
|
60
|
+
const h = layout.bounds.height + PAD * 2 + topInset;
|
|
43
61
|
const ox = -layout.bounds.x + PAD;
|
|
44
|
-
const oy = -layout.bounds.y + PAD;
|
|
62
|
+
const oy = -layout.bounds.y + PAD + topInset;
|
|
45
63
|
const nodeMap = new Map(layout.nodes.map((n) => [n.id, n]));
|
|
46
64
|
function edgePath(e) {
|
|
47
65
|
const s = nodeMap.get(e.source);
|
|
@@ -70,22 +88,14 @@ export function renderGoalsSvg(tree, options = {}) {
|
|
|
70
88
|
</g>`;
|
|
71
89
|
})
|
|
72
90
|
.join('\n');
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
: '';
|
|
76
|
-
// Embed the shared theme CSS inside the SVG so the rendered output is
|
|
77
|
-
// self-contained — the JCEF host page only needs to drop the SVG into the
|
|
78
|
-
// DOM and styling resolves without any cooperation from the host stylesheet.
|
|
79
|
-
// Matches what the VS Code path produces via `prepareSvgForExport`.
|
|
80
|
-
const embedCss = generateSvgEmbedCss('transitrix');
|
|
81
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
|
|
82
|
-
<style>${embedCss}</style>
|
|
91
|
+
const styleLine = embedCssTheme ? `\n<style>${generateSvgEmbedCss(embedCssTheme)}</style>` : '';
|
|
92
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">${styleLine}
|
|
83
93
|
<defs>
|
|
84
94
|
<marker id="arrow" markerWidth="8" markerHeight="8" refX="8" refY="3" orient="auto">
|
|
85
95
|
<path d="M0,0 L0,6 L8,3 z" class="arrow-fill"/>
|
|
86
96
|
</marker>
|
|
87
97
|
</defs>
|
|
88
|
-
${
|
|
98
|
+
${title}
|
|
89
99
|
${nodeSvg}
|
|
90
100
|
${edgeSvg}
|
|
91
101
|
</svg>`;
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
import type { ProcessBlueprintFile } from '../process-blueprint/types.js';
|
|
1
|
+
import type { ProcessBlueprintFile, ProcessBlueprintLayout } from '../process-blueprint/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Canonical Process Blueprint body — everything inside the `<svg>` except the
|
|
4
|
+
* host-specific title block. Shared verbatim by the VS Code preview and the
|
|
5
|
+
* host-neutral wrapper below so both surfaces stay pixel-identical.
|
|
6
|
+
*
|
|
7
|
+
* Layering: a clipped group holds the row fills and grid lines; pills and
|
|
8
|
+
* compliance chips render on top (so grid lines sit under pill content); the
|
|
9
|
+
* outer rounded border is drawn last so corner cells never overflow it.
|
|
10
|
+
*/
|
|
11
|
+
export declare function renderProcessBlueprintBody(layout: ProcessBlueprintLayout, ox: number, oy: number, clipId?: string): string;
|
|
2
12
|
export interface RenderProcessBlueprintOptions {
|
|
3
13
|
title?: string;
|
|
4
14
|
}
|