@transitrix/diagrams 1.7.6 → 1.8.2
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/__tests__/node-size-presets.test.d.ts +1 -0
- package/dist/__tests__/node-size-presets.test.js +30 -0
- package/dist/activities/layout.js +7 -7
- package/dist/activities/types.d.ts +4 -0
- package/dist/blocks/__tests__/layout.test.js +2 -2
- package/dist/blocks/layout.js +3 -2
- package/dist/capability-map/dsm-layout.js +1 -1
- 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 +6 -2
- package/dist/capability-map/layout-tree.js +12 -10
- 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 +10 -6
- package/dist/fgca/preview-layout.js +16 -15
- package/dist/node-size-presets.d.ts +63 -0
- package/dist/node-size-presets.js +117 -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 +60 -0
- package/dist/webview/__tests__/entry.test.js +3 -3
- package/dist/webview/__tests__/render-blocks.test.js +10 -13
- package/dist/webview/__tests__/render-goals.test.js +41 -0
- package/dist/webview/entity-text-layout.d.ts +95 -0
- package/dist/webview/entity-text-layout.js +277 -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/__tests__/node-size-presets.test.ts +50 -0
- package/src/activities/layout.ts +12 -7
- package/src/activities/types.ts +4 -0
- package/src/blocks/__tests__/layout.test.ts +2 -2
- package/src/blocks/layout.ts +3 -2
- package/src/capability-map/dsm-layout.ts +1 -1
- package/src/capability-map/dsm-schema.ts +1 -1
- package/src/capability-map/index.ts +1 -1
- package/src/capability-map/layout-tree.ts +17 -9
- package/src/capability-map/render-capability-tree.ts +18 -30
- package/src/fgca/preview-layout.ts +24 -17
- package/src/node-size-presets.ts +180 -0
- package/src/process-blueprint/layout.ts +4 -48
- package/src/webview/__tests__/entity-text-layout.test.ts +73 -0
- package/src/webview/__tests__/entry.test.ts +3 -3
- package/src/webview/__tests__/render-blocks.test.ts +11 -14
- package/src/webview/__tests__/render-goals.test.ts +42 -0
- 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
|
@@ -10,45 +10,24 @@
|
|
|
10
10
|
* self-contained. No `vscode`, no `node:*`, no host APIs.
|
|
11
11
|
*/
|
|
12
12
|
import { layoutProcessBlueprint } from '../process-blueprint/layout.js';
|
|
13
|
+
import { parseNodeSizePreset, resolveProcessBlueprintSize, } from '../node-size-presets.js';
|
|
13
14
|
import { generateSvgEmbedCss } from '../theme/index.js';
|
|
15
|
+
import { layoutCenteredPillText, layoutLeftCellLines, maxCharsForInnerWidth, truncateLine, } from './entity-text-layout.js';
|
|
14
16
|
import { escXml } from './render-util.js';
|
|
15
17
|
const PAD = 24;
|
|
16
|
-
// Blueprint-specific rules not carried by the shared theme CSS. The outer
|
|
17
|
-
// border is drawn last on top of the clipped inner content; the row
|
|
18
|
-
// backgrounds are transparent (pills/chips carry the colour). Mirrors
|
|
19
|
-
// `BLUEPRINT_CSS` from the VS Code preview so the self-contained JCEF output
|
|
20
|
-
// matches the editor.
|
|
21
18
|
const BP_EMBED_CSS = `
|
|
22
19
|
.bp-row-bg { fill: none; }
|
|
23
20
|
.bp-border { fill: none; stroke: var(--ts-border, #cbd5e1); stroke-width: 1; }`;
|
|
24
|
-
function truncate(text, maxChars) {
|
|
25
|
-
if (text.length <= maxChars)
|
|
26
|
-
return text;
|
|
27
|
-
return text.slice(0, Math.max(0, maxChars - 1)) + '…';
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Render a left-anchored, vertically centred multi-line text cell. `lines` are
|
|
31
|
-
* pre-wrapped by the layout; the block is centred within the cell height.
|
|
32
|
-
*/
|
|
33
21
|
function textCellSvg(lines, cls, x, cellTop, cellHeight, lineHeight) {
|
|
22
|
+
const specs = layoutLeftCellLines(lines, x, cellTop, cellHeight, lineHeight, cls);
|
|
34
23
|
const ls = lines.length > 0 ? lines : [''];
|
|
35
24
|
const first = cellTop + cellHeight / 2 - ((ls.length - 1) / 2) * lineHeight;
|
|
36
25
|
const tspans = ls
|
|
37
26
|
.map((ln, i) => `<tspan x="${x}" y="${first + i * lineHeight}">${escXml(ln)}</tspan>`)
|
|
38
27
|
.join('');
|
|
28
|
+
void specs;
|
|
39
29
|
return `<text class="${cls}" dominant-baseline="central">${tspans}</text>`;
|
|
40
30
|
}
|
|
41
|
-
/**
|
|
42
|
-
* Render a single compliance law chip wrapped in a `<g data-chip-law…>` so the
|
|
43
|
-
* VS Code host can wire click-to-drill-down. Other hosts (JCEF) simply ignore
|
|
44
|
-
* the data attributes.
|
|
45
|
-
*
|
|
46
|
-
* Three orthogonal decorations are expressed as SVG overrides:
|
|
47
|
-
* - `new` — dashed stroke border (`stroke-dasharray="4 2"`)
|
|
48
|
-
* - `gap` — warning-level fill (`class="…compliance-gap"`)
|
|
49
|
-
* - `deadline` — urgent fill override (`class="…compliance-deadline"`) + a small
|
|
50
|
-
* badge circle in the top-right corner
|
|
51
|
-
*/
|
|
52
31
|
function complianceChipSvg(chip, ox, oy, stageId) {
|
|
53
32
|
const { x, y, width, height, lawId, decorations } = chip;
|
|
54
33
|
const ax = x + ox;
|
|
@@ -62,9 +41,10 @@ function complianceChipSvg(chip, ox, oy, stageId) {
|
|
|
62
41
|
else if (hasGap)
|
|
63
42
|
rectClass += ' compliance-gap';
|
|
64
43
|
const strokeDash = hasNew ? ' stroke-dasharray="4 2"' : '';
|
|
44
|
+
const maxChars = maxCharsForInnerWidth(Math.max(0, width - 8), 8);
|
|
65
45
|
const parts = [];
|
|
66
46
|
parts.push(`<rect class="${rectClass}" x="${ax}" y="${ay}" width="${width}" height="${height}" rx="6"${strokeDash}/>`);
|
|
67
|
-
parts.push(`<text class="text-pill" x="${ax + width / 2}" y="${ay + height / 2}" text-anchor="middle" dominant-baseline="central">${escXml(
|
|
47
|
+
parts.push(`<text class="text-pill" x="${ax + width / 2}" y="${ay + height / 2}" text-anchor="middle" dominant-baseline="central">${escXml(truncateLine(lawId, maxChars))}</text>`);
|
|
68
48
|
if (hasDeadline) {
|
|
69
49
|
const br = 5;
|
|
70
50
|
const bx = ax + width - br - 3;
|
|
@@ -74,38 +54,23 @@ function complianceChipSvg(chip, ox, oy, stageId) {
|
|
|
74
54
|
}
|
|
75
55
|
return `<g data-chip-law="${escXml(lawId)}" data-chip-stage="${escXml(stageId)}">\n${parts.join('\n')}\n</g>`;
|
|
76
56
|
}
|
|
77
|
-
/**
|
|
78
|
-
* Canonical Process Blueprint body — everything inside the `<svg>` except the
|
|
79
|
-
* host-specific title block. Shared verbatim by the VS Code preview and the
|
|
80
|
-
* host-neutral wrapper below so both surfaces stay pixel-identical.
|
|
81
|
-
*
|
|
82
|
-
* Layering: a clipped group holds the row fills and grid lines; pills and
|
|
83
|
-
* compliance chips render on top (so grid lines sit under pill content); the
|
|
84
|
-
* outer rounded border is drawn last so corner cells never overflow it.
|
|
85
|
-
*/
|
|
86
57
|
export function renderProcessBlueprintBody(layout, ox, oy, clipId = 'bp-clip') {
|
|
87
58
|
const tw = layout.bounds.width;
|
|
88
59
|
const th = layout.bounds.height;
|
|
89
60
|
const parts = [];
|
|
90
|
-
// Pills and chips collected separately so they render after (on top of) grid lines.
|
|
91
61
|
const topParts = [];
|
|
92
|
-
// Outer background (fill only; border drawn last on top).
|
|
93
62
|
parts.push(`<rect class="diagram-node level-0 bp-row-bg" x="${ox}" y="${oy}" width="${tw}" height="${th}" rx="6" stroke="none"/>`);
|
|
94
|
-
// ClipPath — clips all inner content to the outer rounded rect so corner cells
|
|
95
|
-
// don't visually overflow the rx=6 boundary.
|
|
96
63
|
parts.push(`<defs><clipPath id="${clipId}"><rect x="${ox}" y="${oy}" width="${tw}" height="${th}" rx="6"/></clipPath></defs>`);
|
|
97
64
|
const inner = [];
|
|
98
|
-
|
|
65
|
+
const headerMaxChars = Math.max(8, Math.floor((layout.stageColumnWidth - 16) / 7));
|
|
99
66
|
for (const s of layout.stageHeaders) {
|
|
100
67
|
inner.push(`<rect class="diagram-node level-1" x="${s.x + ox}" y="${s.y + oy}" width="${s.width}" height="${s.height}" stroke="none"/>`);
|
|
101
|
-
inner.push(`<text class="text-header" x="${s.x + ox + s.width / 2}" y="${s.y + oy + s.height / 2}" text-anchor="middle" dominant-baseline="central">${escXml(
|
|
68
|
+
inner.push(`<text class="text-header" x="${s.x + ox + s.width / 2}" y="${s.y + oy + s.height / 2}" text-anchor="middle" dominant-baseline="central">${escXml(truncateLine(s.name, headerMaxChars))}</text>`);
|
|
102
69
|
}
|
|
103
|
-
// Legend column (row labels).
|
|
104
70
|
for (const l of layout.legend) {
|
|
105
71
|
inner.push(`<rect class="diagram-node level-2 bp-row-bg" x="${ox}" y="${l.y + oy}" width="${layout.legendColumnWidth}" height="${l.height}" stroke="none"/>`);
|
|
106
72
|
inner.push(`<text class="text-primary" x="${ox + 12}" y="${l.y + oy + l.height / 2}" dominant-baseline="central">${escXml(l.label)}</text>`);
|
|
107
73
|
}
|
|
108
|
-
// Goal and result cells.
|
|
109
74
|
const textX = layout.cellTextPadX;
|
|
110
75
|
for (const c of layout.goalCells) {
|
|
111
76
|
inner.push(`<rect class="diagram-node level-3 bp-row-bg" x="${c.x + ox}" y="${c.y + oy}" width="${c.width}" height="${c.height}" stroke="none"/>`);
|
|
@@ -115,32 +80,22 @@ export function renderProcessBlueprintBody(layout, ox, oy, clipId = 'bp-clip') {
|
|
|
115
80
|
inner.push(`<rect class="diagram-node level-4 bp-row-bg" x="${c.x + ox}" y="${c.y + oy}" width="${c.width}" height="${c.height}" stroke="none"/>`);
|
|
116
81
|
inner.push(textCellSvg(c.lines, 'text-secondary', c.x + ox + textX, c.y + oy, c.height, layout.textLineHeight));
|
|
117
82
|
}
|
|
118
|
-
// Aspect rows — transparent background; pills carry the colour.
|
|
119
83
|
for (let r = 0; r < layout.aspectRows.length; r++) {
|
|
120
84
|
const row = layout.aspectRows[r];
|
|
121
85
|
const level = 5 + (r % 3);
|
|
122
86
|
inner.push(`<rect class="diagram-node level-${level} bp-row-bg" x="${layout.legendColumnWidth + ox}" y="${row.y + oy}" width="${tw - layout.legendColumnWidth}" height="${row.height}" stroke="none"/>`);
|
|
123
87
|
for (const p of row.pills) {
|
|
124
|
-
const cx = p.x + ox + p.width / 2;
|
|
125
|
-
const pillLH = 15;
|
|
126
|
-
// p.lines: name lines (possibly wrapped), optionally with id as last line.
|
|
127
88
|
const hasIdLine = !!(p.id && p.lines.length > 0 && p.lines[p.lines.length - 1] === p.id);
|
|
128
89
|
const nameLines = hasIdLine ? p.lines.slice(0, -1) : p.lines;
|
|
129
90
|
const idLine = hasIdLine ? p.id : undefined;
|
|
130
|
-
const
|
|
91
|
+
const pillSpecs = layoutCenteredPillText(p.x + ox, p.y + oy, p.width, p.height, nameLines, idLine);
|
|
131
92
|
topParts.push(`<rect class="diagram-node level-${level}" x="${p.x + ox}" y="${p.y + oy}" width="${p.width}" height="${p.height}" rx="6"/>`);
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
.join('');
|
|
136
|
-
topParts.push(`<text class="text-pill" text-anchor="middle" dominant-baseline="central">${tspans}</text>`);
|
|
137
|
-
}
|
|
138
|
-
if (idLine) {
|
|
139
|
-
topParts.push(`<text class="text-secondary" x="${cx}" y="${pillFirstY + nameLines.length * pillLH}" text-anchor="middle" dominant-baseline="central">${escXml(idLine)}</text>`);
|
|
93
|
+
for (const spec of pillSpecs) {
|
|
94
|
+
const anchor = spec.cls === 'text-pill' || spec.cls === 'text-secondary' ? ' text-anchor="middle"' : '';
|
|
95
|
+
topParts.push(`<text class="${spec.cls}" x="${spec.x}" y="${spec.y}"${anchor} dominant-baseline="central">${escXml(spec.text)}</text>`);
|
|
140
96
|
}
|
|
141
97
|
}
|
|
142
98
|
}
|
|
143
|
-
// Compliance row (optional).
|
|
144
99
|
if (layout.complianceRow) {
|
|
145
100
|
const row = layout.complianceRow;
|
|
146
101
|
inner.push(`<rect class="diagram-node level-5 bp-row-bg" x="${layout.legendColumnWidth + ox}" y="${row.y + oy}" width="${tw - layout.legendColumnWidth}" height="${row.height}" stroke="none"/>`);
|
|
@@ -149,7 +104,6 @@ export function renderProcessBlueprintBody(layout, ox, oy, clipId = 'bp-clip') {
|
|
|
149
104
|
topParts.push(complianceChipSvg(chip, ox, oy, stageId));
|
|
150
105
|
}
|
|
151
106
|
}
|
|
152
|
-
// Grid lines — drawn before pills so they appear under pill content.
|
|
153
107
|
const gridX1 = ox;
|
|
154
108
|
const gridX2 = ox + tw;
|
|
155
109
|
const gridY1 = oy;
|
|
@@ -168,15 +122,17 @@ export function renderProcessBlueprintBody(layout, ox, oy, clipId = 'bp-clip') {
|
|
|
168
122
|
const stageLineX = ox + layout.legendColumnWidth + i * layout.stageColumnWidth;
|
|
169
123
|
inner.push(`<line class="diagram-edge" x1="${stageLineX}" y1="${gridY1}" x2="${stageLineX}" y2="${gridY2}"/>`);
|
|
170
124
|
}
|
|
171
|
-
// Inner content (fills + grid lines) clipped to rounded outer rect.
|
|
172
125
|
parts.push(`<g clip-path="url(#${clipId})">${inner.join('\n')}${topParts.join('\n')}</g>`);
|
|
173
|
-
// Outer border on top — fill="none" as attribute so PNG export (no external CSS) never shows black.
|
|
174
126
|
parts.push(`<rect class="bp-border" x="${ox}" y="${oy}" width="${tw}" height="${th}" rx="6" fill="none"/>`);
|
|
175
127
|
return parts.join('\n');
|
|
176
128
|
}
|
|
177
129
|
export function renderProcessBlueprintSvg(doc, options = {}) {
|
|
178
|
-
const { title = '' } = options;
|
|
179
|
-
const
|
|
130
|
+
const { title = '', nodeSizePreset = 'normal', layoutOptions } = options;
|
|
131
|
+
const sizing = resolveProcessBlueprintSize(parseNodeSizePreset(nodeSizePreset));
|
|
132
|
+
const layout = layoutProcessBlueprint(doc, {
|
|
133
|
+
...sizing,
|
|
134
|
+
...layoutOptions,
|
|
135
|
+
});
|
|
180
136
|
if (layout.bounds.width === 0 || layout.bounds.height === 0) {
|
|
181
137
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
|
|
182
138
|
}
|
|
@@ -189,8 +145,6 @@ export function renderProcessBlueprintSvg(doc, options = {}) {
|
|
|
189
145
|
const titleSvg = title
|
|
190
146
|
? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(title)}</text>`
|
|
191
147
|
: '';
|
|
192
|
-
// Embed the shared theme CSS plus the blueprint-specific rules inside the SVG
|
|
193
|
-
// so the rendered output is self-contained for the JCEF host.
|
|
194
148
|
const embedCss = generateSvgEmbedCss('transitrix') + BP_EMBED_CSS;
|
|
195
149
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
|
|
196
150
|
<style>${embedCss}</style>
|
package/package.json
CHANGED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ACTION_NODE_SIZE,
|
|
5
|
+
BLOCKS_LEAF_SIZE,
|
|
6
|
+
CAPABILITY_MAP_NODE_SIZE,
|
|
7
|
+
DGCA_NODE_SIZE,
|
|
8
|
+
ENTITY_NODE_HEIGHT,
|
|
9
|
+
ENTITY_NODE_SIZE,
|
|
10
|
+
ENTITY_NODE_WIDTH,
|
|
11
|
+
GOALS_NODE_SIZE,
|
|
12
|
+
PROCESS_BLUEPRINT_SIZE,
|
|
13
|
+
resolveGoalsNodeSize,
|
|
14
|
+
} from '../node-size-presets.js';
|
|
15
|
+
|
|
16
|
+
describe('node-size-presets', () => {
|
|
17
|
+
it('uses shared width and height tiers for entity nodes', () => {
|
|
18
|
+
expect(ENTITY_NODE_WIDTH).toEqual({ compact: 200, normal: 250, wide: 320 });
|
|
19
|
+
expect(ENTITY_NODE_HEIGHT).toEqual({ compact: 72, normal: 80, wide: 96 });
|
|
20
|
+
expect(ENTITY_NODE_SIZE.normal).toEqual({ width: 250, height: 80 });
|
|
21
|
+
expect(ENTITY_NODE_SIZE.compact).toEqual({ width: 200, height: 72 });
|
|
22
|
+
expect(ENTITY_NODE_SIZE.wide).toEqual({ width: 320, height: 96 });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('exposes the same entity sizes for every box-based notation', () => {
|
|
26
|
+
for (const table of [GOALS_NODE_SIZE, DGCA_NODE_SIZE, BLOCKS_LEAF_SIZE, ACTION_NODE_SIZE]) {
|
|
27
|
+
expect(table).toBe(ENTITY_NODE_SIZE);
|
|
28
|
+
expect(table.normal).toEqual(ENTITY_NODE_SIZE.normal);
|
|
29
|
+
}
|
|
30
|
+
expect(CAPABILITY_MAP_NODE_SIZE.normal).toEqual({
|
|
31
|
+
nodeWidth: ENTITY_NODE_WIDTH.normal,
|
|
32
|
+
nodeHeight: ENTITY_NODE_HEIGHT.normal,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('scales wide above normal and compact below', () => {
|
|
37
|
+
expect(resolveGoalsNodeSize('wide').height).toBeGreaterThan(resolveGoalsNodeSize('normal').height);
|
|
38
|
+
expect(resolveGoalsNodeSize('compact').height).toBeLessThan(resolveGoalsNodeSize('normal').height);
|
|
39
|
+
expect(resolveGoalsNodeSize('wide').width).toBeGreaterThan(resolveGoalsNodeSize('normal').width);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('scales process blueprint from the same tier ratios', () => {
|
|
43
|
+
expect(PROCESS_BLUEPRINT_SIZE.wide.stageColumnWidth).toBeGreaterThan(
|
|
44
|
+
PROCESS_BLUEPRINT_SIZE.normal.stageColumnWidth,
|
|
45
|
+
);
|
|
46
|
+
expect(PROCESS_BLUEPRINT_SIZE.compact.goalRowHeight).toBeLessThan(
|
|
47
|
+
PROCESS_BLUEPRINT_SIZE.normal.goalRowHeight,
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
});
|
package/src/activities/layout.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import type { ActivityDoc, Activity, ActivitiesLayout, ActivitiesLayoutOptions, LayoutNode, LayoutEdge } from './types.js';
|
|
2
2
|
import { computeCpm } from './cpm.js';
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
const
|
|
4
|
+
const DEFAULT_NODE_W = 250;
|
|
5
|
+
const DEFAULT_NODE_H = 80;
|
|
6
6
|
const H_GAP = 80;
|
|
7
7
|
const V_GAP = 24;
|
|
8
8
|
|
|
9
9
|
export function layoutActivities(doc: ActivityDoc, options: ActivitiesLayoutOptions = {}): ActivitiesLayout {
|
|
10
|
-
const {
|
|
10
|
+
const {
|
|
11
|
+
horizontalGap = H_GAP,
|
|
12
|
+
verticalGap = V_GAP,
|
|
13
|
+
nodeWidth = DEFAULT_NODE_W,
|
|
14
|
+
nodeHeight = DEFAULT_NODE_H,
|
|
15
|
+
} = options;
|
|
11
16
|
const activities = doc.activities;
|
|
12
17
|
if (activities.length === 0) {
|
|
13
18
|
return { nodes: [], edges: [], bounds: { x: 0, y: 0, width: 0, height: 0 } };
|
|
@@ -70,21 +75,21 @@ export function layoutActivities(doc: ActivityDoc, options: ActivitiesLayoutOpti
|
|
|
70
75
|
|
|
71
76
|
for (let c = 0; c < colCount; c++) {
|
|
72
77
|
const list = cols.get(c) ?? [];
|
|
73
|
-
const x = c * (
|
|
78
|
+
const x = c * (nodeWidth + horizontalGap);
|
|
74
79
|
let y = 0;
|
|
75
80
|
for (const a of list) {
|
|
76
81
|
const node: LayoutNode = {
|
|
77
82
|
id: a.id,
|
|
78
83
|
x,
|
|
79
84
|
y,
|
|
80
|
-
width:
|
|
81
|
-
height:
|
|
85
|
+
width: nodeWidth,
|
|
86
|
+
height: nodeHeight,
|
|
82
87
|
data: a,
|
|
83
88
|
cpm: cpm.get(a.id),
|
|
84
89
|
};
|
|
85
90
|
nodes.push(node);
|
|
86
91
|
nodeMap.set(a.id, node);
|
|
87
|
-
y +=
|
|
92
|
+
y += nodeHeight + verticalGap;
|
|
88
93
|
}
|
|
89
94
|
}
|
|
90
95
|
|
package/src/activities/types.ts
CHANGED
|
@@ -100,6 +100,10 @@ export interface ActivitiesLayoutOptions {
|
|
|
100
100
|
horizontalGap?: number;
|
|
101
101
|
/** Vertical gap (px) between stacked activity nodes. Default matches the historical hardcoded value. */
|
|
102
102
|
verticalGap?: number;
|
|
103
|
+
/** Activity node width (px). Default 200. */
|
|
104
|
+
nodeWidth?: number;
|
|
105
|
+
/** Activity node height (px). Default 80. */
|
|
106
|
+
nodeHeight?: number;
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
// ── Gantt view (spec §9) ─────────────────────────────────────────────────────
|
|
@@ -118,8 +118,8 @@ describe('layoutNestedBlocks', () => {
|
|
|
118
118
|
},
|
|
119
119
|
});
|
|
120
120
|
const a = layout.blocks[0];
|
|
121
|
-
expect(a.width).toBe(
|
|
122
|
-
expect(a.height).toBe(
|
|
121
|
+
expect(a.width).toBe(250);
|
|
122
|
+
expect(a.height).toBe(80);
|
|
123
123
|
});
|
|
124
124
|
|
|
125
125
|
it('honours custom layout options', () => {
|
package/src/blocks/layout.ts
CHANGED
|
@@ -5,10 +5,11 @@ import type {
|
|
|
5
5
|
BlocksLayoutOptions,
|
|
6
6
|
LaidOutBlock,
|
|
7
7
|
} from './types.js';
|
|
8
|
+
import { ENTITY_NODE_SIZE } from '../node-size-presets.js';
|
|
8
9
|
|
|
9
10
|
const DEFAULTS: Required<BlocksLayoutOptions> = {
|
|
10
|
-
leafWidth:
|
|
11
|
-
leafHeight:
|
|
11
|
+
leafWidth: ENTITY_NODE_SIZE.normal.width,
|
|
12
|
+
leafHeight: ENTITY_NODE_SIZE.normal.height,
|
|
12
13
|
padding: 12,
|
|
13
14
|
headerHeight: 28,
|
|
14
15
|
childGap: 12,
|
|
@@ -2,7 +2,7 @@ import type { CapabilityMap, LayoutOptions, CapabilityMapLayout, LaidOutNode, La
|
|
|
2
2
|
import { getLevel, getParentAddress } from './address.js';
|
|
3
3
|
|
|
4
4
|
const DEFAULT_NODE_WIDTH = 250;
|
|
5
|
-
const DEFAULT_NODE_HEIGHT =
|
|
5
|
+
const DEFAULT_NODE_HEIGHT = 80;
|
|
6
6
|
const DEFAULT_RANK_SEP = 80;
|
|
7
7
|
const DEFAULT_NODE_SEP = 24;
|
|
8
8
|
/** Width of the virtual organisation-root node — narrower than a capability
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
|
|
2
|
+
// Types for the DSM-migration capability map (docs/internal/extraction/capability-map.md).
|
|
3
3
|
*
|
|
4
4
|
* Distinct from `./types.ts` (`CapabilityNode`/`CapabilityMapFile`), which is
|
|
5
5
|
* Transitrix's own native `*.capability-map.transitrix.yaml` notation — a
|
|
@@ -18,7 +18,7 @@ export { layoutCapabilityTree, TREE_NODE_WIDTH, TREE_NODE_HEIGHT, TREE_RANK_SEP,
|
|
|
18
18
|
export type { RenderCapabilityTreeOptions } from './render-capability-tree.js';
|
|
19
19
|
export { renderCapabilityTreeSvg } from './render-capability-tree.js';
|
|
20
20
|
|
|
21
|
-
// ── DSM-migration capability map (docs/extraction/capability-map.md) ───────
|
|
21
|
+
// ── DSM-migration capability map (docs/internal/extraction/capability-map.md) ───────
|
|
22
22
|
// A separate, flat, address-triple data model from CapabilityNode above —
|
|
23
23
|
// see dsm-schema.ts for why the two coexist in this folder. Named
|
|
24
24
|
// `validateCapabilityMapData` rather than `validateCapabilityMap` because
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CapabilityNode, CapabilityMapHeader } from './types.js';
|
|
2
2
|
|
|
3
3
|
export const TREE_NODE_WIDTH = 250;
|
|
4
|
-
export const TREE_NODE_HEIGHT =
|
|
4
|
+
export const TREE_NODE_HEIGHT = 80;
|
|
5
5
|
/** Gap between sibling columns (node width + rank sep = 330px, matching DSM). */
|
|
6
6
|
export const TREE_RANK_SEP = 80;
|
|
7
7
|
/** Vertical gap between sibling nodes. */
|
|
@@ -40,10 +40,18 @@ export interface CapabilityTreeLayout {
|
|
|
40
40
|
levelCounts: CapabilityTreeLevelCounts;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
export interface CapabilityTreeLayoutOptions {
|
|
44
|
+
nodeWidth?: number;
|
|
45
|
+
nodeHeight?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
43
48
|
export function layoutCapabilityTree(
|
|
44
49
|
map: CapabilityMapHeader,
|
|
45
50
|
collapsedIds: Set<string> = new Set(),
|
|
51
|
+
options: CapabilityTreeLayoutOptions = {},
|
|
46
52
|
): CapabilityTreeLayout {
|
|
53
|
+
const nodeWidth = options.nodeWidth ?? TREE_NODE_WIDTH;
|
|
54
|
+
const nodeHeight = options.nodeHeight ?? TREE_NODE_HEIGHT;
|
|
47
55
|
const nodes: CapabilityTreeNode[] = [];
|
|
48
56
|
const edges: CapabilityTreeEdge[] = [];
|
|
49
57
|
|
|
@@ -60,13 +68,13 @@ export function layoutCapabilityTree(
|
|
|
60
68
|
const hasChildren = Boolean(node.children?.length);
|
|
61
69
|
const visibleChildren = !isCollapsed && node.children?.length ? node.children : [];
|
|
62
70
|
|
|
63
|
-
const x = depth * (
|
|
71
|
+
const x = depth * (nodeWidth + TREE_RANK_SEP);
|
|
64
72
|
|
|
65
73
|
if (visibleChildren.length === 0) {
|
|
66
74
|
const y = getNextY(depth);
|
|
67
|
-
advanceY(depth,
|
|
68
|
-
nodes.push({ id: node.id, x, y, width:
|
|
69
|
-
return { top: y, bottom: y +
|
|
75
|
+
advanceY(depth, nodeHeight + TREE_NODE_SEP);
|
|
76
|
+
nodes.push({ id: node.id, x, y, width: nodeWidth, height: nodeHeight, data: node, depth, hasChildren, isCollapsed });
|
|
77
|
+
return { top: y, bottom: y + nodeHeight };
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
const childSpans: Array<{ top: number; bottom: number }> = [];
|
|
@@ -77,14 +85,14 @@ export function layoutCapabilityTree(
|
|
|
77
85
|
|
|
78
86
|
const spanTop = Math.min(...childSpans.map(s => s.top));
|
|
79
87
|
const spanBottom = Math.max(...childSpans.map(s => s.bottom));
|
|
80
|
-
const parentY = spanTop + (spanBottom - spanTop) / 2 -
|
|
88
|
+
const parentY = spanTop + (spanBottom - spanTop) / 2 - nodeHeight / 2;
|
|
81
89
|
|
|
82
90
|
const curColY = getNextY(depth);
|
|
83
91
|
const finalY = Math.max(parentY, curColY);
|
|
84
|
-
advanceY(depth, finalY - curColY +
|
|
92
|
+
advanceY(depth, finalY - curColY + nodeHeight + TREE_NODE_SEP);
|
|
85
93
|
|
|
86
|
-
nodes.push({ id: node.id, x, y: finalY, width:
|
|
87
|
-
return { top: Math.min(finalY, spanTop), bottom: Math.max(finalY +
|
|
94
|
+
nodes.push({ id: node.id, x, y: finalY, width: nodeWidth, height: nodeHeight, data: node, depth, hasChildren, isCollapsed });
|
|
95
|
+
return { top: Math.min(finalY, spanTop), bottom: Math.max(finalY + nodeHeight, spanBottom) };
|
|
88
96
|
}
|
|
89
97
|
|
|
90
98
|
for (const root of map.capabilities) {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { CapabilityMapHeader } from './types.js';
|
|
2
|
-
import { layoutCapabilityTree,
|
|
2
|
+
import { layoutCapabilityTree, TREE_NODE_SEP } from './layout-tree.js';
|
|
3
3
|
import { horizontalCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
|
|
4
|
+
import { parseNodeSizePreset, resolveCapabilityMapNodeSize, type NodeSizePreset } from '../node-size-presets.js';
|
|
5
|
+
import { maxCharsForInnerWidth, truncateLine } from '../webview/entity-text-layout.js';
|
|
4
6
|
import { escXml } from '../webview/render-util.js';
|
|
5
7
|
|
|
6
8
|
const PAD = 24;
|
|
7
9
|
const BTN_R = 8;
|
|
8
10
|
const LEGEND_H = 40;
|
|
9
|
-
const LABEL_MAX_CHARS = 28;
|
|
10
|
-
const LABEL_TRIM_CHARS = 26;
|
|
11
11
|
|
|
12
12
|
const BAND_LABELS = ['Levels 0–2', 'Levels 3–4', 'Levels 5+'] as const;
|
|
13
13
|
|
|
@@ -17,30 +17,19 @@ function depthBand(depth: number): 0 | 1 | 2 {
|
|
|
17
17
|
return 2;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function truncate(s: string, max: number, trim: number): string {
|
|
21
|
-
return s.length > max ? s.slice(0, trim) + '…' : s;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
20
|
export interface RenderCapabilityTreeOptions {
|
|
25
21
|
collapsedIds?: Set<string>;
|
|
26
22
|
curvature?: number;
|
|
23
|
+
nodeSizePreset?: NodeSizePreset;
|
|
27
24
|
}
|
|
28
25
|
|
|
29
|
-
/**
|
|
30
|
-
* Renders a capability-map as a left-to-right SVG node tree with DSM-matching
|
|
31
|
-
* visual design: depth-banded fills, maturity badges, and +/− collapse buttons.
|
|
32
|
-
*
|
|
33
|
-
* The SVG uses CSS classes (`tree-level-N`, `tree-maturity-N`, `tree-collapse-btn`)
|
|
34
|
-
* that must be resolved by the host's stylesheet (injected via `diagramClassCss()`
|
|
35
|
-
* in themes.ts). Collapse buttons carry `data-node-id` / `data-collapsed` attributes
|
|
36
|
-
* for the webview click handler.
|
|
37
|
-
*/
|
|
38
26
|
export function renderCapabilityTreeSvg(
|
|
39
27
|
map: CapabilityMapHeader,
|
|
40
28
|
opts: RenderCapabilityTreeOptions = {},
|
|
41
29
|
): string {
|
|
42
|
-
const { collapsedIds = new Set(), curvature = DEFAULT_EDGE_CURVATURE } = opts;
|
|
43
|
-
const
|
|
30
|
+
const { collapsedIds = new Set(), curvature = DEFAULT_EDGE_CURVATURE, nodeSizePreset = 'normal' } = opts;
|
|
31
|
+
const nodeSize = resolveCapabilityMapNodeSize(parseNodeSizePreset(nodeSizePreset));
|
|
32
|
+
const layout = layoutCapabilityTree(map, collapsedIds, nodeSize);
|
|
44
33
|
|
|
45
34
|
if (layout.nodes.length === 0) {
|
|
46
35
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
|
|
@@ -70,23 +59,22 @@ export function renderCapabilityTreeSvg(
|
|
|
70
59
|
const band = depthBand(n.depth);
|
|
71
60
|
const mat = Math.max(1, Math.min(5, n.data.current_maturity | 0));
|
|
72
61
|
|
|
73
|
-
const nameText = truncate(n.data.name, LABEL_MAX_CHARS, LABEL_TRIM_CHARS);
|
|
74
|
-
const nameTitle = n.data.name.length > LABEL_MAX_CHARS ? escXml(n.data.name) : '';
|
|
75
|
-
|
|
76
|
-
// Maturity badge: left side of node
|
|
77
62
|
const badgeW = 28;
|
|
78
63
|
const badgeH = 18;
|
|
64
|
+
const textAreaW = Math.max(0, n.width - badgeW - 26);
|
|
65
|
+
const maxChars = maxCharsForInnerWidth(textAreaW, 7);
|
|
66
|
+
const nameText = truncateLine(n.data.name, maxChars);
|
|
67
|
+
const nameTitle = n.data.name.length > maxChars ? escXml(n.data.name) : '';
|
|
68
|
+
|
|
79
69
|
const badgeX = x + 10;
|
|
80
|
-
const badgeY = y + (
|
|
70
|
+
const badgeY = y + (n.height - badgeH) / 2;
|
|
81
71
|
|
|
82
|
-
// Text area: right of badge
|
|
83
72
|
const textX = badgeX + badgeW + 8;
|
|
84
|
-
const nameY = y +
|
|
85
|
-
const idY = y +
|
|
73
|
+
const nameY = y + n.height / 2 - 8;
|
|
74
|
+
const idY = y + n.height / 2 + 9;
|
|
86
75
|
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
const btnCy = y + TREE_NODE_HEIGHT / 2;
|
|
76
|
+
const btnCx = x + n.width;
|
|
77
|
+
const btnCy = y + n.height / 2;
|
|
90
78
|
|
|
91
79
|
const collapseBtn = n.hasChildren
|
|
92
80
|
? `<circle class="tree-collapse-btn" cx="${btnCx}" cy="${btnCy}" r="${BTN_R}" data-node-id="${escXml(n.id)}" data-collapsed="${n.isCollapsed}"/>
|
|
@@ -94,7 +82,7 @@ export function renderCapabilityTreeSvg(
|
|
|
94
82
|
: '';
|
|
95
83
|
|
|
96
84
|
return `<g${nameTitle ? ` title="${nameTitle}"` : ''}>
|
|
97
|
-
<rect class="tree-level-${band}" x="${x}" y="${y}" width="${
|
|
85
|
+
<rect class="tree-level-${band}" x="${x}" y="${y}" width="${n.width}" height="${n.height}" rx="11" stroke="var(--ts-node-stroke,#94a3b8)" stroke-width="3"/>
|
|
98
86
|
<rect class="tree-maturity-${mat}" x="${badgeX}" y="${badgeY}" width="${badgeW}" height="${badgeH}" rx="4"/>
|
|
99
87
|
<text class="text-pill" x="${badgeX + badgeW / 2}" y="${badgeY + badgeH / 2}" text-anchor="middle" fill="white">L${mat}</text>
|
|
100
88
|
<text class="text-primary" x="${textX}" y="${nameY}" dominant-baseline="central">${escXml(nameText)}</text>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// Pure column-layout geometry for the static
|
|
1
|
+
// Pure column-layout geometry for the static DGCA / DGA previews.
|
|
2
2
|
//
|
|
3
|
-
// This is the layout the Studio extension's `
|
|
3
|
+
// This is the layout the Studio extension's `dgca-preview.ts` renders to SVG
|
|
4
4
|
// (NOT the ReactFlow `buildFGCALayout` in `./layout.ts`, which targets the
|
|
5
5
|
// interactive web UI). It lives here — rather than inline in the extension —
|
|
6
6
|
// so the gap geometry is unit-testable: the extension has no test harness.
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// No React, no DOM, no vscode. Safe to call in Node.js or a test environment.
|
|
9
9
|
|
|
10
10
|
import type { Scope } from '../scope.js';
|
|
11
|
+
import { ENTITY_NODE_SIZE } from '../node-size-presets.js';
|
|
11
12
|
|
|
12
13
|
export type FGCAPreviewColumn = 'driver' | 'goal' | 'change' | 'activity';
|
|
13
14
|
|
|
@@ -33,7 +34,7 @@ export interface FGCAPreviewActivity {
|
|
|
33
34
|
goal_id?: number | string | null;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
/** Structural input the preview layout needs — a subset of the parsed
|
|
37
|
+
/** Structural input the preview layout needs — a subset of the parsed DGCA/DGA doc. */
|
|
37
38
|
export interface FGCAPreviewDoc {
|
|
38
39
|
factors: FGCAPreviewFactor[];
|
|
39
40
|
goals: FGCAPreviewGoal[];
|
|
@@ -42,7 +43,7 @@ export interface FGCAPreviewDoc {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
export interface FGCAPreviewLayoutOptions {
|
|
45
|
-
/**
|
|
46
|
+
/** DGA hides the Changes column and links Goal → Activity directly. */
|
|
46
47
|
hideChanges?: boolean;
|
|
47
48
|
/** Horizontal gap (px) between columns. Default matches the historical hardcoded value. */
|
|
48
49
|
colGap?: number;
|
|
@@ -50,12 +51,16 @@ export interface FGCAPreviewLayoutOptions {
|
|
|
50
51
|
rowGap?: number;
|
|
51
52
|
/** Trim to a level cap or a single root goal (vkgeorgia/strategy#77). Defaults to 'all'. */
|
|
52
53
|
scope?: Scope;
|
|
54
|
+
/** Entity node width (px). Default {@link FGCA_NODE_W}. */
|
|
55
|
+
nodeWidth?: number;
|
|
56
|
+
/** Entity node height (px). Default {@link FGCA_NODE_H}. */
|
|
57
|
+
nodeHeight?: number;
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
/**
|
|
56
|
-
* Trims
|
|
61
|
+
* Trims a DGCA/DGA doc to a scope.
|
|
57
62
|
*
|
|
58
|
-
*
|
|
63
|
+
* DGCA/DGA goals are flat (no parent_id), so:
|
|
59
64
|
* - 'level' → goals with `(level ?? 0) <= maxLevel`.
|
|
60
65
|
* - 'root' → the single goal whose id matches `rootGoalId` (empty when absent).
|
|
61
66
|
*
|
|
@@ -119,10 +124,10 @@ export interface FGCAPreviewLayout {
|
|
|
119
124
|
height: number;
|
|
120
125
|
}
|
|
121
126
|
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
export const FGCA_NODE_W =
|
|
125
|
-
export const FGCA_NODE_H =
|
|
127
|
+
// Default node + frame geometry. Inter-node gaps are user-configurable via
|
|
128
|
+
// Inter-node gaps are user-configurable via spacing settings; node size uses presets.
|
|
129
|
+
export const FGCA_NODE_W = ENTITY_NODE_SIZE.normal.width;
|
|
130
|
+
export const FGCA_NODE_H = ENTITY_NODE_SIZE.normal.height;
|
|
126
131
|
export const FGCA_HEADER_H = 32;
|
|
127
132
|
export const FGCA_PAD = 20;
|
|
128
133
|
export const FGCA_DEFAULT_COL_GAP = 160;
|
|
@@ -137,12 +142,14 @@ export function layoutFGCAPreview(
|
|
|
137
142
|
colGap = FGCA_DEFAULT_COL_GAP,
|
|
138
143
|
rowGap = FGCA_DEFAULT_ROW_GAP,
|
|
139
144
|
scope = { mode: 'all' },
|
|
145
|
+
nodeWidth = FGCA_NODE_W,
|
|
146
|
+
nodeHeight = FGCA_NODE_H,
|
|
140
147
|
} = options;
|
|
141
148
|
|
|
142
149
|
// Trim to scope first; everything below lays out the visible subset only.
|
|
143
150
|
const doc = selectScopedFGCA(inputDoc, scope);
|
|
144
151
|
|
|
145
|
-
const colStride =
|
|
152
|
+
const colStride = nodeWidth + colGap;
|
|
146
153
|
const cols: FGCAPreviewColumn[] = hideChanges
|
|
147
154
|
? ['driver', 'goal', 'activity']
|
|
148
155
|
: ['driver', 'goal', 'change', 'activity'];
|
|
@@ -206,7 +213,7 @@ export function layoutFGCAPreview(
|
|
|
206
213
|
const nodeMap = new Map<string, FGCAPreviewNode>();
|
|
207
214
|
const nodes: FGCAPreviewNode[] = [];
|
|
208
215
|
const columns: FGCAPreviewColumnPos[] = [];
|
|
209
|
-
const yCenters = new Map<string, number>(); // nodeId → y +
|
|
216
|
+
const yCenters = new Map<string, number>(); // nodeId → y + nodeHeight / 2
|
|
210
217
|
|
|
211
218
|
for (let ci = 0; ci < cols.length; ci++) {
|
|
212
219
|
const col = cols[ci];
|
|
@@ -218,8 +225,8 @@ export function layoutFGCAPreview(
|
|
|
218
225
|
const node: FGCAPreviewNode = { id: item.id, x, y, label: item.label, col };
|
|
219
226
|
nodes.push(node);
|
|
220
227
|
nodeMap.set(item.id, node);
|
|
221
|
-
yCenters.set(item.id, y +
|
|
222
|
-
y +=
|
|
228
|
+
yCenters.set(item.id, y + nodeHeight / 2);
|
|
229
|
+
y += nodeHeight + rowGap;
|
|
223
230
|
}
|
|
224
231
|
}
|
|
225
232
|
|
|
@@ -228,7 +235,7 @@ export function layoutFGCAPreview(
|
|
|
228
235
|
const s = nodeMap.get(sourceId);
|
|
229
236
|
const t = nodeMap.get(targetId);
|
|
230
237
|
if (!s || !t) return;
|
|
231
|
-
edges.push({ sx: s.x +
|
|
238
|
+
edges.push({ sx: s.x + nodeWidth, sy: s.y + nodeHeight / 2, tx: t.x, ty: t.y + nodeHeight / 2 });
|
|
232
239
|
}
|
|
233
240
|
|
|
234
241
|
for (const g of doc.goals) {
|
|
@@ -259,8 +266,8 @@ export function layoutFGCAPreview(
|
|
|
259
266
|
}
|
|
260
267
|
|
|
261
268
|
const maxNodeBottom = nodes.reduce(
|
|
262
|
-
(m, n) => Math.max(m, n.y +
|
|
263
|
-
FGCA_PAD + FGCA_HEADER_H + rowGap +
|
|
269
|
+
(m, n) => Math.max(m, n.y + nodeHeight),
|
|
270
|
+
FGCA_PAD + FGCA_HEADER_H + rowGap + nodeHeight,
|
|
264
271
|
);
|
|
265
272
|
const width = FGCA_PAD * 2 + cols.length * colStride - colGap;
|
|
266
273
|
const height = maxNodeBottom + FGCA_PAD;
|