@transitrix/diagrams 1.7.6 → 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.
Files changed (53) hide show
  1. package/dist/activities/layout.js +7 -7
  2. package/dist/activities/types.d.ts +4 -0
  3. package/dist/capability-map/dsm-schema.d.ts +1 -1
  4. package/dist/capability-map/dsm-schema.js +1 -1
  5. package/dist/capability-map/layout-tree.d.ts +5 -1
  6. package/dist/capability-map/layout-tree.js +11 -9
  7. package/dist/capability-map/render-capability-tree.d.ts +2 -9
  8. package/dist/capability-map/render-capability-tree.js +16 -28
  9. package/dist/fgca/preview-layout.d.ts +8 -4
  10. package/dist/fgca/preview-layout.js +13 -13
  11. package/dist/node-size-presets.d.ts +50 -0
  12. package/dist/node-size-presets.js +103 -0
  13. package/dist/process-blueprint/layout.js +4 -47
  14. package/dist/webview/__tests__/entity-text-layout.test.d.ts +1 -0
  15. package/dist/webview/__tests__/entity-text-layout.test.js +43 -0
  16. package/dist/webview/__tests__/entry.test.js +3 -3
  17. package/dist/webview/__tests__/render-blocks.test.js +1 -7
  18. package/dist/webview/entity-text-layout.d.ts +95 -0
  19. package/dist/webview/entity-text-layout.js +279 -0
  20. package/dist/webview/entry.js +7 -3
  21. package/dist/webview/notation-style.d.ts +1 -1
  22. package/dist/webview/notation-style.js +1 -1
  23. package/dist/webview/render-activities.d.ts +3 -1
  24. package/dist/webview/render-activities.js +29 -32
  25. package/dist/webview/render-blocks.d.ts +4 -1
  26. package/dist/webview/render-blocks.js +27 -108
  27. package/dist/webview/render-fgca.d.ts +15 -8
  28. package/dist/webview/render-fgca.js +41 -44
  29. package/dist/webview/render-goals.d.ts +2 -0
  30. package/dist/webview/render-goals.js +21 -36
  31. package/dist/webview/render-process-blueprint.d.ts +4 -10
  32. package/dist/webview/render-process-blueprint.js +18 -64
  33. package/package.json +1 -1
  34. package/src/activities/layout.ts +12 -7
  35. package/src/activities/types.ts +4 -0
  36. package/src/capability-map/dsm-schema.ts +1 -1
  37. package/src/capability-map/index.ts +1 -1
  38. package/src/capability-map/layout-tree.ts +16 -8
  39. package/src/capability-map/render-capability-tree.ts +18 -30
  40. package/src/fgca/preview-layout.ts +21 -15
  41. package/src/node-size-presets.ts +157 -0
  42. package/src/process-blueprint/layout.ts +4 -48
  43. package/src/webview/__tests__/entity-text-layout.test.ts +52 -0
  44. package/src/webview/__tests__/entry.test.ts +3 -3
  45. package/src/webview/__tests__/render-blocks.test.ts +1 -9
  46. package/src/webview/entity-text-layout.ts +355 -0
  47. package/src/webview/entry.ts +8 -4
  48. package/src/webview/notation-style.ts +1 -1
  49. package/src/webview/render-activities.ts +38 -27
  50. package/src/webview/render-blocks.ts +35 -118
  51. package/src/webview/render-fgca.ts +59 -44
  52. package/src/webview/render-goals.ts +22 -34
  53. package/src/webview/render-process-blueprint.ts +30 -65
@@ -13,63 +13,12 @@
13
13
  * title `<text>`.
14
14
  */
15
15
  import { layoutNestedBlocks } from '../blocks/layout.js';
16
+ import { parseNodeSizePreset, resolveBlocksLeafSize } from '../node-size-presets.js';
16
17
  import { generateSvgEmbedCss } from '../theme/index.js';
18
+ import { emitCenteredTextSvg, layoutHeaderBlockText, layoutLeafBlockText, } from './entity-text-layout.js';
17
19
  import { escXml } from './render-util.js';
18
20
  import { ENTITY_NODE_RX } from './notation-style.js';
19
21
  const PAD = 24;
20
- // Approximate character width (px) at text-primary size (12px/600).
21
- const CHAR_W = 7;
22
- // Approximate character width (px) at text-id size (10px/600).
23
- const CHAR_W_ID = 6;
24
- // Vertical spacing between line centres.
25
- const LINE_H = 14; // name lines (text-primary, 12 px font)
26
- const LINE_H_ID = 12; // id lines (text-id, 10 px font)
27
- // Minimum gap to avoid overlap: half of name text height (6 px) + half of id
28
- // text height (5 px) = 11 px. Use 14 px to match the name-line separation and
29
- // leave a 3 px visual buffer — same as the gap between consecutive name lines.
30
- const NAME_ID_GAP = 14;
31
- /** Word-wrap `text` to at most `maxLines` lines of `maxChars` each. */
32
- function wrapWords(text, maxChars, maxLines) {
33
- const safeMax = Math.max(2, maxChars);
34
- const words = text.split(' ');
35
- const lines = [];
36
- let cur = '';
37
- for (let i = 0; i < words.length; i++) {
38
- const w = words[i];
39
- const next = cur ? `${cur} ${w}` : w;
40
- if (next.length <= safeMax) {
41
- cur = next;
42
- }
43
- else {
44
- if (cur)
45
- lines.push(cur);
46
- cur = '';
47
- if (lines.length >= maxLines)
48
- break;
49
- if (lines.length === maxLines - 1) {
50
- const rest = words.slice(i).join(' ');
51
- lines.push(rest.length <= safeMax ? rest : rest.slice(0, safeMax - 1) + '…');
52
- return lines;
53
- }
54
- cur = w.length <= safeMax ? w : w.slice(0, safeMax - 1) + '…';
55
- }
56
- }
57
- if (cur && lines.length < maxLines)
58
- lines.push(cur);
59
- return lines.length ? lines : [text.slice(0, safeMax - 1) + '…'];
60
- }
61
- /** Split an ID (no spaces) across at most 2 lines, breaking at `_` or `-`. */
62
- function wrapId(id, maxChars) {
63
- if (id.length <= maxChars)
64
- return [id];
65
- const seg = id.slice(0, maxChars);
66
- const sepIdx = Math.max(seg.lastIndexOf('_'), seg.lastIndexOf('-'));
67
- const cut = sepIdx > Math.floor(maxChars / 3) ? sepIdx : maxChars - 1;
68
- const isSep = id[cut] === '_' || id[cut] === '-';
69
- const line1 = id.slice(0, cut);
70
- const rest = id.slice(cut + (isSep ? 1 : 0));
71
- return [line1, rest.length <= maxChars ? rest : rest.slice(0, maxChars - 1) + '…'];
72
- }
73
22
  /**
74
23
  * Pick the diagram-frame level class for a block at the given depth.
75
24
  *
@@ -82,67 +31,32 @@ function levelClassForDepth(depth) {
82
31
  const idx = Math.min(Math.max(depth - 1, 0), 6);
83
32
  return `level-${idx}`;
84
33
  }
85
- // Reserve a small horizontal margin so text never abuts the rounded corners.
86
- const TEXT_MARGIN_X = 8;
87
- /** Truncate `text` to at most `maxChars` characters, at a word boundary when possible. */
88
- function truncateLine(text, maxChars) {
89
- const safeMax = Math.max(2, maxChars);
90
- if (text.length <= safeMax)
91
- return text;
92
- const cut = text.slice(0, safeMax - 1);
93
- const lastSpace = cut.lastIndexOf(' ');
94
- return (lastSpace > safeMax / 2 ? cut.slice(0, lastSpace) : cut) + '…';
95
- }
96
- /** Single-line ID truncation that prefers `_`/`-` separators before falling back to a hard cut. */
97
- function truncateId(id, maxChars) {
98
- const safeMax = Math.max(2, maxChars);
99
- if (id.length <= safeMax)
100
- return id;
101
- const cut = id.slice(0, safeMax - 1);
102
- const sepIdx = Math.max(cut.lastIndexOf('_'), cut.lastIndexOf('-'));
103
- return (sepIdx > safeMax / 3 ? cut.slice(0, sepIdx) : cut) + '…';
104
- }
105
34
  function emitBlockSvg(b, ox, oy, parts) {
106
35
  const cls = levelClassForDepth(b.depth);
107
36
  const cx = b.x + ox + b.width / 2;
108
37
  parts.push(`<rect class="diagram-node ${cls}" x="${b.x + ox}" y="${b.y + oy}" width="${b.width}" height="${b.height}" rx="${ENTITY_NODE_RX}"/>`);
109
- const innerW = Math.max(0, b.width - TEXT_MARGIN_X * 2);
110
38
  const isLeaf = b.children.length === 0;
111
39
  if (isLeaf) {
112
- // Leaf block: name (up to 3 lines) then ID (up to 2 lines), centred vertically.
113
- const maxCharsName = Math.max(4, Math.floor(innerW / CHAR_W));
114
- const maxCharsId = Math.max(4, Math.floor(innerW / CHAR_W_ID));
115
- const nameLines = wrapWords(b.name, maxCharsName, 3);
116
- const idLines = wrapId(b.id, maxCharsId);
117
- const nameTotalSpan = (nameLines.length - 1) * LINE_H;
118
- const idTotalSpan = (idLines.length - 1) * LINE_H_ID;
119
- const totalSpan = nameTotalSpan + NAME_ID_GAP + idTotalSpan;
120
- const firstY = Math.round(b.y + oy + (b.height - totalSpan) / 2);
121
- for (let i = 0; i < nameLines.length; i++) {
122
- parts.push(`<text class="text-primary" x="${cx}" y="${firstY + i * LINE_H}" text-anchor="middle" dominant-baseline="central">${escXml(nameLines[i])}</text>`);
123
- }
124
- const idFirstY = firstY + nameTotalSpan + NAME_ID_GAP;
125
- for (let i = 0; i < idLines.length; i++) {
126
- parts.push(`<text class="text-id" x="${cx}" y="${idFirstY + i * LINE_H_ID}" text-anchor="middle" dominant-baseline="central">${escXml(idLines[i])}</text>`);
127
- }
40
+ const specs = layoutLeafBlockText({
41
+ boxX: b.x + ox,
42
+ boxY: b.y + oy,
43
+ boxWidth: b.width,
44
+ boxHeight: b.height,
45
+ name: b.name,
46
+ id: b.id,
47
+ });
48
+ parts.push(emitCenteredTextSvg(specs, cx, escXml));
128
49
  }
129
50
  else {
130
- // Container block: name (text-primary) above, ID (text-id, grey) below — both
131
- // single-line, truncated with `…` to fit within `b.width`. The header strip is
132
- // only `headerHeight` tall (default 28px), which is enough for one line each at
133
- // the configured text sizes. Stacking the ID as its own text-id element keeps
134
- // the wrapping/truncation rule consistent with leaves and guarantees no overflow
135
- // for any combination of name/ID length.
136
- const maxCharsName = Math.max(4, Math.floor(innerW / CHAR_W));
137
- const maxCharsId = Math.max(4, Math.floor(innerW / CHAR_W_ID));
138
- const nameText = truncateLine(b.name, maxCharsName);
139
- const idText = truncateId(b.id, maxCharsId);
140
- const headerMid = b.y + oy + b.headerHeight / 2;
141
- const halfSpan = (LINE_H + LINE_H_ID) / 4;
142
- const nameY = Math.round(headerMid - halfSpan);
143
- const idY = Math.round(headerMid + halfSpan);
144
- parts.push(`<text class="text-primary" x="${cx}" y="${nameY}" text-anchor="middle" dominant-baseline="central">${escXml(nameText)}</text>`);
145
- parts.push(`<text class="text-id" x="${cx}" y="${idY}" text-anchor="middle" dominant-baseline="central">${escXml(idText)}</text>`);
51
+ const specs = layoutHeaderBlockText({
52
+ boxX: b.x + ox,
53
+ boxY: b.y + oy,
54
+ boxWidth: b.width,
55
+ headerHeight: b.headerHeight,
56
+ name: b.name,
57
+ id: b.id,
58
+ });
59
+ parts.push(emitCenteredTextSvg(specs, cx, escXml));
146
60
  for (const c of b.children)
147
61
  emitBlockSvg(c, ox, oy, parts);
148
62
  }
@@ -176,8 +90,13 @@ ${parts.join('\n')}
176
90
  * with the shared theme CSS embedded so the output is self-contained.
177
91
  */
178
92
  export function renderBlocksSvg(doc, options = {}) {
179
- const { title = '' } = options;
180
- const layout = layoutNestedBlocks(doc);
93
+ const { title = '', nodeSizePreset = 'normal', layoutOptions } = options;
94
+ const leaf = resolveBlocksLeafSize(parseNodeSizePreset(nodeSizePreset));
95
+ const layout = layoutNestedBlocks(doc, {
96
+ leafWidth: leaf.width,
97
+ leafHeight: leaf.height,
98
+ ...layoutOptions,
99
+ });
181
100
  if (layout.blocks.length === 0) {
182
101
  return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
183
102
  }
@@ -1,22 +1,27 @@
1
1
  /**
2
- * Browser-safe SVG renderer for the canonical FGCA and FGA notations.
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 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
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
- * FGCA and FGA share the same renderer; FGA only differs by hiding the
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 { layoutFGCAPreview } from '../fgca/preview-layout.js';
16
16
  import type { FGCADoc } from '../fgca/validate.js';
17
+ import { type NodeSizePreset } from '../node-size-presets.js';
17
18
  type FgcaLayout = ReturnType<typeof layoutFGCAPreview>;
19
+ export interface RenderFgcaBodyOptions {
20
+ nodeWidth?: number;
21
+ nodeHeight?: number;
22
+ }
18
23
  /**
19
- * Canonical FGCA/FGA body — the column headers, node rects and edge paths that
24
+ * Canonical DGCA/DGA body — the column headers, node rects and edge paths that
20
25
  * go inside the `<svg>`, shared verbatim by the VS Code preview (`buildSvg`)
21
26
  * and the host-neutral wrapper below. Excludes the host-specific title block
22
27
  * and the (identical) `<defs>` arrow marker. Coordinates are absolute (the
@@ -24,16 +29,18 @@ type FgcaLayout = ReturnType<typeof layoutFGCAPreview>;
24
29
  * to make room for its title block, while the host-neutral wrapper renders it
25
30
  * untranslated.
26
31
  */
27
- export declare function renderFgcaBody(columns: FgcaLayout['columns'], nodes: FgcaLayout['nodes'], edges: FgcaLayout['edges'], curvature: number, entryCurvature: number | undefined): string;
32
+ export declare function renderFgcaBody(columns: FgcaLayout['columns'], nodes: FgcaLayout['nodes'], edges: FgcaLayout['edges'], curvature: number, entryCurvature: number | undefined, bodyOptions?: RenderFgcaBodyOptions): string;
28
33
  export interface RenderFgcaOptions {
29
- /** `'fga'` hides the Changes column; defaults to `'fgca'`. */
30
- variant?: 'fgca' | 'fga';
34
+ /** `'dga'` hides the Changes column; defaults to `'dgca'`. */
35
+ variant?: 'dgca' | 'dga';
31
36
  /** Optional heading rendered as a left-anchored `text-header` line. */
32
37
  title?: string;
33
38
  /** Exit edge curvature; 1 = default, 0 = straight, higher = stronger arc. */
34
39
  curvature?: number;
35
40
  /** Entry curvature at the target node; defaults to `curvature` when omitted. */
36
41
  entryCurvature?: number;
42
+ nodeSizePreset?: NodeSizePreset;
43
+ layoutOptions?: Parameters<typeof layoutFGCAPreview>[1];
37
44
  }
38
45
  export declare function renderFgcaSvg(doc: FGCADoc, options?: RenderFgcaOptions): string;
39
46
  export {};
@@ -1,20 +1,22 @@
1
1
  /**
2
- * Browser-safe SVG renderer for the canonical FGCA and FGA notations.
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 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
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
- * FGCA and FGA share the same renderer; FGA only differs by hiding the
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
- import { layoutFGCAPreview, FGCA_NODE_W as NODE_W, FGCA_NODE_H as NODE_H, FGCA_HEADER_H as HEADER_H, FGCA_PAD as PAD, } from '../fgca/preview-layout.js';
15
+ import { layoutFGCAPreview, FGCA_NODE_W, FGCA_NODE_H, FGCA_HEADER_H as HEADER_H, FGCA_PAD as PAD, } from '../fgca/preview-layout.js';
16
16
  import { horizontalCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
17
+ import { parseNodeSizePreset, resolveDgcaNodeSize } from '../node-size-presets.js';
17
18
  import { generateSvgEmbedCss } from '../theme/index.js';
19
+ import { emitCenteredTextSvg, layoutCenteredEntityText, truncateLine } from './entity-text-layout.js';
18
20
  import { escXml } from './render-util.js';
19
21
  const COL_LABELS = {
20
22
  driver: 'Drivers (D)',
@@ -23,7 +25,7 @@ const COL_LABELS = {
23
25
  activity: 'Actions (A)',
24
26
  };
25
27
  /**
26
- * Canonical FGCA/FGA body — the column headers, node rects and edge paths that
28
+ * Canonical DGCA/DGA body — the column headers, node rects and edge paths that
27
29
  * go inside the `<svg>`, shared verbatim by the VS Code preview (`buildSvg`)
28
30
  * and the host-neutral wrapper below. Excludes the host-specific title block
29
31
  * and the (identical) `<defs>` arrow marker. Coordinates are absolute (the
@@ -31,11 +33,14 @@ const COL_LABELS = {
31
33
  * to make room for its title block, while the host-neutral wrapper renders it
32
34
  * untranslated.
33
35
  */
34
- export function renderFgcaBody(columns, nodes, edges, curvature, entryCurvature) {
36
+ export function renderFgcaBody(columns, nodes, edges, curvature, entryCurvature, bodyOptions = {}) {
37
+ const nodeWidth = bodyOptions.nodeWidth ?? FGCA_NODE_W;
38
+ const nodeHeight = bodyOptions.nodeHeight ?? FGCA_NODE_H;
39
+ const headerTruncate = Math.max(8, Math.floor((nodeWidth - 16) / 7));
35
40
  const headerSvg = columns
36
41
  .map(({ col, x }) => [
37
- `<rect class="diagram-node layer-${col}" x="${x}" y="${PAD}" width="${NODE_W}" height="${HEADER_H}" rx="6"/>`,
38
- `<text class="text-header" x="${x + NODE_W / 2}" y="${PAD + HEADER_H / 2}" text-anchor="middle" dominant-baseline="central">${escXml(COL_LABELS[col])}</text>`,
42
+ `<rect class="diagram-node layer-${col}" x="${x}" y="${PAD}" width="${nodeWidth}" height="${HEADER_H}" rx="6"/>`,
43
+ `<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>`,
39
44
  ].join('\n'))
40
45
  .join('\n');
41
46
  const edgeSvg = edges
@@ -43,54 +48,46 @@ export function renderFgcaBody(columns, nodes, edges, curvature, entryCurvature)
43
48
  .join('\n');
44
49
  const nodeSvg = nodes
45
50
  .map((n) => {
46
- const words = n.label.split(' ');
47
- let line1 = '';
48
- let line2 = '';
49
- for (const w of words) {
50
- if ((line1 + ' ' + w).trim().length <= 26) {
51
- line1 = (line1 + ' ' + w).trim();
52
- }
53
- else if ((line2 + ' ' + w).trim().length <= 26) {
54
- line2 = (line2 + ' ' + w).trim();
55
- }
56
- else if (!line2) {
57
- line2 = w.slice(0, 24) + '…';
58
- break;
59
- }
60
- }
61
- const twoLines = line2.length > 0;
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
51
  const entityId = n.id.slice(n.id.indexOf('_') + 1);
52
+ const specs = layoutCenteredEntityText({
53
+ boxX: n.x,
54
+ boxY: n.y,
55
+ boxWidth: nodeWidth,
56
+ boxHeight: nodeHeight,
57
+ name: n.label,
58
+ id: entityId,
59
+ nameMaxLines: 2,
60
+ idMaxLines: 1,
61
+ });
62
+ const textSvg = emitCenteredTextSvg(specs, n.x + nodeWidth / 2, escXml);
66
63
  return [
67
- `<rect class="diagram-node layer-${n.col}" x="${n.x}" y="${n.y}" width="${NODE_W}" height="${NODE_H}" rx="8"/>`,
68
- `<text class="text-primary" x="${n.x + NODE_W / 2}" y="${y1}" text-anchor="middle" dominant-baseline="central">${escXml(line1)}</text>`,
69
- twoLines
70
- ? `<text class="text-primary" x="${n.x + NODE_W / 2}" y="${y2}" text-anchor="middle" dominant-baseline="central">${escXml(line2)}</text>`
71
- : '',
72
- `<text class="text-id" x="${n.x + NODE_W / 2}" y="${idY}" text-anchor="middle" dominant-baseline="central">${escXml(entityId)}</text>`,
73
- ]
74
- .filter(Boolean)
75
- .join('\n');
64
+ `<rect class="diagram-node layer-${n.col}" x="${n.x}" y="${n.y}" width="${nodeWidth}" height="${nodeHeight}" rx="8"/>`,
65
+ textSvg,
66
+ ].join('\n');
76
67
  })
77
68
  .join('\n');
78
69
  return `${headerSvg}\n${nodeSvg}\n${edgeSvg}`;
79
70
  }
80
71
  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 });
72
+ const { variant = 'dgca', title = '', curvature = DEFAULT_EDGE_CURVATURE, entryCurvature, nodeSizePreset = 'normal', layoutOptions, } = options;
73
+ const hideChanges = variant === 'dga';
74
+ const nodeSize = resolveDgcaNodeSize(parseNodeSizePreset(nodeSizePreset));
75
+ const { nodes, edges, columns, width, height } = layoutFGCAPreview(doc, {
76
+ hideChanges,
77
+ nodeWidth: nodeSize.width,
78
+ nodeHeight: nodeSize.height,
79
+ ...layoutOptions,
80
+ });
84
81
  if (nodes.length === 0) {
85
82
  return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
86
83
  }
87
- const body = renderFgcaBody(columns, nodes, edges, curvature, entryCurvature);
84
+ const body = renderFgcaBody(columns, nodes, edges, curvature, entryCurvature, {
85
+ nodeWidth: nodeSize.width,
86
+ nodeHeight: nodeSize.height,
87
+ });
88
88
  const titleSvg = title
89
89
  ? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(title)}</text>`
90
90
  : '';
91
- // Embed the shared theme CSS inside the SVG so the rendered output is
92
- // self-contained — the JCEF host page only needs to drop the SVG into the
93
- // DOM and styling resolves without any cooperation from the host stylesheet.
94
91
  const embedCss = generateSvgEmbedCss('transitrix');
95
92
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
96
93
  <style>${embedCss}</style>
@@ -1,9 +1,11 @@
1
1
  import type { GoalTree, GoalTreeLayout } from '../goals/types.js';
2
+ import { type NodeSizePreset } from '../node-size-presets.js';
2
3
  import { type ThemeId } from '../theme/index.js';
3
4
  export interface RenderGoalsOptions {
4
5
  treeName?: string;
5
6
  curvature?: number;
6
7
  entryCurvature?: number;
8
+ nodeSizePreset?: NodeSizePreset;
7
9
  }
8
10
  /**
9
11
  * Host-neutral goals renderer (IntelliJ/UI). Lays the tree out with the default
@@ -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 preview
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`.
@@ -13,25 +13,25 @@
13
13
  import { layoutGoalTree } from '../goals/layout.js';
14
14
  import { displayGoalId } from '../goals/parse-canonical.js';
15
15
  import { horizontalCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
16
+ import { parseNodeSizePreset, resolveGoalsNodeSize } from '../node-size-presets.js';
16
17
  import { generateSvgEmbedCss } from '../theme/index.js';
18
+ import { emitCenteredTextSvg, layoutCenteredEntityText } from './entity-text-layout.js';
17
19
  import { escXml } from './render-util.js';
18
20
  import { ENTITY_NODE_RX } from './notation-style.js';
19
- const NODE_W = 250;
20
- const NODE_H = 72;
21
21
  const RANK_SEP = 100;
22
22
  const NODE_SEP = 24;
23
23
  const PAD = 24;
24
- const LABEL_CHARS = 30;
25
24
  /**
26
25
  * Host-neutral goals renderer (IntelliJ/UI). Lays the tree out with the default
27
26
  * spacing, then delegates the actual SVG emission to {@link renderGoalsLayoutSvg}
28
27
  * with the shared theme CSS embedded so the output is self-contained.
29
28
  */
30
29
  export function renderGoalsSvg(tree, options = {}) {
31
- const { treeName = '', curvature = DEFAULT_EDGE_CURVATURE, entryCurvature } = options;
30
+ const { treeName = '', curvature = DEFAULT_EDGE_CURVATURE, entryCurvature, nodeSizePreset = 'normal' } = options;
31
+ const nodeSize = resolveGoalsNodeSize(parseNodeSizePreset(nodeSizePreset));
32
32
  const layout = layoutGoalTree(tree, {
33
- nodeWidth: NODE_W,
34
- nodeHeight: NODE_H,
33
+ nodeWidth: nodeSize.width,
34
+ nodeHeight: nodeSize.height,
35
35
  rankSep: RANK_SEP,
36
36
  nodeSep: NODE_SEP,
37
37
  });
@@ -41,10 +41,6 @@ export function renderGoalsSvg(tree, options = {}) {
41
41
  const title = treeName
42
42
  ? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(`Goal tree — ${treeName}`)}</text>`
43
43
  : '';
44
- // Embed the shared theme CSS so the JCEF host page only needs to drop the SVG
45
- // into the DOM — styling resolves without help from the host stylesheet. The
46
- // VS Code preview omits this (its webview supplies the CSS) and embeds it only
47
- // on export via `prepareSvgForExport`.
48
44
  return renderGoalsLayoutSvg(layout, { curvature, entryCurvature, title, embedCssTheme: 'transitrix' });
49
45
  }
50
46
  /**
@@ -82,34 +78,23 @@ export function renderGoalsLayoutSvg(layout, options = {}) {
82
78
  const y = n.y + oy;
83
79
  const level = n.data.level % 8;
84
80
  const labelText = n.data.name ?? String(n.id);
85
- const words = labelText.split(' ');
86
- let line1 = '';
87
- let line2 = '';
88
- for (const w of words) {
89
- if ((line1 + ' ' + w).trim().length <= LABEL_CHARS) {
90
- line1 = (line1 + ' ' + w).trim();
91
- }
92
- else if ((line2 + ' ' + w).trim().length <= LABEL_CHARS) {
93
- line2 = (line2 + ' ' + w).trim();
94
- }
95
- else if (!line2) {
96
- line2 = w.slice(0, LABEL_CHARS - 2) + '…';
97
- break;
98
- }
99
- }
100
- const twoLines = line2.length > 0;
101
- const nameY1 = twoLines ? y + 12 : y + 16;
102
- const nameY2 = y + 26;
103
- const typeY = twoLines ? y + 43 : y + 35;
104
- const idY = twoLines ? y + 59 : y + 55;
105
81
  const typeLabel = n.data.type ?? '';
106
82
  const idText = displayGoalId(n.data);
83
+ const specs = layoutCenteredEntityText({
84
+ boxX: x,
85
+ boxY: y,
86
+ boxWidth: n.width,
87
+ boxHeight: n.height,
88
+ name: labelText,
89
+ type: typeLabel || undefined,
90
+ id: idText,
91
+ nameMaxLines: 2,
92
+ idMaxLines: 1,
93
+ });
94
+ const textSvg = emitCenteredTextSvg(specs, x + n.width / 2, escXml);
107
95
  return `<g>
108
96
  <rect class="diagram-node level-${level}" x="${x}" y="${y}" width="${n.width}" height="${n.height}" rx="${ENTITY_NODE_RX}"/>
109
- <text class="text-primary" x="${x + n.width / 2}" y="${nameY1}" text-anchor="middle" dominant-baseline="central">${escXml(line1)}</text>${twoLines ? `
110
- <text class="text-primary" x="${x + n.width / 2}" y="${nameY2}" text-anchor="middle" dominant-baseline="central">${escXml(line2)}</text>` : ''}${typeLabel ? `
111
- <text class="text-secondary" x="${x + n.width / 2}" y="${typeY}" text-anchor="middle" dominant-baseline="central">${escXml(typeLabel)}</text>` : ''}
112
- <text class="text-id" x="${x + n.width / 2}" y="${idY}" text-anchor="middle" dominant-baseline="central">${escXml(idText)}</text>
97
+ ${textSvg}
113
98
  </g>`;
114
99
  })
115
100
  .join('\n');
@@ -1,15 +1,9 @@
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
- */
1
+ import type { ProcessBlueprintFile, ProcessBlueprintLayout, ProcessBlueprintLayoutOptions } from '../process-blueprint/types.js';
2
+ import { type NodeSizePreset } from '../node-size-presets.js';
11
3
  export declare function renderProcessBlueprintBody(layout: ProcessBlueprintLayout, ox: number, oy: number, clipId?: string): string;
12
4
  export interface RenderProcessBlueprintOptions {
13
5
  title?: string;
6
+ nodeSizePreset?: NodeSizePreset;
7
+ layoutOptions?: ProcessBlueprintLayoutOptions;
14
8
  }
15
9
  export declare function renderProcessBlueprintSvg(doc: ProcessBlueprintFile, options?: RenderProcessBlueprintOptions): string;