@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
@@ -1,10 +1,10 @@
1
1
  import { computeCpm } from './cpm.js';
2
- const NODE_W = 200;
3
- const NODE_H = 80;
2
+ const DEFAULT_NODE_W = 200;
3
+ const DEFAULT_NODE_H = 80;
4
4
  const H_GAP = 80;
5
5
  const V_GAP = 24;
6
6
  export function layoutActivities(doc, options = {}) {
7
- const { horizontalGap = H_GAP, verticalGap = V_GAP } = options;
7
+ const { horizontalGap = H_GAP, verticalGap = V_GAP, nodeWidth = DEFAULT_NODE_W, nodeHeight = DEFAULT_NODE_H, } = options;
8
8
  const activities = doc.activities;
9
9
  if (activities.length === 0) {
10
10
  return { nodes: [], edges: [], bounds: { x: 0, y: 0, width: 0, height: 0 } };
@@ -64,21 +64,21 @@ export function layoutActivities(doc, options = {}) {
64
64
  const colCount = Math.max(0, ...[...cols.keys()]) + 1;
65
65
  for (let c = 0; c < colCount; c++) {
66
66
  const list = cols.get(c) ?? [];
67
- const x = c * (NODE_W + horizontalGap);
67
+ const x = c * (nodeWidth + horizontalGap);
68
68
  let y = 0;
69
69
  for (const a of list) {
70
70
  const node = {
71
71
  id: a.id,
72
72
  x,
73
73
  y,
74
- width: NODE_W,
75
- height: NODE_H,
74
+ width: nodeWidth,
75
+ height: nodeHeight,
76
76
  data: a,
77
77
  cpm: cpm.get(a.id),
78
78
  };
79
79
  nodes.push(node);
80
80
  nodeMap.set(a.id, node);
81
- y += NODE_H + verticalGap;
81
+ y += nodeHeight + verticalGap;
82
82
  }
83
83
  }
84
84
  // Build edges
@@ -86,6 +86,10 @@ export interface ActivitiesLayoutOptions {
86
86
  horizontalGap?: number;
87
87
  /** Vertical gap (px) between stacked activity nodes. Default matches the historical hardcoded value. */
88
88
  verticalGap?: number;
89
+ /** Activity node width (px). Default 200. */
90
+ nodeWidth?: number;
91
+ /** Activity node height (px). Default 80. */
92
+ nodeHeight?: number;
89
93
  }
90
94
  export type GanttMode = 'computed' | 'pinned';
91
95
  export type GanttBarKind = 'leaf' | 'milestone' | 'phase';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Types for the DSM-migration capability map (docs/extraction/capability-map.md).
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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Types for the DSM-migration capability map (docs/extraction/capability-map.md).
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
@@ -39,4 +39,8 @@ export interface CapabilityTreeLayout {
39
39
  };
40
40
  levelCounts: CapabilityTreeLevelCounts;
41
41
  }
42
- export declare function layoutCapabilityTree(map: CapabilityMapHeader, collapsedIds?: Set<string>): CapabilityTreeLayout;
42
+ export interface CapabilityTreeLayoutOptions {
43
+ nodeWidth?: number;
44
+ nodeHeight?: number;
45
+ }
46
+ export declare function layoutCapabilityTree(map: CapabilityMapHeader, collapsedIds?: Set<string>, options?: CapabilityTreeLayoutOptions): CapabilityTreeLayout;
@@ -4,7 +4,9 @@ export const TREE_NODE_HEIGHT = 64;
4
4
  export const TREE_RANK_SEP = 80;
5
5
  /** Vertical gap between sibling nodes. */
6
6
  export const TREE_NODE_SEP = 24;
7
- export function layoutCapabilityTree(map, collapsedIds = new Set()) {
7
+ export function layoutCapabilityTree(map, collapsedIds = new Set(), options = {}) {
8
+ const nodeWidth = options.nodeWidth ?? TREE_NODE_WIDTH;
9
+ const nodeHeight = options.nodeHeight ?? TREE_NODE_HEIGHT;
8
10
  const nodes = [];
9
11
  const edges = [];
10
12
  const colNextY = new Map();
@@ -18,12 +20,12 @@ export function layoutCapabilityTree(map, collapsedIds = new Set()) {
18
20
  const isCollapsed = collapsedIds.has(node.id);
19
21
  const hasChildren = Boolean(node.children?.length);
20
22
  const visibleChildren = !isCollapsed && node.children?.length ? node.children : [];
21
- const x = depth * (TREE_NODE_WIDTH + TREE_RANK_SEP);
23
+ const x = depth * (nodeWidth + TREE_RANK_SEP);
22
24
  if (visibleChildren.length === 0) {
23
25
  const y = getNextY(depth);
24
- advanceY(depth, TREE_NODE_HEIGHT + TREE_NODE_SEP);
25
- nodes.push({ id: node.id, x, y, width: TREE_NODE_WIDTH, height: TREE_NODE_HEIGHT, data: node, depth, hasChildren, isCollapsed });
26
- return { top: y, bottom: y + TREE_NODE_HEIGHT };
26
+ advanceY(depth, nodeHeight + TREE_NODE_SEP);
27
+ nodes.push({ id: node.id, x, y, width: nodeWidth, height: nodeHeight, data: node, depth, hasChildren, isCollapsed });
28
+ return { top: y, bottom: y + nodeHeight };
27
29
  }
28
30
  const childSpans = [];
29
31
  for (const child of visibleChildren) {
@@ -32,12 +34,12 @@ export function layoutCapabilityTree(map, collapsedIds = new Set()) {
32
34
  }
33
35
  const spanTop = Math.min(...childSpans.map(s => s.top));
34
36
  const spanBottom = Math.max(...childSpans.map(s => s.bottom));
35
- const parentY = spanTop + (spanBottom - spanTop) / 2 - TREE_NODE_HEIGHT / 2;
37
+ const parentY = spanTop + (spanBottom - spanTop) / 2 - nodeHeight / 2;
36
38
  const curColY = getNextY(depth);
37
39
  const finalY = Math.max(parentY, curColY);
38
- advanceY(depth, finalY - curColY + TREE_NODE_HEIGHT + TREE_NODE_SEP);
39
- nodes.push({ id: node.id, x, y: finalY, width: TREE_NODE_WIDTH, height: TREE_NODE_HEIGHT, data: node, depth, hasChildren, isCollapsed });
40
- return { top: Math.min(finalY, spanTop), bottom: Math.max(finalY + TREE_NODE_HEIGHT, spanBottom) };
40
+ advanceY(depth, finalY - curColY + nodeHeight + TREE_NODE_SEP);
41
+ nodes.push({ id: node.id, x, y: finalY, width: nodeWidth, height: nodeHeight, data: node, depth, hasChildren, isCollapsed });
42
+ return { top: Math.min(finalY, spanTop), bottom: Math.max(finalY + nodeHeight, spanBottom) };
41
43
  }
42
44
  for (const root of map.capabilities) {
43
45
  placeNode(root, 0);
@@ -1,15 +1,8 @@
1
1
  import type { CapabilityMapHeader } from './types.js';
2
+ import { type NodeSizePreset } from '../node-size-presets.js';
2
3
  export interface RenderCapabilityTreeOptions {
3
4
  collapsedIds?: Set<string>;
4
5
  curvature?: number;
6
+ nodeSizePreset?: NodeSizePreset;
5
7
  }
6
- /**
7
- * Renders a capability-map as a left-to-right SVG node tree with DSM-matching
8
- * visual design: depth-banded fills, maturity badges, and +/− collapse buttons.
9
- *
10
- * The SVG uses CSS classes (`tree-level-N`, `tree-maturity-N`, `tree-collapse-btn`)
11
- * that must be resolved by the host's stylesheet (injected via `diagramClassCss()`
12
- * in themes.ts). Collapse buttons carry `data-node-id` / `data-collapsed` attributes
13
- * for the webview click handler.
14
- */
15
8
  export declare function renderCapabilityTreeSvg(map: CapabilityMapHeader, opts?: RenderCapabilityTreeOptions): string;
@@ -1,11 +1,11 @@
1
- import { layoutCapabilityTree, TREE_NODE_WIDTH, TREE_NODE_HEIGHT, TREE_NODE_SEP } from './layout-tree.js';
1
+ import { layoutCapabilityTree, TREE_NODE_SEP } from './layout-tree.js';
2
2
  import { horizontalCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
3
+ import { parseNodeSizePreset, resolveCapabilityMapNodeSize } from '../node-size-presets.js';
4
+ import { maxCharsForInnerWidth, truncateLine } from '../webview/entity-text-layout.js';
3
5
  import { escXml } from '../webview/render-util.js';
4
6
  const PAD = 24;
5
7
  const BTN_R = 8;
6
8
  const LEGEND_H = 40;
7
- const LABEL_MAX_CHARS = 28;
8
- const LABEL_TRIM_CHARS = 26;
9
9
  const BAND_LABELS = ['Levels 0–2', 'Levels 3–4', 'Levels 5+'];
10
10
  function depthBand(depth) {
11
11
  if (depth <= 2)
@@ -14,21 +14,10 @@ function depthBand(depth) {
14
14
  return 1;
15
15
  return 2;
16
16
  }
17
- function truncate(s, max, trim) {
18
- return s.length > max ? s.slice(0, trim) + '…' : s;
19
- }
20
- /**
21
- * Renders a capability-map as a left-to-right SVG node tree with DSM-matching
22
- * visual design: depth-banded fills, maturity badges, and +/− collapse buttons.
23
- *
24
- * The SVG uses CSS classes (`tree-level-N`, `tree-maturity-N`, `tree-collapse-btn`)
25
- * that must be resolved by the host's stylesheet (injected via `diagramClassCss()`
26
- * in themes.ts). Collapse buttons carry `data-node-id` / `data-collapsed` attributes
27
- * for the webview click handler.
28
- */
29
17
  export function renderCapabilityTreeSvg(map, opts = {}) {
30
- const { collapsedIds = new Set(), curvature = DEFAULT_EDGE_CURVATURE } = opts;
31
- const layout = layoutCapabilityTree(map, collapsedIds);
18
+ const { collapsedIds = new Set(), curvature = DEFAULT_EDGE_CURVATURE, nodeSizePreset = 'normal' } = opts;
19
+ const nodeSize = resolveCapabilityMapNodeSize(parseNodeSizePreset(nodeSizePreset));
20
+ const layout = layoutCapabilityTree(map, collapsedIds, nodeSize);
32
21
  if (layout.nodes.length === 0) {
33
22
  return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
34
23
  }
@@ -53,26 +42,25 @@ export function renderCapabilityTreeSvg(map, opts = {}) {
53
42
  const y = n.y + oy;
54
43
  const band = depthBand(n.depth);
55
44
  const mat = Math.max(1, Math.min(5, n.data.current_maturity | 0));
56
- const nameText = truncate(n.data.name, LABEL_MAX_CHARS, LABEL_TRIM_CHARS);
57
- const nameTitle = n.data.name.length > LABEL_MAX_CHARS ? escXml(n.data.name) : '';
58
- // Maturity badge: left side of node
59
45
  const badgeW = 28;
60
46
  const badgeH = 18;
47
+ const textAreaW = Math.max(0, n.width - badgeW - 26);
48
+ const maxChars = maxCharsForInnerWidth(textAreaW, 7);
49
+ const nameText = truncateLine(n.data.name, maxChars);
50
+ const nameTitle = n.data.name.length > maxChars ? escXml(n.data.name) : '';
61
51
  const badgeX = x + 10;
62
- const badgeY = y + (TREE_NODE_HEIGHT - badgeH) / 2;
63
- // Text area: right of badge
52
+ const badgeY = y + (n.height - badgeH) / 2;
64
53
  const textX = badgeX + badgeW + 8;
65
- const nameY = y + TREE_NODE_HEIGHT / 2 - 8;
66
- const idY = y + TREE_NODE_HEIGHT / 2 + 9;
67
- // Collapse button: centered on right edge of node
68
- const btnCx = x + TREE_NODE_WIDTH;
69
- const btnCy = y + TREE_NODE_HEIGHT / 2;
54
+ const nameY = y + n.height / 2 - 8;
55
+ const idY = y + n.height / 2 + 9;
56
+ const btnCx = x + n.width;
57
+ const btnCy = y + n.height / 2;
70
58
  const collapseBtn = n.hasChildren
71
59
  ? `<circle class="tree-collapse-btn" cx="${btnCx}" cy="${btnCy}" r="${BTN_R}" data-node-id="${escXml(n.id)}" data-collapsed="${n.isCollapsed}"/>
72
60
  <text class="tree-collapse-lbl" x="${btnCx}" y="${btnCy}" text-anchor="middle">${n.isCollapsed ? '+' : '−'}</text>`
73
61
  : '';
74
62
  return `<g${nameTitle ? ` title="${nameTitle}"` : ''}>
75
- <rect class="tree-level-${band}" x="${x}" y="${y}" width="${TREE_NODE_WIDTH}" height="${TREE_NODE_HEIGHT}" rx="11" stroke="var(--ts-node-stroke,#94a3b8)" stroke-width="3"/>
63
+ <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"/>
76
64
  <rect class="tree-maturity-${mat}" x="${badgeX}" y="${badgeY}" width="${badgeW}" height="${badgeH}" rx="4"/>
77
65
  <text class="text-pill" x="${badgeX + badgeW / 2}" y="${badgeY + badgeH / 2}" text-anchor="middle" fill="white">L${mat}</text>
78
66
  <text class="text-primary" x="${textX}" y="${nameY}" dominant-baseline="central">${escXml(nameText)}</text>
@@ -23,7 +23,7 @@ export interface FGCAPreviewActivity {
23
23
  name: string;
24
24
  goal_id?: number | string | null;
25
25
  }
26
- /** Structural input the preview layout needs — a subset of the parsed FGCA/FGA doc. */
26
+ /** Structural input the preview layout needs — a subset of the parsed DGCA/DGA doc. */
27
27
  export interface FGCAPreviewDoc {
28
28
  factors: FGCAPreviewFactor[];
29
29
  goals: FGCAPreviewGoal[];
@@ -31,7 +31,7 @@ export interface FGCAPreviewDoc {
31
31
  activities: FGCAPreviewActivity[];
32
32
  }
33
33
  export interface FGCAPreviewLayoutOptions {
34
- /** FGA hides the Changes column and links Goal → Activity directly. */
34
+ /** DGA hides the Changes column and links Goal → Activity directly. */
35
35
  hideChanges?: boolean;
36
36
  /** Horizontal gap (px) between columns. Default matches the historical hardcoded value. */
37
37
  colGap?: number;
@@ -39,11 +39,15 @@ export interface FGCAPreviewLayoutOptions {
39
39
  rowGap?: number;
40
40
  /** Trim to a level cap or a single root goal (vkgeorgia/strategy#77). Defaults to 'all'. */
41
41
  scope?: Scope;
42
+ /** Entity node width (px). Default {@link FGCA_NODE_W}. */
43
+ nodeWidth?: number;
44
+ /** Entity node height (px). Default {@link FGCA_NODE_H}. */
45
+ nodeHeight?: number;
42
46
  }
43
47
  /**
44
- * Trims an FGCA/FGA doc to a scope (vkgeorgia/strategy#77).
48
+ * Trims a DGCA/DGA doc to a scope.
45
49
  *
46
- * FGCA/FGA goals are flat (no parent_id), so:
50
+ * DGCA/DGA goals are flat (no parent_id), so:
47
51
  * - 'level' → goals with `(level ?? 0) <= maxLevel`.
48
52
  * - 'root' → the single goal whose id matches `rootGoalId` (empty when absent).
49
53
  *
@@ -1,15 +1,15 @@
1
- // Pure column-layout geometry for the static FGCA / FGA previews.
1
+ // Pure column-layout geometry for the static DGCA / DGA previews.
2
2
  //
3
- // This is the layout the Studio extension's `fgca-preview.ts` renders to SVG
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.
7
7
  //
8
8
  // No React, no DOM, no vscode. Safe to call in Node.js or a test environment.
9
9
  /**
10
- * Trims an FGCA/FGA doc to a scope (vkgeorgia/strategy#77).
10
+ * Trims a DGCA/DGA doc to a scope.
11
11
  *
12
- * FGCA/FGA goals are flat (no parent_id), so:
12
+ * DGCA/DGA goals are flat (no parent_id), so:
13
13
  * - 'level' → goals with `(level ?? 0) <= maxLevel`.
14
14
  * - 'root' → the single goal whose id matches `rootGoalId` (empty when absent).
15
15
  *
@@ -38,8 +38,8 @@ export function selectScopedFGCA(doc, scope) {
38
38
  activities: visibleActivities,
39
39
  };
40
40
  }
41
- // Fixed node + frame geometry. Only the inter-node gaps are user-configurable
42
- // (vkgeorgia/strategy#75); node size and padding stay constant.
41
+ // Default node + frame geometry. Inter-node gaps are user-configurable via
42
+ // Inter-node gaps are user-configurable via spacing settings; node size uses presets.
43
43
  export const FGCA_NODE_W = 220;
44
44
  export const FGCA_NODE_H = 72;
45
45
  export const FGCA_HEADER_H = 32;
@@ -47,10 +47,10 @@ export const FGCA_PAD = 20;
47
47
  export const FGCA_DEFAULT_COL_GAP = 160;
48
48
  export const FGCA_DEFAULT_ROW_GAP = 20;
49
49
  export function layoutFGCAPreview(inputDoc, options = {}) {
50
- const { hideChanges = false, colGap = FGCA_DEFAULT_COL_GAP, rowGap = FGCA_DEFAULT_ROW_GAP, scope = { mode: 'all' }, } = options;
50
+ const { hideChanges = false, colGap = FGCA_DEFAULT_COL_GAP, rowGap = FGCA_DEFAULT_ROW_GAP, scope = { mode: 'all' }, nodeWidth = FGCA_NODE_W, nodeHeight = FGCA_NODE_H, } = options;
51
51
  // Trim to scope first; everything below lays out the visible subset only.
52
52
  const doc = selectScopedFGCA(inputDoc, scope);
53
- const colStride = FGCA_NODE_W + colGap;
53
+ const colStride = nodeWidth + colGap;
54
54
  const cols = hideChanges
55
55
  ? ['driver', 'goal', 'activity']
56
56
  : ['driver', 'goal', 'change', 'activity'];
@@ -110,7 +110,7 @@ export function layoutFGCAPreview(inputDoc, options = {}) {
110
110
  const nodeMap = new Map();
111
111
  const nodes = [];
112
112
  const columns = [];
113
- const yCenters = new Map(); // nodeId → y + FGCA_NODE_H / 2
113
+ const yCenters = new Map(); // nodeId → y + nodeHeight / 2
114
114
  for (let ci = 0; ci < cols.length; ci++) {
115
115
  const col = cols[ci];
116
116
  const x = FGCA_PAD + ci * colStride;
@@ -121,8 +121,8 @@ export function layoutFGCAPreview(inputDoc, options = {}) {
121
121
  const node = { id: item.id, x, y, label: item.label, col };
122
122
  nodes.push(node);
123
123
  nodeMap.set(item.id, node);
124
- yCenters.set(item.id, y + FGCA_NODE_H / 2);
125
- y += FGCA_NODE_H + rowGap;
124
+ yCenters.set(item.id, y + nodeHeight / 2);
125
+ y += nodeHeight + rowGap;
126
126
  }
127
127
  }
128
128
  const edges = [];
@@ -131,7 +131,7 @@ export function layoutFGCAPreview(inputDoc, options = {}) {
131
131
  const t = nodeMap.get(targetId);
132
132
  if (!s || !t)
133
133
  return;
134
- edges.push({ sx: s.x + FGCA_NODE_W, sy: s.y + FGCA_NODE_H / 2, tx: t.x, ty: t.y + FGCA_NODE_H / 2 });
134
+ edges.push({ sx: s.x + nodeWidth, sy: s.y + nodeHeight / 2, tx: t.x, ty: t.y + nodeHeight / 2 });
135
135
  }
136
136
  for (const g of doc.goals) {
137
137
  for (const f of (g.factor ?? []))
@@ -164,7 +164,7 @@ export function layoutFGCAPreview(inputDoc, options = {}) {
164
164
  }
165
165
  }
166
166
  }
167
- const maxNodeBottom = nodes.reduce((m, n) => Math.max(m, n.y + FGCA_NODE_H), FGCA_PAD + FGCA_HEADER_H + rowGap + FGCA_NODE_H);
167
+ const maxNodeBottom = nodes.reduce((m, n) => Math.max(m, n.y + nodeHeight), FGCA_PAD + FGCA_HEADER_H + rowGap + nodeHeight);
168
168
  const width = FGCA_PAD * 2 + cols.length * colStride - colGap;
169
169
  const height = maxNodeBottom + FGCA_PAD;
170
170
  return { nodes, edges, columns, width, height };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Block/cell size presets for diagram renderers.
3
+ *
4
+ * Presets drive both layout geometry and shared text-layout char budgets.
5
+ * Smooth per-pixel sliders are intentionally deferred.
6
+ */
7
+ export type NodeSizePreset = 'compact' | 'normal' | 'wide';
8
+ export declare const NODE_SIZE_PRESET_VALUES: readonly NodeSizePreset[];
9
+ export interface BoxDimensions {
10
+ width: number;
11
+ height: number;
12
+ }
13
+ export interface BlocksLeafDimensions extends BoxDimensions {
14
+ }
15
+ export interface ProcessBlueprintDimensions {
16
+ legendColumnWidth: number;
17
+ stageColumnWidth: number;
18
+ stageHeaderHeight: number;
19
+ goalRowHeight: number;
20
+ resultRowHeight: number;
21
+ aspectRowMinHeight: number;
22
+ pillHeight: number;
23
+ pillGap: number;
24
+ cellPadding: number;
25
+ textLineHeight: number;
26
+ textCharWidth: number;
27
+ cellTextPadX: number;
28
+ cellTextPadY: number;
29
+ }
30
+ export interface CapabilityMapDimensions {
31
+ nodeWidth: number;
32
+ nodeHeight: number;
33
+ }
34
+ type PresetTable<T> = Record<NodeSizePreset, T>;
35
+ export declare const GOALS_NODE_SIZE: PresetTable<BoxDimensions>;
36
+ export declare const DGCA_NODE_SIZE: PresetTable<BoxDimensions>;
37
+ export declare const BLOCKS_LEAF_SIZE: PresetTable<BlocksLeafDimensions>;
38
+ export declare const ACTION_NODE_SIZE: PresetTable<BoxDimensions>;
39
+ export declare const PROCESS_BLUEPRINT_SIZE: PresetTable<ProcessBlueprintDimensions>;
40
+ export declare const CAPABILITY_MAP_NODE_SIZE: PresetTable<CapabilityMapDimensions>;
41
+ export type NodeSizeNotation = 'goals' | 'dgca' | 'dga' | 'blocks' | 'action' | 'processBlueprint' | 'capabilityMap';
42
+ /** Parse a VS Code setting value into a preset id (defaults to `normal`). */
43
+ export declare function parseNodeSizePreset(value: string | undefined): NodeSizePreset;
44
+ export declare function resolveGoalsNodeSize(preset: NodeSizePreset): BoxDimensions;
45
+ export declare function resolveDgcaNodeSize(preset: NodeSizePreset): BoxDimensions;
46
+ export declare function resolveBlocksLeafSize(preset: NodeSizePreset): BlocksLeafDimensions;
47
+ export declare function resolveActionNodeSize(preset: NodeSizePreset): BoxDimensions;
48
+ export declare function resolveProcessBlueprintSize(preset: NodeSizePreset): ProcessBlueprintDimensions;
49
+ export declare function resolveCapabilityMapNodeSize(preset: NodeSizePreset): CapabilityMapDimensions;
50
+ export {};
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Block/cell size presets for diagram renderers.
3
+ *
4
+ * Presets drive both layout geometry and shared text-layout char budgets.
5
+ * Smooth per-pixel sliders are intentionally deferred.
6
+ */
7
+ export const NODE_SIZE_PRESET_VALUES = ['compact', 'normal', 'wide'];
8
+ export const GOALS_NODE_SIZE = {
9
+ compact: { width: 200, height: 72 },
10
+ normal: { width: 250, height: 72 },
11
+ wide: { width: 320, height: 72 },
12
+ };
13
+ export const DGCA_NODE_SIZE = {
14
+ compact: { width: 200, height: 72 },
15
+ normal: { width: 220, height: 72 },
16
+ wide: { width: 280, height: 72 },
17
+ };
18
+ export const BLOCKS_LEAF_SIZE = {
19
+ compact: { width: 140, height: 72 },
20
+ normal: { width: 160, height: 72 },
21
+ wide: { width: 200, height: 72 },
22
+ };
23
+ export const ACTION_NODE_SIZE = {
24
+ compact: { width: 180, height: 72 },
25
+ normal: { width: 200, height: 80 },
26
+ wide: { width: 260, height: 80 },
27
+ };
28
+ export const PROCESS_BLUEPRINT_SIZE = {
29
+ compact: {
30
+ legendColumnWidth: 120,
31
+ stageColumnWidth: 180,
32
+ stageHeaderHeight: 36,
33
+ goalRowHeight: 52,
34
+ resultRowHeight: 52,
35
+ aspectRowMinHeight: 56,
36
+ pillHeight: 36,
37
+ pillGap: 4,
38
+ cellPadding: 8,
39
+ textLineHeight: 16,
40
+ textCharWidth: 7.5,
41
+ cellTextPadX: 10,
42
+ cellTextPadY: 10,
43
+ },
44
+ normal: {
45
+ legendColumnWidth: 140,
46
+ stageColumnWidth: 220,
47
+ stageHeaderHeight: 40,
48
+ goalRowHeight: 56,
49
+ resultRowHeight: 56,
50
+ aspectRowMinHeight: 60,
51
+ pillHeight: 40,
52
+ pillGap: 4,
53
+ cellPadding: 8,
54
+ textLineHeight: 17,
55
+ textCharWidth: 7.5,
56
+ cellTextPadX: 10,
57
+ cellTextPadY: 10,
58
+ },
59
+ wide: {
60
+ legendColumnWidth: 160,
61
+ stageColumnWidth: 280,
62
+ stageHeaderHeight: 44,
63
+ goalRowHeight: 60,
64
+ resultRowHeight: 60,
65
+ aspectRowMinHeight: 64,
66
+ pillHeight: 44,
67
+ pillGap: 4,
68
+ cellPadding: 8,
69
+ textLineHeight: 18,
70
+ textCharWidth: 7.5,
71
+ cellTextPadX: 10,
72
+ cellTextPadY: 10,
73
+ },
74
+ };
75
+ export const CAPABILITY_MAP_NODE_SIZE = {
76
+ compact: { nodeWidth: 200, nodeHeight: 56 },
77
+ normal: { nodeWidth: 240, nodeHeight: 60 },
78
+ wide: { nodeWidth: 300, nodeHeight: 64 },
79
+ };
80
+ /** Parse a VS Code setting value into a preset id (defaults to `normal`). */
81
+ export function parseNodeSizePreset(value) {
82
+ if (value === 'compact' || value === 'wide')
83
+ return value;
84
+ return 'normal';
85
+ }
86
+ export function resolveGoalsNodeSize(preset) {
87
+ return GOALS_NODE_SIZE[preset];
88
+ }
89
+ export function resolveDgcaNodeSize(preset) {
90
+ return DGCA_NODE_SIZE[preset];
91
+ }
92
+ export function resolveBlocksLeafSize(preset) {
93
+ return BLOCKS_LEAF_SIZE[preset];
94
+ }
95
+ export function resolveActionNodeSize(preset) {
96
+ return ACTION_NODE_SIZE[preset];
97
+ }
98
+ export function resolveProcessBlueprintSize(preset) {
99
+ return PROCESS_BLUEPRINT_SIZE[preset];
100
+ }
101
+ export function resolveCapabilityMapNodeSize(preset) {
102
+ return CAPABILITY_MAP_NODE_SIZE[preset];
103
+ }
@@ -1,3 +1,4 @@
1
+ import { wrapTextLines } from '../webview/entity-text-layout.js';
1
2
  const ASPECT_CATEGORIES = ['systems', 'actors', 'equipment', 'information_entities'];
2
3
  const ASPECT_LABEL = {
3
4
  systems: 'Systems',
@@ -30,55 +31,11 @@ function resolveOptions(options) {
30
31
  return { ...DEFAULTS, ...(options ?? {}) };
31
32
  }
32
33
  /**
33
- * Greedy word-wrap to a max characters-per-line budget. Words longer than the
34
- * budget are hard-split; if the result exceeds `maxLines`, the last kept line
35
- * is truncated with an ellipsis so a single pathological cell can't grow the
36
- * whole row unbounded.
34
+ * Greedy word-wrap to a max characters-per-line budget. Delegates to the shared
35
+ * shared text-layout module.
37
36
  */
38
37
  function wrapText(text, maxChars, maxLines) {
39
- const trimmed = (text ?? '').trim();
40
- if (trimmed.length === 0)
41
- return [];
42
- const budget = Math.max(1, maxChars);
43
- const words = trimmed.split(/\s+/);
44
- const lines = [];
45
- let cur = '';
46
- const flush = () => {
47
- if (cur.length > 0) {
48
- lines.push(cur);
49
- cur = '';
50
- }
51
- };
52
- for (const word of words) {
53
- if (word.length > budget) {
54
- // Token wider than the cell: hard-break it across lines.
55
- flush();
56
- let rest = word;
57
- while (rest.length > budget) {
58
- lines.push(rest.slice(0, budget));
59
- rest = rest.slice(budget);
60
- }
61
- cur = rest;
62
- continue;
63
- }
64
- const candidate = cur.length > 0 ? `${cur} ${word}` : word;
65
- if (candidate.length > budget) {
66
- flush();
67
- cur = word;
68
- }
69
- else {
70
- cur = candidate;
71
- }
72
- }
73
- flush();
74
- if (lines.length > maxLines) {
75
- const capped = lines.slice(0, maxLines);
76
- const last = capped[maxLines - 1];
77
- const clipped = last.length > budget - 1 ? last.slice(0, budget - 1) : last;
78
- capped[maxLines - 1] = `${clipped.replace(/…$/, '')}…`;
79
- return capped;
80
- }
81
- return lines;
38
+ return wrapTextLines(text, maxChars, maxLines);
82
39
  }
83
40
  function buildStageIndex(stages) {
84
41
  const idx = new Map();
@@ -0,0 +1,43 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { layoutCenteredEntityText, maxCharsForInnerWidth, wrapWords, wrapTextLines, } from '../entity-text-layout.js';
3
+ describe('entity-text-layout', () => {
4
+ it('wrapWords respects max lines and truncates with ellipsis', () => {
5
+ const long = 'one two three four five six seven eight nine ten eleven twelve';
6
+ const lines = wrapWords(long, 12, 2);
7
+ expect(lines.length).toBeLessThanOrEqual(2);
8
+ expect(lines[lines.length - 1]).toMatch(/…$/);
9
+ });
10
+ it('maxCharsForInnerWidth scales with inner width', () => {
11
+ expect(maxCharsForInnerWidth(160, 7)).toBeGreaterThan(maxCharsForInnerWidth(120, 7));
12
+ });
13
+ it('layoutCenteredEntityText emits more name capacity on wide boxes', () => {
14
+ const narrow = layoutCenteredEntityText({
15
+ boxX: 0,
16
+ boxY: 0,
17
+ boxWidth: 200,
18
+ boxHeight: 72,
19
+ name: 'Improve customer onboarding experience across regions',
20
+ id: 'GOAL-001',
21
+ nameMaxLines: 2,
22
+ idMaxLines: 1,
23
+ });
24
+ const wide = layoutCenteredEntityText({
25
+ boxX: 0,
26
+ boxY: 0,
27
+ boxWidth: 320,
28
+ boxHeight: 72,
29
+ name: 'Improve customer onboarding experience across regions',
30
+ id: 'GOAL-001',
31
+ nameMaxLines: 2,
32
+ idMaxLines: 1,
33
+ });
34
+ const narrowPrimary = narrow.filter((l) => l.cls === 'text-primary').map((l) => l.text).join(' ');
35
+ const widePrimary = wide.filter((l) => l.cls === 'text-primary').map((l) => l.text).join(' ');
36
+ expect(widePrimary.replace(/…/g, '').length).toBeGreaterThanOrEqual(narrowPrimary.replace(/…/g, '').length);
37
+ });
38
+ it('wrapTextLines hard-breaks oversized tokens (blueprint cells)', () => {
39
+ const lines = wrapTextLines('supercalifragilisticexpialidocious word', 10, 3);
40
+ expect(lines.length).toBeGreaterThan(1);
41
+ expect(lines.every((l) => l.length <= 10)).toBe(true);
42
+ });
43
+ });
@@ -40,9 +40,9 @@ describe('webview/entry — Step 2 host API', () => {
40
40
  });
41
41
  it('routes non-goals kinds to their validator after Step 4 (no NOTATION-NOT-WIRED)', () => {
42
42
  // Every supported kind is now wired to its validator + renderer. A
43
- // malformed fgca document must surface a structured validation error from
44
- // the fgca validator — never the Step-2/3 "not wired" placeholder.
45
- const r = render('fgca', 'changes: []\n');
43
+ // malformed dgca document must surface a structured validation error from
44
+ // the dgca validator — never the Step-2/3 "not wired" placeholder.
45
+ const r = render('dgca', 'changes: []\n');
46
46
  expect(r.status).toBe('error');
47
47
  expect(r.errors.length).toBeGreaterThan(0);
48
48
  expect(r.errors.every((e) => e.code !== 'NOTATION-NOT-WIRED')).toBe(true);
@@ -8,13 +8,7 @@
8
8
  */
9
9
  import { describe, expect, it } from 'vitest';
10
10
  import { renderBlocksSvg } from '../render-blocks.js';
11
- // Approximate character widths used by the renderer when sizing labels.
12
- // Kept in sync with `CHAR_W` / `CHAR_W_ID` in render-blocks.ts so the test
13
- // can compute the same overflow bounds the renderer uses.
14
- const CHAR_W = 7;
15
- const CHAR_W_ID = 6;
16
- // Same horizontal margin the renderer reserves so text never abuts the rect.
17
- const TEXT_MARGIN_X = 8;
11
+ import { CHAR_W_PRIMARY as CHAR_W, CHAR_W_ID, TEXT_MARGIN_X } from '../entity-text-layout.js';
18
12
  function leafDoc(name, id) {
19
13
  return {
20
14
  notation: 'blocks',