@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.
- 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/fgca/preview-layout.d.ts +8 -4
- package/dist/fgca/preview-layout.js +13 -13
- 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/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/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 +21 -36
- 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/fgca/preview-layout.ts +21 -15
- package/src/node-size-presets.ts +157 -0
- package/src/process-blueprint/layout.ts +4 -48
- 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/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 +22 -34
- 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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
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
|
|
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 { 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
|
|
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
|
-
/** `'
|
|
30
|
-
variant?: '
|
|
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
|
|
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
|
-
import { layoutFGCAPreview, FGCA_NODE_W
|
|
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
|
|
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="${
|
|
38
|
-
`<text class="text-header" x="${x +
|
|
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="${
|
|
68
|
-
|
|
69
|
-
|
|
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 = '
|
|
82
|
-
const hideChanges = variant === '
|
|
83
|
-
const
|
|
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
|
|
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:
|
|
34
|
-
nodeHeight:
|
|
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
|
-
|
|
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;
|