@transitrix/diagrams 1.6.0 → 1.8.1
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/layout.js +7 -7
- package/dist/activities/types.d.ts +4 -0
- package/dist/capability-map/dsm-schema.d.ts +1 -1
- package/dist/capability-map/dsm-schema.js +1 -1
- package/dist/capability-map/layout-tree.d.ts +5 -1
- package/dist/capability-map/layout-tree.js +11 -9
- package/dist/capability-map/render-capability-tree.d.ts +2 -9
- package/dist/capability-map/render-capability-tree.js +16 -28
- package/dist/codex/__tests__/example.test.d.ts +1 -0
- package/dist/codex/__tests__/example.test.js +33 -0
- package/dist/codex/__tests__/validate.test.d.ts +1 -0
- package/dist/codex/__tests__/validate.test.js +68 -0
- package/dist/codex/index.d.ts +4 -0
- package/dist/codex/index.js +2 -0
- package/dist/codex/types.d.ts +7 -0
- package/dist/codex/types.js +8 -0
- package/dist/codex/validate.d.ts +11 -0
- package/dist/codex/validate.js +103 -0
- package/dist/compliance/__tests__/canon-catalog.test.d.ts +1 -0
- package/dist/compliance/__tests__/canon-catalog.test.js +60 -0
- package/dist/compliance/__tests__/view-resolution.test.d.ts +1 -0
- package/dist/compliance/__tests__/view-resolution.test.js +59 -0
- package/dist/compliance/canon-catalog.d.ts +19 -0
- package/dist/compliance/canon-catalog.js +59 -0
- package/dist/compliance/coverage-metric.d.ts +12 -0
- package/dist/compliance/coverage-metric.js +35 -0
- package/dist/compliance/impact.d.ts +7 -1
- package/dist/compliance/impact.js +53 -0
- package/dist/compliance/index.d.ts +6 -4
- package/dist/compliance/index.js +3 -2
- package/dist/constraint/__tests__/example.test.d.ts +1 -0
- package/dist/constraint/__tests__/example.test.js +14 -0
- package/dist/constraint/__tests__/validate.test.d.ts +1 -0
- package/dist/constraint/__tests__/validate.test.js +58 -0
- package/dist/constraint/index.d.ts +4 -0
- package/dist/constraint/index.js +2 -0
- package/dist/constraint/types.d.ts +4 -0
- package/dist/constraint/types.js +2 -0
- package/dist/constraint/validate.d.ts +7 -0
- package/dist/constraint/validate.js +115 -0
- package/dist/fgca/__tests__/parse-canonical.test.js +31 -0
- package/dist/fgca/parse-canonical.d.ts +1 -1
- package/dist/fgca/parse-canonical.js +18 -4
- package/dist/fgca/preview-layout.d.ts +8 -4
- package/dist/fgca/preview-layout.js +13 -13
- package/dist/goals/GoalNode.d.ts +2 -1
- package/dist/goals/GoalNode.js +7 -2
- package/dist/goals/GoalTreeView.d.ts +3 -1
- package/dist/goals/GoalTreeView.js +31 -14
- package/dist/goals/__tests__/GoalTreeView.test.js +41 -0
- package/dist/goals/__tests__/mutations.test.js +39 -0
- package/dist/goals/__tests__/parse-canonical.test.js +2 -0
- package/dist/goals/layout.js +2 -1
- package/dist/goals/mutations.js +6 -6
- package/dist/goals/parse-canonical.d.ts +5 -0
- package/dist/goals/parse-canonical.js +5 -0
- package/dist/goals/types.d.ts +2 -0
- package/dist/node-size-presets.d.ts +50 -0
- package/dist/node-size-presets.js +103 -0
- package/dist/process-blueprint/layout.js +4 -47
- package/dist/repo-validate/types.js +2 -3
- package/dist/webview/__tests__/entity-text-layout.test.d.ts +1 -0
- package/dist/webview/__tests__/entity-text-layout.test.js +43 -0
- package/dist/webview/__tests__/entry.test.js +3 -3
- package/dist/webview/__tests__/render-blocks.test.js +1 -7
- package/dist/webview/__tests__/render-goals.test.js +17 -3
- package/dist/webview/entity-text-layout.d.ts +95 -0
- package/dist/webview/entity-text-layout.js +279 -0
- package/dist/webview/entry.js +7 -3
- package/dist/webview/notation-style.d.ts +1 -1
- package/dist/webview/notation-style.js +1 -1
- package/dist/webview/render-activities.d.ts +3 -1
- package/dist/webview/render-activities.js +29 -32
- package/dist/webview/render-blocks.d.ts +4 -1
- package/dist/webview/render-blocks.js +27 -108
- package/dist/webview/render-fgca.d.ts +15 -8
- package/dist/webview/render-fgca.js +41 -44
- package/dist/webview/render-goals.d.ts +2 -0
- package/dist/webview/render-goals.js +23 -37
- package/dist/webview/render-process-blueprint.d.ts +4 -10
- package/dist/webview/render-process-blueprint.js +18 -64
- package/package.json +1 -1
- package/src/activities/layout.ts +12 -7
- package/src/activities/types.ts +4 -0
- package/src/capability-map/dsm-schema.ts +1 -1
- package/src/capability-map/index.ts +1 -1
- package/src/capability-map/layout-tree.ts +16 -8
- package/src/capability-map/render-capability-tree.ts +18 -30
- package/src/codex/__tests__/example.test.ts +40 -0
- package/src/codex/__tests__/validate.test.ts +84 -0
- package/src/codex/index.ts +4 -0
- package/src/codex/types.ts +13 -0
- package/src/codex/validate.ts +125 -0
- package/src/compliance/__tests__/canon-catalog.test.ts +68 -0
- package/src/compliance/__tests__/view-resolution.test.ts +80 -0
- package/src/compliance/canon-catalog.ts +76 -0
- package/src/compliance/coverage-metric.ts +50 -0
- package/src/compliance/impact.ts +67 -1
- package/src/compliance/index.ts +10 -1
- package/src/constraint/__tests__/example.test.ts +19 -0
- package/src/constraint/__tests__/validate.test.ts +68 -0
- package/src/constraint/index.ts +4 -0
- package/src/constraint/types.ts +5 -0
- package/src/constraint/validate.ts +131 -0
- package/src/fgca/__tests__/parse-canonical.test.ts +34 -0
- package/src/fgca/parse-canonical.ts +17 -4
- package/src/fgca/preview-layout.ts +21 -15
- package/src/goals/GoalNode.tsx +8 -1
- package/src/goals/GoalTreeView.tsx +61 -27
- package/src/goals/__tests__/GoalTreeView.test.tsx +48 -0
- package/src/goals/__tests__/mutations.test.ts +42 -0
- package/src/goals/__tests__/parse-canonical.test.ts +2 -0
- package/src/goals/layout.ts +2 -1
- package/src/goals/mutations.ts +6 -6
- package/src/goals/parse-canonical.ts +6 -0
- package/src/goals/types.ts +2 -0
- package/src/node-size-presets.ts +157 -0
- package/src/process-blueprint/layout.ts +4 -48
- package/src/repo-validate/types.ts +2 -3
- package/src/webview/__tests__/entity-text-layout.test.ts +52 -0
- package/src/webview/__tests__/entry.test.ts +3 -3
- package/src/webview/__tests__/render-blocks.test.ts +1 -9
- package/src/webview/__tests__/render-goals.test.ts +17 -3
- package/src/webview/entity-text-layout.ts +355 -0
- package/src/webview/entry.ts +8 -4
- package/src/webview/notation-style.ts +1 -1
- package/src/webview/render-activities.ts +38 -27
- package/src/webview/render-blocks.ts +35 -118
- package/src/webview/render-fgca.ts +59 -44
- package/src/webview/render-goals.ts +24 -35
- package/src/webview/render-process-blueprint.ts +30 -65
|
@@ -13,67 +13,23 @@
|
|
|
13
13
|
* title `<text>`.
|
|
14
14
|
*/
|
|
15
15
|
import { layoutNestedBlocks } from '../blocks/layout.js';
|
|
16
|
-
import type { BlocksFile, BlocksLayout, LaidOutBlock } from '../blocks/types.js';
|
|
16
|
+
import type { BlocksFile, BlocksLayout, BlocksLayoutOptions, LaidOutBlock } from '../blocks/types.js';
|
|
17
|
+
import { parseNodeSizePreset, resolveBlocksLeafSize, type NodeSizePreset } from '../node-size-presets.js';
|
|
17
18
|
import { generateSvgEmbedCss, type ThemeId } from '../theme/index.js';
|
|
19
|
+
import {
|
|
20
|
+
emitCenteredTextSvg,
|
|
21
|
+
layoutHeaderBlockText,
|
|
22
|
+
layoutLeafBlockText,
|
|
23
|
+
} from './entity-text-layout.js';
|
|
18
24
|
import { escXml } from './render-util.js';
|
|
19
25
|
import { ENTITY_NODE_RX } from './notation-style.js';
|
|
20
26
|
|
|
21
27
|
const PAD = 24;
|
|
22
28
|
|
|
23
|
-
// Approximate character width (px) at text-primary size (12px/600).
|
|
24
|
-
const CHAR_W = 7;
|
|
25
|
-
// Approximate character width (px) at text-id size (10px/600).
|
|
26
|
-
const CHAR_W_ID = 6;
|
|
27
|
-
|
|
28
|
-
// Vertical spacing between line centres.
|
|
29
|
-
const LINE_H = 14; // name lines (text-primary, 12 px font)
|
|
30
|
-
const LINE_H_ID = 12; // id lines (text-id, 10 px font)
|
|
31
|
-
// Minimum gap to avoid overlap: half of name text height (6 px) + half of id
|
|
32
|
-
// text height (5 px) = 11 px. Use 14 px to match the name-line separation and
|
|
33
|
-
// leave a 3 px visual buffer — same as the gap between consecutive name lines.
|
|
34
|
-
const NAME_ID_GAP = 14;
|
|
35
|
-
|
|
36
|
-
/** Word-wrap `text` to at most `maxLines` lines of `maxChars` each. */
|
|
37
|
-
function wrapWords(text: string, maxChars: number, maxLines: number): string[] {
|
|
38
|
-
const safeMax = Math.max(2, maxChars);
|
|
39
|
-
const words = text.split(' ');
|
|
40
|
-
const lines: string[] = [];
|
|
41
|
-
let cur = '';
|
|
42
|
-
for (let i = 0; i < words.length; i++) {
|
|
43
|
-
const w = words[i];
|
|
44
|
-
const next = cur ? `${cur} ${w}` : w;
|
|
45
|
-
if (next.length <= safeMax) {
|
|
46
|
-
cur = next;
|
|
47
|
-
} else {
|
|
48
|
-
if (cur) lines.push(cur);
|
|
49
|
-
cur = '';
|
|
50
|
-
if (lines.length >= maxLines) break;
|
|
51
|
-
if (lines.length === maxLines - 1) {
|
|
52
|
-
const rest = words.slice(i).join(' ');
|
|
53
|
-
lines.push(rest.length <= safeMax ? rest : rest.slice(0, safeMax - 1) + '…');
|
|
54
|
-
return lines;
|
|
55
|
-
}
|
|
56
|
-
cur = w.length <= safeMax ? w : w.slice(0, safeMax - 1) + '…';
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
if (cur && lines.length < maxLines) lines.push(cur);
|
|
60
|
-
return lines.length ? lines : [text.slice(0, safeMax - 1) + '…'];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Split an ID (no spaces) across at most 2 lines, breaking at `_` or `-`. */
|
|
64
|
-
function wrapId(id: string, maxChars: number): string[] {
|
|
65
|
-
if (id.length <= maxChars) return [id];
|
|
66
|
-
const seg = id.slice(0, maxChars);
|
|
67
|
-
const sepIdx = Math.max(seg.lastIndexOf('_'), seg.lastIndexOf('-'));
|
|
68
|
-
const cut = sepIdx > Math.floor(maxChars / 3) ? sepIdx : maxChars - 1;
|
|
69
|
-
const isSep = id[cut] === '_' || id[cut] === '-';
|
|
70
|
-
const line1 = id.slice(0, cut);
|
|
71
|
-
const rest = id.slice(cut + (isSep ? 1 : 0));
|
|
72
|
-
return [line1, rest.length <= maxChars ? rest : rest.slice(0, maxChars - 1) + '…'];
|
|
73
|
-
}
|
|
74
|
-
|
|
75
29
|
export interface RenderBlocksOptions {
|
|
76
30
|
title?: string;
|
|
31
|
+
nodeSizePreset?: NodeSizePreset;
|
|
32
|
+
layoutOptions?: BlocksLayoutOptions;
|
|
77
33
|
}
|
|
78
34
|
|
|
79
35
|
/**
|
|
@@ -89,27 +45,6 @@ function levelClassForDepth(depth: number): string {
|
|
|
89
45
|
return `level-${idx}`;
|
|
90
46
|
}
|
|
91
47
|
|
|
92
|
-
// Reserve a small horizontal margin so text never abuts the rounded corners.
|
|
93
|
-
const TEXT_MARGIN_X = 8;
|
|
94
|
-
|
|
95
|
-
/** Truncate `text` to at most `maxChars` characters, at a word boundary when possible. */
|
|
96
|
-
function truncateLine(text: string, maxChars: number): string {
|
|
97
|
-
const safeMax = Math.max(2, maxChars);
|
|
98
|
-
if (text.length <= safeMax) return text;
|
|
99
|
-
const cut = text.slice(0, safeMax - 1);
|
|
100
|
-
const lastSpace = cut.lastIndexOf(' ');
|
|
101
|
-
return (lastSpace > safeMax / 2 ? cut.slice(0, lastSpace) : cut) + '…';
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Single-line ID truncation that prefers `_`/`-` separators before falling back to a hard cut. */
|
|
105
|
-
function truncateId(id: string, maxChars: number): string {
|
|
106
|
-
const safeMax = Math.max(2, maxChars);
|
|
107
|
-
if (id.length <= safeMax) return id;
|
|
108
|
-
const cut = id.slice(0, safeMax - 1);
|
|
109
|
-
const sepIdx = Math.max(cut.lastIndexOf('_'), cut.lastIndexOf('-'));
|
|
110
|
-
return (sepIdx > safeMax / 3 ? cut.slice(0, sepIdx) : cut) + '…';
|
|
111
|
-
}
|
|
112
|
-
|
|
113
48
|
function emitBlockSvg(b: LaidOutBlock, ox: number, oy: number, parts: string[]): void {
|
|
114
49
|
const cls = levelClassForDepth(b.depth);
|
|
115
50
|
const cx = b.x + ox + b.width / 2;
|
|
@@ -117,50 +52,27 @@ function emitBlockSvg(b: LaidOutBlock, ox: number, oy: number, parts: string[]):
|
|
|
117
52
|
`<rect class="diagram-node ${cls}" x="${b.x + ox}" y="${b.y + oy}" width="${b.width}" height="${b.height}" rx="${ENTITY_NODE_RX}"/>`,
|
|
118
53
|
);
|
|
119
54
|
|
|
120
|
-
const innerW = Math.max(0, b.width - TEXT_MARGIN_X * 2);
|
|
121
55
|
const isLeaf = b.children.length === 0;
|
|
122
56
|
if (isLeaf) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
for (let i = 0; i < nameLines.length; i++) {
|
|
133
|
-
parts.push(
|
|
134
|
-
`<text class="text-primary" x="${cx}" y="${firstY + i * LINE_H}" text-anchor="middle" dominant-baseline="central">${escXml(nameLines[i])}</text>`,
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
const idFirstY = firstY + nameTotalSpan + NAME_ID_GAP;
|
|
138
|
-
for (let i = 0; i < idLines.length; i++) {
|
|
139
|
-
parts.push(
|
|
140
|
-
`<text class="text-id" x="${cx}" y="${idFirstY + i * LINE_H_ID}" text-anchor="middle" dominant-baseline="central">${escXml(idLines[i])}</text>`,
|
|
141
|
-
);
|
|
142
|
-
}
|
|
57
|
+
const specs = layoutLeafBlockText({
|
|
58
|
+
boxX: b.x + ox,
|
|
59
|
+
boxY: b.y + oy,
|
|
60
|
+
boxWidth: b.width,
|
|
61
|
+
boxHeight: b.height,
|
|
62
|
+
name: b.name,
|
|
63
|
+
id: b.id,
|
|
64
|
+
});
|
|
65
|
+
parts.push(emitCenteredTextSvg(specs, cx, escXml));
|
|
143
66
|
} else {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const idText = truncateId(b.id, maxCharsId);
|
|
154
|
-
const headerMid = b.y + oy + b.headerHeight / 2;
|
|
155
|
-
const halfSpan = (LINE_H + LINE_H_ID) / 4;
|
|
156
|
-
const nameY = Math.round(headerMid - halfSpan);
|
|
157
|
-
const idY = Math.round(headerMid + halfSpan);
|
|
158
|
-
parts.push(
|
|
159
|
-
`<text class="text-primary" x="${cx}" y="${nameY}" text-anchor="middle" dominant-baseline="central">${escXml(nameText)}</text>`,
|
|
160
|
-
);
|
|
161
|
-
parts.push(
|
|
162
|
-
`<text class="text-id" x="${cx}" y="${idY}" text-anchor="middle" dominant-baseline="central">${escXml(idText)}</text>`,
|
|
163
|
-
);
|
|
67
|
+
const specs = layoutHeaderBlockText({
|
|
68
|
+
boxX: b.x + ox,
|
|
69
|
+
boxY: b.y + oy,
|
|
70
|
+
boxWidth: b.width,
|
|
71
|
+
headerHeight: b.headerHeight,
|
|
72
|
+
name: b.name,
|
|
73
|
+
id: b.id,
|
|
74
|
+
});
|
|
75
|
+
parts.push(emitCenteredTextSvg(specs, cx, escXml));
|
|
164
76
|
for (const c of b.children) emitBlockSvg(c, ox, oy, parts);
|
|
165
77
|
}
|
|
166
78
|
}
|
|
@@ -210,9 +122,14 @@ ${parts.join('\n')}
|
|
|
210
122
|
* with the shared theme CSS embedded so the output is self-contained.
|
|
211
123
|
*/
|
|
212
124
|
export function renderBlocksSvg(doc: BlocksFile, options: RenderBlocksOptions = {}): string {
|
|
213
|
-
const { title = '' } = options;
|
|
214
|
-
|
|
215
|
-
|
|
125
|
+
const { title = '', nodeSizePreset = 'normal', layoutOptions } = options;
|
|
126
|
+
const leaf = resolveBlocksLeafSize(parseNodeSizePreset(nodeSizePreset));
|
|
127
|
+
|
|
128
|
+
const layout: BlocksLayout = layoutNestedBlocks(doc, {
|
|
129
|
+
leafWidth: leaf.width,
|
|
130
|
+
leafHeight: leaf.height,
|
|
131
|
+
...layoutOptions,
|
|
132
|
+
});
|
|
216
133
|
|
|
217
134
|
if (layout.blocks.length === 0) {
|
|
218
135
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
|
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Browser-safe SVG renderer for the canonical
|
|
2
|
+
* Browser-safe SVG renderer for the canonical DGCA and DGA notations.
|
|
3
3
|
*
|
|
4
4
|
* Step 4 of the IntelliJ epic (ADR 0001): the webview bundle must turn a
|
|
5
|
-
* validated
|
|
6
|
-
* preview panel. The VS Code path lives in `extension/src/
|
|
5
|
+
* validated DGCA/DGA document into renderable SVG so JCEF can drop it into the
|
|
6
|
+
* preview panel. The VS Code path lives in `extension/src/dgca-preview.ts` and
|
|
7
7
|
* pulls in VS Code-specific concerns (themes, title block, save dialogs,
|
|
8
8
|
* spacing/scope controls); this module is the host-neutral subset — pure
|
|
9
9
|
* column layout → SVG with no VS Code APIs, no `node:fs`, no `node:path`.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
11
|
+
* DGCA and DGA share the same renderer; DGA only differs by hiding the
|
|
12
12
|
* `change` column (`hideChanges` / the layout's Goal → Activity collapse),
|
|
13
13
|
* exactly as the extension preview does.
|
|
14
14
|
*/
|
|
15
15
|
import {
|
|
16
16
|
layoutFGCAPreview,
|
|
17
|
-
FGCA_NODE_W
|
|
18
|
-
FGCA_NODE_H
|
|
17
|
+
FGCA_NODE_W,
|
|
18
|
+
FGCA_NODE_H,
|
|
19
19
|
FGCA_HEADER_H as HEADER_H,
|
|
20
20
|
FGCA_PAD as PAD,
|
|
21
21
|
type FGCAPreviewColumn,
|
|
22
22
|
} from '../fgca/preview-layout.js';
|
|
23
23
|
import type { FGCADoc } from '../fgca/validate.js';
|
|
24
24
|
import { horizontalCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
|
|
25
|
+
import { parseNodeSizePreset, resolveDgcaNodeSize, type NodeSizePreset } from '../node-size-presets.js';
|
|
25
26
|
import { generateSvgEmbedCss } from '../theme/index.js';
|
|
27
|
+
import { emitCenteredTextSvg, layoutCenteredEntityText, truncateLine } from './entity-text-layout.js';
|
|
26
28
|
import { escXml } from './render-util.js';
|
|
27
29
|
|
|
28
30
|
const COL_LABELS: Record<FGCAPreviewColumn, string> = {
|
|
@@ -34,8 +36,13 @@ const COL_LABELS: Record<FGCAPreviewColumn, string> = {
|
|
|
34
36
|
|
|
35
37
|
type FgcaLayout = ReturnType<typeof layoutFGCAPreview>;
|
|
36
38
|
|
|
39
|
+
export interface RenderFgcaBodyOptions {
|
|
40
|
+
nodeWidth?: number;
|
|
41
|
+
nodeHeight?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
37
44
|
/**
|
|
38
|
-
* Canonical
|
|
45
|
+
* Canonical DGCA/DGA body — the column headers, node rects and edge paths that
|
|
39
46
|
* go inside the `<svg>`, shared verbatim by the VS Code preview (`buildSvg`)
|
|
40
47
|
* and the host-neutral wrapper below. Excludes the host-specific title block
|
|
41
48
|
* and the (identical) `<defs>` arrow marker. Coordinates are absolute (the
|
|
@@ -49,12 +56,17 @@ export function renderFgcaBody(
|
|
|
49
56
|
edges: FgcaLayout['edges'],
|
|
50
57
|
curvature: number,
|
|
51
58
|
entryCurvature: number | undefined,
|
|
59
|
+
bodyOptions: RenderFgcaBodyOptions = {},
|
|
52
60
|
): string {
|
|
61
|
+
const nodeWidth = bodyOptions.nodeWidth ?? FGCA_NODE_W;
|
|
62
|
+
const nodeHeight = bodyOptions.nodeHeight ?? FGCA_NODE_H;
|
|
63
|
+
const headerTruncate = Math.max(8, Math.floor((nodeWidth - 16) / 7));
|
|
64
|
+
|
|
53
65
|
const headerSvg = columns
|
|
54
66
|
.map(({ col, x }) =>
|
|
55
67
|
[
|
|
56
|
-
`<rect class="diagram-node layer-${col}" x="${x}" y="${PAD}" width="${
|
|
57
|
-
`<text class="text-header" x="${x +
|
|
68
|
+
`<rect class="diagram-node layer-${col}" x="${x}" y="${PAD}" width="${nodeWidth}" height="${HEADER_H}" rx="6"/>`,
|
|
69
|
+
`<text class="text-header" x="${x + nodeWidth / 2}" y="${PAD + HEADER_H / 2}" text-anchor="middle" dominant-baseline="central">${escXml(truncateLine(COL_LABELS[col], headerTruncate))}</text>`,
|
|
58
70
|
].join('\n'),
|
|
59
71
|
)
|
|
60
72
|
.join('\n');
|
|
@@ -68,34 +80,22 @@ export function renderFgcaBody(
|
|
|
68
80
|
|
|
69
81
|
const nodeSvg = nodes
|
|
70
82
|
.map((n) => {
|
|
71
|
-
const words = n.label.split(' ');
|
|
72
|
-
let line1 = '';
|
|
73
|
-
let line2 = '';
|
|
74
|
-
for (const w of words) {
|
|
75
|
-
if ((line1 + ' ' + w).trim().length <= 26) {
|
|
76
|
-
line1 = (line1 + ' ' + w).trim();
|
|
77
|
-
} else if ((line2 + ' ' + w).trim().length <= 26) {
|
|
78
|
-
line2 = (line2 + ' ' + w).trim();
|
|
79
|
-
} else if (!line2) {
|
|
80
|
-
line2 = w.slice(0, 24) + '…';
|
|
81
|
-
break;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
const twoLines = line2.length > 0;
|
|
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
83
|
const entityId = n.id.slice(n.id.indexOf('_') + 1);
|
|
84
|
+
const specs = layoutCenteredEntityText({
|
|
85
|
+
boxX: n.x,
|
|
86
|
+
boxY: n.y,
|
|
87
|
+
boxWidth: nodeWidth,
|
|
88
|
+
boxHeight: nodeHeight,
|
|
89
|
+
name: n.label,
|
|
90
|
+
id: entityId,
|
|
91
|
+
nameMaxLines: 2,
|
|
92
|
+
idMaxLines: 1,
|
|
93
|
+
});
|
|
94
|
+
const textSvg = emitCenteredTextSvg(specs, n.x + nodeWidth / 2, escXml);
|
|
89
95
|
return [
|
|
90
|
-
`<rect class="diagram-node layer-${n.col}" x="${n.x}" y="${n.y}" width="${
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
? `<text class="text-primary" x="${n.x + NODE_W / 2}" y="${y2}" text-anchor="middle" dominant-baseline="central">${escXml(line2)}</text>`
|
|
94
|
-
: '',
|
|
95
|
-
`<text class="text-id" x="${n.x + NODE_W / 2}" y="${idY}" text-anchor="middle" dominant-baseline="central">${escXml(entityId)}</text>`,
|
|
96
|
-
]
|
|
97
|
-
.filter(Boolean)
|
|
98
|
-
.join('\n');
|
|
96
|
+
`<rect class="diagram-node layer-${n.col}" x="${n.x}" y="${n.y}" width="${nodeWidth}" height="${nodeHeight}" rx="8"/>`,
|
|
97
|
+
textSvg,
|
|
98
|
+
].join('\n');
|
|
99
99
|
})
|
|
100
100
|
.join('\n');
|
|
101
101
|
|
|
@@ -103,35 +103,50 @@ export function renderFgcaBody(
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
export interface RenderFgcaOptions {
|
|
106
|
-
/** `'
|
|
107
|
-
variant?: '
|
|
106
|
+
/** `'dga'` hides the Changes column; defaults to `'dgca'`. */
|
|
107
|
+
variant?: 'dgca' | 'dga';
|
|
108
108
|
/** Optional heading rendered as a left-anchored `text-header` line. */
|
|
109
109
|
title?: string;
|
|
110
110
|
/** Exit edge curvature; 1 = default, 0 = straight, higher = stronger arc. */
|
|
111
111
|
curvature?: number;
|
|
112
112
|
/** Entry curvature at the target node; defaults to `curvature` when omitted. */
|
|
113
113
|
entryCurvature?: number;
|
|
114
|
+
nodeSizePreset?: NodeSizePreset;
|
|
115
|
+
layoutOptions?: Parameters<typeof layoutFGCAPreview>[1];
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
export function renderFgcaSvg(doc: FGCADoc, options: RenderFgcaOptions = {}): string {
|
|
117
|
-
const {
|
|
118
|
-
|
|
119
|
+
const {
|
|
120
|
+
variant = 'dgca',
|
|
121
|
+
title = '',
|
|
122
|
+
curvature = DEFAULT_EDGE_CURVATURE,
|
|
123
|
+
entryCurvature,
|
|
124
|
+
nodeSizePreset = 'normal',
|
|
125
|
+
layoutOptions,
|
|
126
|
+
} = options;
|
|
127
|
+
const hideChanges = variant === 'dga';
|
|
128
|
+
const nodeSize = resolveDgcaNodeSize(parseNodeSizePreset(nodeSizePreset));
|
|
119
129
|
|
|
120
|
-
const { nodes, edges, columns, width, height } = layoutFGCAPreview(doc, {
|
|
130
|
+
const { nodes, edges, columns, width, height } = layoutFGCAPreview(doc, {
|
|
131
|
+
hideChanges,
|
|
132
|
+
nodeWidth: nodeSize.width,
|
|
133
|
+
nodeHeight: nodeSize.height,
|
|
134
|
+
...layoutOptions,
|
|
135
|
+
});
|
|
121
136
|
|
|
122
137
|
if (nodes.length === 0) {
|
|
123
138
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
|
|
124
139
|
}
|
|
125
140
|
|
|
126
|
-
const body = renderFgcaBody(columns, nodes, edges, curvature, entryCurvature
|
|
141
|
+
const body = renderFgcaBody(columns, nodes, edges, curvature, entryCurvature, {
|
|
142
|
+
nodeWidth: nodeSize.width,
|
|
143
|
+
nodeHeight: nodeSize.height,
|
|
144
|
+
});
|
|
127
145
|
|
|
128
146
|
const titleSvg = title
|
|
129
147
|
? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(title)}</text>`
|
|
130
148
|
: '';
|
|
131
149
|
|
|
132
|
-
// Embed the shared theme CSS inside the SVG so the rendered output is
|
|
133
|
-
// self-contained — the JCEF host page only needs to drop the SVG into the
|
|
134
|
-
// DOM and styling resolves without any cooperation from the host stylesheet.
|
|
135
150
|
const embedCss = generateSvgEmbedCss('transitrix');
|
|
136
151
|
|
|
137
152
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Browser-safe SVG renderer for the canonical Goals notation.
|
|
3
3
|
*
|
|
4
4
|
* Step 3 of the IntelliJ epic (ADR 0001): the webview bundle must turn a
|
|
5
|
-
* validated GoalTree into renderable SVG so JCEF can drop it into the
|
|
6
|
-
* panel. The VS Code path lives in `extension/src/goals-preview.ts` and pulls
|
|
5
|
+
* validated GoalTree into renderable SVG so JCEF can drop it into the
|
|
6
|
+
* preview panel. The VS Code path lives in `extension/src/goals-preview.ts` and pulls
|
|
7
7
|
* in VS Code-specific concerns (themes, title block, save dialogs); this
|
|
8
8
|
* module is the host-neutral subset — pure layout → SVG with no VS Code APIs,
|
|
9
9
|
* no `node:fs`, no `node:path`.
|
|
@@ -12,22 +12,23 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { layoutGoalTree } from '../goals/layout.js';
|
|
14
14
|
import type { GoalTree, GoalTreeLayout, LaidOutEdge } from '../goals/types.js';
|
|
15
|
+
import { displayGoalId } from '../goals/parse-canonical.js';
|
|
15
16
|
import { horizontalCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
|
|
17
|
+
import { parseNodeSizePreset, resolveGoalsNodeSize, type NodeSizePreset } from '../node-size-presets.js';
|
|
16
18
|
import { generateSvgEmbedCss, type ThemeId } from '../theme/index.js';
|
|
19
|
+
import { emitCenteredTextSvg, layoutCenteredEntityText } from './entity-text-layout.js';
|
|
17
20
|
import { escXml } from './render-util.js';
|
|
18
21
|
import { ENTITY_NODE_RX } from './notation-style.js';
|
|
19
22
|
|
|
20
|
-
const NODE_W = 250;
|
|
21
|
-
const NODE_H = 72;
|
|
22
23
|
const RANK_SEP = 100;
|
|
23
24
|
const NODE_SEP = 24;
|
|
24
25
|
const PAD = 24;
|
|
25
|
-
const LABEL_CHARS = 30;
|
|
26
26
|
|
|
27
27
|
export interface RenderGoalsOptions {
|
|
28
28
|
treeName?: string;
|
|
29
29
|
curvature?: number;
|
|
30
30
|
entryCurvature?: number;
|
|
31
|
+
nodeSizePreset?: NodeSizePreset;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/**
|
|
@@ -36,11 +37,12 @@ export interface RenderGoalsOptions {
|
|
|
36
37
|
* with the shared theme CSS embedded so the output is self-contained.
|
|
37
38
|
*/
|
|
38
39
|
export function renderGoalsSvg(tree: GoalTree, options: RenderGoalsOptions = {}): string {
|
|
39
|
-
const { treeName = '', curvature = DEFAULT_EDGE_CURVATURE, entryCurvature } = options;
|
|
40
|
+
const { treeName = '', curvature = DEFAULT_EDGE_CURVATURE, entryCurvature, nodeSizePreset = 'normal' } = options;
|
|
41
|
+
const nodeSize = resolveGoalsNodeSize(parseNodeSizePreset(nodeSizePreset));
|
|
40
42
|
|
|
41
43
|
const layout: GoalTreeLayout = layoutGoalTree(tree, {
|
|
42
|
-
nodeWidth:
|
|
43
|
-
nodeHeight:
|
|
44
|
+
nodeWidth: nodeSize.width,
|
|
45
|
+
nodeHeight: nodeSize.height,
|
|
44
46
|
rankSep: RANK_SEP,
|
|
45
47
|
nodeSep: NODE_SEP,
|
|
46
48
|
});
|
|
@@ -53,10 +55,6 @@ export function renderGoalsSvg(tree: GoalTree, options: RenderGoalsOptions = {})
|
|
|
53
55
|
? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(`Goal tree — ${treeName}`)}</text>`
|
|
54
56
|
: '';
|
|
55
57
|
|
|
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
58
|
return renderGoalsLayoutSvg(layout, { curvature, entryCurvature, title, embedCssTheme: 'transitrix' });
|
|
61
59
|
}
|
|
62
60
|
|
|
@@ -110,32 +108,23 @@ export function renderGoalsLayoutSvg(layout: GoalTreeLayout, options: RenderGoal
|
|
|
110
108
|
const y = n.y + oy;
|
|
111
109
|
const level = n.data.level % 8;
|
|
112
110
|
const labelText = n.data.name ?? String(n.id);
|
|
113
|
-
const words = labelText.split(' ');
|
|
114
|
-
let line1 = '';
|
|
115
|
-
let line2 = '';
|
|
116
|
-
for (const w of words) {
|
|
117
|
-
if ((line1 + ' ' + w).trim().length <= LABEL_CHARS) {
|
|
118
|
-
line1 = (line1 + ' ' + w).trim();
|
|
119
|
-
} else if ((line2 + ' ' + w).trim().length <= LABEL_CHARS) {
|
|
120
|
-
line2 = (line2 + ' ' + w).trim();
|
|
121
|
-
} else if (!line2) {
|
|
122
|
-
line2 = w.slice(0, LABEL_CHARS - 2) + '…';
|
|
123
|
-
break;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
const twoLines = line2.length > 0;
|
|
127
|
-
const nameY1 = twoLines ? y + 12 : y + 16;
|
|
128
|
-
const nameY2 = y + 26;
|
|
129
|
-
const typeY = twoLines ? y + 43 : y + 35;
|
|
130
|
-
const idY = twoLines ? y + 59 : y + 55;
|
|
131
111
|
const typeLabel = n.data.type ?? '';
|
|
132
|
-
const idText =
|
|
112
|
+
const idText = displayGoalId(n.data);
|
|
113
|
+
const specs = layoutCenteredEntityText({
|
|
114
|
+
boxX: x,
|
|
115
|
+
boxY: y,
|
|
116
|
+
boxWidth: n.width,
|
|
117
|
+
boxHeight: n.height,
|
|
118
|
+
name: labelText,
|
|
119
|
+
type: typeLabel || undefined,
|
|
120
|
+
id: idText,
|
|
121
|
+
nameMaxLines: 2,
|
|
122
|
+
idMaxLines: 1,
|
|
123
|
+
});
|
|
124
|
+
const textSvg = emitCenteredTextSvg(specs, x + n.width / 2, escXml);
|
|
133
125
|
return `<g>
|
|
134
126
|
<rect class="diagram-node level-${level}" x="${x}" y="${y}" width="${n.width}" height="${n.height}" rx="${ENTITY_NODE_RX}"/>
|
|
135
|
-
|
|
136
|
-
<text class="text-primary" x="${x + n.width / 2}" y="${nameY2}" text-anchor="middle" dominant-baseline="central">${escXml(line2)}</text>` : ''}${typeLabel ? `
|
|
137
|
-
<text class="text-secondary" x="${x + n.width / 2}" y="${typeY}" text-anchor="middle" dominant-baseline="central">${escXml(typeLabel)}</text>` : ''}
|
|
138
|
-
<text class="text-id" x="${x + n.width / 2}" y="${idY}" text-anchor="middle" dominant-baseline="central">${escXml(idText)}</text>
|
|
127
|
+
${textSvg}
|
|
139
128
|
</g>`;
|
|
140
129
|
})
|
|
141
130
|
.join('\n');
|