@transitrix/diagrams 1.8.1 → 1.8.3
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 +1 -1
- package/dist/blocks/__tests__/layout.test.js +2 -2
- package/dist/blocks/layout.js +4 -3
- package/dist/capability-map/dsm-layout.js +1 -1
- package/dist/capability-map/layout-tree.d.ts +1 -1
- package/dist/capability-map/layout-tree.js +1 -1
- package/dist/fgca/preview-layout.d.ts +2 -2
- package/dist/fgca/preview-layout.js +3 -2
- package/dist/node-size-presets.d.ts +17 -4
- package/dist/node-size-presets.js +86 -72
- package/dist/webview/__tests__/entity-text-layout.test.js +115 -3
- package/dist/webview/__tests__/render-blocks.test.js +9 -6
- package/dist/webview/__tests__/render-goals.test.js +41 -0
- package/dist/webview/entity-text-layout.d.ts +31 -3
- package/dist/webview/entity-text-layout.js +123 -57
- package/package.json +1 -1
- package/src/__tests__/node-size-presets.test.ts +50 -0
- package/src/activities/layout.ts +1 -1
- package/src/blocks/__tests__/layout.test.ts +2 -2
- package/src/blocks/layout.ts +4 -3
- package/src/capability-map/dsm-layout.ts +1 -1
- package/src/capability-map/layout-tree.ts +1 -1
- package/src/fgca/preview-layout.ts +3 -2
- package/src/node-size-presets.ts +95 -72
- package/src/webview/__tests__/entity-text-layout.test.ts +132 -2
- package/src/webview/__tests__/render-blocks.test.ts +11 -6
- package/src/webview/__tests__/render-goals.test.ts +42 -0
- package/src/webview/entity-text-layout.ts +140 -54
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ACTION_NODE_SIZE, BLOCKS_LEAF_SIZE, CAPABILITY_MAP_NODE_SIZE, DGCA_NODE_SIZE, ENTITY_NODE_HEIGHT, ENTITY_NODE_SIZE, ENTITY_NODE_WIDTH, GOALS_NODE_SIZE, PROCESS_BLUEPRINT_SIZE, resolveGoalsNodeSize, } from '../node-size-presets.js';
|
|
3
|
+
describe('node-size-presets', () => {
|
|
4
|
+
it('uses shared width and height tiers for entity nodes', () => {
|
|
5
|
+
expect(ENTITY_NODE_WIDTH).toEqual({ compact: 200, normal: 250, wide: 320 });
|
|
6
|
+
expect(ENTITY_NODE_HEIGHT).toEqual({ compact: 72, normal: 80, wide: 96 });
|
|
7
|
+
expect(ENTITY_NODE_SIZE.normal).toEqual({ width: 250, height: 80 });
|
|
8
|
+
expect(ENTITY_NODE_SIZE.compact).toEqual({ width: 200, height: 72 });
|
|
9
|
+
expect(ENTITY_NODE_SIZE.wide).toEqual({ width: 320, height: 96 });
|
|
10
|
+
});
|
|
11
|
+
it('exposes the same entity sizes for every box-based notation', () => {
|
|
12
|
+
for (const table of [GOALS_NODE_SIZE, DGCA_NODE_SIZE, BLOCKS_LEAF_SIZE, ACTION_NODE_SIZE]) {
|
|
13
|
+
expect(table).toBe(ENTITY_NODE_SIZE);
|
|
14
|
+
expect(table.normal).toEqual(ENTITY_NODE_SIZE.normal);
|
|
15
|
+
}
|
|
16
|
+
expect(CAPABILITY_MAP_NODE_SIZE.normal).toEqual({
|
|
17
|
+
nodeWidth: ENTITY_NODE_WIDTH.normal,
|
|
18
|
+
nodeHeight: ENTITY_NODE_HEIGHT.normal,
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
it('scales wide above normal and compact below', () => {
|
|
22
|
+
expect(resolveGoalsNodeSize('wide').height).toBeGreaterThan(resolveGoalsNodeSize('normal').height);
|
|
23
|
+
expect(resolveGoalsNodeSize('compact').height).toBeLessThan(resolveGoalsNodeSize('normal').height);
|
|
24
|
+
expect(resolveGoalsNodeSize('wide').width).toBeGreaterThan(resolveGoalsNodeSize('normal').width);
|
|
25
|
+
});
|
|
26
|
+
it('scales process blueprint from the same tier ratios', () => {
|
|
27
|
+
expect(PROCESS_BLUEPRINT_SIZE.wide.stageColumnWidth).toBeGreaterThan(PROCESS_BLUEPRINT_SIZE.normal.stageColumnWidth);
|
|
28
|
+
expect(PROCESS_BLUEPRINT_SIZE.compact.goalRowHeight).toBeLessThan(PROCESS_BLUEPRINT_SIZE.normal.goalRowHeight);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -108,8 +108,8 @@ describe('layoutNestedBlocks', () => {
|
|
|
108
108
|
},
|
|
109
109
|
});
|
|
110
110
|
const a = layout.blocks[0];
|
|
111
|
-
expect(a.width).toBe(
|
|
112
|
-
expect(a.height).toBe(
|
|
111
|
+
expect(a.width).toBe(250);
|
|
112
|
+
expect(a.height).toBe(80);
|
|
113
113
|
});
|
|
114
114
|
it('honours custom layout options', () => {
|
|
115
115
|
const layout = layoutNestedBlocks({
|
package/dist/blocks/layout.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { ENTITY_NODE_SIZE } from '../node-size-presets.js';
|
|
1
2
|
const DEFAULTS = {
|
|
2
|
-
leafWidth:
|
|
3
|
-
leafHeight:
|
|
3
|
+
leafWidth: ENTITY_NODE_SIZE.normal.width,
|
|
4
|
+
leafHeight: ENTITY_NODE_SIZE.normal.height,
|
|
4
5
|
padding: 12,
|
|
5
|
-
headerHeight:
|
|
6
|
+
headerHeight: 32,
|
|
6
7
|
childGap: 12,
|
|
7
8
|
topLevelGap: 24,
|
|
8
9
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getLevel, getParentAddress } from './address.js';
|
|
2
2
|
const DEFAULT_NODE_WIDTH = 250;
|
|
3
|
-
const DEFAULT_NODE_HEIGHT =
|
|
3
|
+
const DEFAULT_NODE_HEIGHT = 80;
|
|
4
4
|
const DEFAULT_RANK_SEP = 80;
|
|
5
5
|
const DEFAULT_NODE_SEP = 24;
|
|
6
6
|
/** Width of the virtual organisation-root node — narrower than a capability
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CapabilityNode, CapabilityMapHeader } from './types.js';
|
|
2
2
|
export declare const TREE_NODE_WIDTH = 250;
|
|
3
|
-
export declare const TREE_NODE_HEIGHT =
|
|
3
|
+
export declare const TREE_NODE_HEIGHT = 80;
|
|
4
4
|
/** Gap between sibling columns (node width + rank sep = 330px, matching DSM). */
|
|
5
5
|
export declare const TREE_RANK_SEP = 80;
|
|
6
6
|
/** Vertical gap between sibling nodes. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const TREE_NODE_WIDTH = 250;
|
|
2
|
-
export const TREE_NODE_HEIGHT =
|
|
2
|
+
export const TREE_NODE_HEIGHT = 80;
|
|
3
3
|
/** Gap between sibling columns (node width + rank sep = 330px, matching DSM). */
|
|
4
4
|
export const TREE_RANK_SEP = 80;
|
|
5
5
|
/** Vertical gap between sibling nodes. */
|
|
@@ -82,8 +82,8 @@ export interface FGCAPreviewLayout {
|
|
|
82
82
|
width: number;
|
|
83
83
|
height: number;
|
|
84
84
|
}
|
|
85
|
-
export declare const FGCA_NODE_W
|
|
86
|
-
export declare const FGCA_NODE_H
|
|
85
|
+
export declare const FGCA_NODE_W: number;
|
|
86
|
+
export declare const FGCA_NODE_H: number;
|
|
87
87
|
export declare const FGCA_HEADER_H = 32;
|
|
88
88
|
export declare const FGCA_PAD = 20;
|
|
89
89
|
export declare const FGCA_DEFAULT_COL_GAP = 160;
|
|
@@ -6,6 +6,7 @@
|
|
|
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
|
+
import { ENTITY_NODE_SIZE } from '../node-size-presets.js';
|
|
9
10
|
/**
|
|
10
11
|
* Trims a DGCA/DGA doc to a scope.
|
|
11
12
|
*
|
|
@@ -40,8 +41,8 @@ export function selectScopedFGCA(doc, scope) {
|
|
|
40
41
|
}
|
|
41
42
|
// Default node + frame geometry. Inter-node gaps are user-configurable via
|
|
42
43
|
// Inter-node gaps are user-configurable via spacing settings; node size uses presets.
|
|
43
|
-
export const FGCA_NODE_W =
|
|
44
|
-
export const FGCA_NODE_H =
|
|
44
|
+
export const FGCA_NODE_W = ENTITY_NODE_SIZE.normal.width;
|
|
45
|
+
export const FGCA_NODE_H = ENTITY_NODE_SIZE.normal.height;
|
|
45
46
|
export const FGCA_HEADER_H = 32;
|
|
46
47
|
export const FGCA_PAD = 20;
|
|
47
48
|
export const FGCA_DEFAULT_COL_GAP = 160;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Block/cell size presets for diagram renderers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Entity nodes (Goals, DGCA/DGA, Blocks leaves, Activities, Capability Map)
|
|
5
|
+
* share one width × height ladder. Heights use fixed tiers (72 / 80 / 96 px)
|
|
6
|
+
* so name + type + id never collide; widths scale independently (200 / 250 / 320).
|
|
7
|
+
*
|
|
8
|
+
* Process Blueprint uses a separate cell grid, scaled from the same tiers.
|
|
6
9
|
*/
|
|
7
10
|
export type NodeSizePreset = 'compact' | 'normal' | 'wide';
|
|
8
11
|
export declare const NODE_SIZE_PRESET_VALUES: readonly NodeSizePreset[];
|
|
@@ -32,12 +35,22 @@ export interface CapabilityMapDimensions {
|
|
|
32
35
|
nodeHeight: number;
|
|
33
36
|
}
|
|
34
37
|
type PresetTable<T> = Record<NodeSizePreset, T>;
|
|
38
|
+
/** Shared entity-node width tiers (px). */
|
|
39
|
+
export declare const ENTITY_NODE_WIDTH: Record<NodeSizePreset, number>;
|
|
40
|
+
/** Shared entity-node height tiers (px) — room for name + type + id. */
|
|
41
|
+
export declare const ENTITY_NODE_HEIGHT: Record<NodeSizePreset, number>;
|
|
42
|
+
/** Canonical entity-node sizes — used by every box-based notation preview. */
|
|
43
|
+
export declare const ENTITY_NODE_SIZE: PresetTable<BoxDimensions>;
|
|
44
|
+
/** @deprecated alias — use {@link ENTITY_NODE_SIZE} */
|
|
35
45
|
export declare const GOALS_NODE_SIZE: PresetTable<BoxDimensions>;
|
|
46
|
+
/** @deprecated alias — use {@link ENTITY_NODE_SIZE} */
|
|
36
47
|
export declare const DGCA_NODE_SIZE: PresetTable<BoxDimensions>;
|
|
37
|
-
|
|
48
|
+
/** @deprecated alias — use {@link ENTITY_NODE_SIZE} */
|
|
49
|
+
export declare const BLOCKS_LEAF_SIZE: PresetTable<BoxDimensions>;
|
|
50
|
+
/** @deprecated alias — use {@link ENTITY_NODE_SIZE} */
|
|
38
51
|
export declare const ACTION_NODE_SIZE: PresetTable<BoxDimensions>;
|
|
39
|
-
export declare const PROCESS_BLUEPRINT_SIZE: PresetTable<ProcessBlueprintDimensions>;
|
|
40
52
|
export declare const CAPABILITY_MAP_NODE_SIZE: PresetTable<CapabilityMapDimensions>;
|
|
53
|
+
export declare const PROCESS_BLUEPRINT_SIZE: PresetTable<ProcessBlueprintDimensions>;
|
|
41
54
|
export type NodeSizeNotation = 'goals' | 'dgca' | 'dga' | 'blocks' | 'action' | 'processBlueprint' | 'capabilityMap';
|
|
42
55
|
/** Parse a VS Code setting value into a preset id (defaults to `normal`). */
|
|
43
56
|
export declare function parseNodeSizePreset(value: string | undefined): NodeSizePreset;
|
|
@@ -1,81 +1,95 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Block/cell size presets for diagram renderers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Entity nodes (Goals, DGCA/DGA, Blocks leaves, Activities, Capability Map)
|
|
5
|
+
* share one width × height ladder. Heights use fixed tiers (72 / 80 / 96 px)
|
|
6
|
+
* so name + type + id never collide; widths scale independently (200 / 250 / 320).
|
|
7
|
+
*
|
|
8
|
+
* Process Blueprint uses a separate cell grid, scaled from the same tiers.
|
|
6
9
|
*/
|
|
7
10
|
export const NODE_SIZE_PRESET_VALUES = ['compact', 'normal', 'wide'];
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
/** Shared entity-node width tiers (px). */
|
|
12
|
+
export const ENTITY_NODE_WIDTH = {
|
|
13
|
+
compact: 200,
|
|
14
|
+
normal: 250,
|
|
15
|
+
wide: 320,
|
|
12
16
|
};
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
/** Shared entity-node height tiers (px) — room for name + type + id. */
|
|
18
|
+
export const ENTITY_NODE_HEIGHT = {
|
|
19
|
+
compact: 72,
|
|
20
|
+
normal: 80,
|
|
21
|
+
wide: 96,
|
|
17
22
|
};
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
function entityNodeSize(preset) {
|
|
24
|
+
return { width: ENTITY_NODE_WIDTH[preset], height: ENTITY_NODE_HEIGHT[preset] };
|
|
25
|
+
}
|
|
26
|
+
/** Canonical entity-node sizes — used by every box-based notation preview. */
|
|
27
|
+
export const ENTITY_NODE_SIZE = {
|
|
28
|
+
compact: entityNodeSize('compact'),
|
|
29
|
+
normal: entityNodeSize('normal'),
|
|
30
|
+
wide: entityNodeSize('wide'),
|
|
22
31
|
};
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
/** @deprecated alias — use {@link ENTITY_NODE_SIZE} */
|
|
33
|
+
export const GOALS_NODE_SIZE = ENTITY_NODE_SIZE;
|
|
34
|
+
/** @deprecated alias — use {@link ENTITY_NODE_SIZE} */
|
|
35
|
+
export const DGCA_NODE_SIZE = ENTITY_NODE_SIZE;
|
|
36
|
+
/** @deprecated alias — use {@link ENTITY_NODE_SIZE} */
|
|
37
|
+
export const BLOCKS_LEAF_SIZE = ENTITY_NODE_SIZE;
|
|
38
|
+
/** @deprecated alias — use {@link ENTITY_NODE_SIZE} */
|
|
39
|
+
export const ACTION_NODE_SIZE = ENTITY_NODE_SIZE;
|
|
40
|
+
export const CAPABILITY_MAP_NODE_SIZE = {
|
|
41
|
+
compact: { nodeWidth: ENTITY_NODE_WIDTH.compact, nodeHeight: ENTITY_NODE_HEIGHT.compact },
|
|
42
|
+
normal: { nodeWidth: ENTITY_NODE_WIDTH.normal, nodeHeight: ENTITY_NODE_HEIGHT.normal },
|
|
43
|
+
wide: { nodeWidth: ENTITY_NODE_WIDTH.wide, nodeHeight: ENTITY_NODE_HEIGHT.wide },
|
|
27
44
|
};
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
},
|
|
45
|
+
const PROCESS_BLUEPRINT_NORMAL = {
|
|
46
|
+
legendColumnWidth: 140,
|
|
47
|
+
stageColumnWidth: 220,
|
|
48
|
+
stageHeaderHeight: 40,
|
|
49
|
+
goalRowHeight: 56,
|
|
50
|
+
resultRowHeight: 56,
|
|
51
|
+
aspectRowMinHeight: 60,
|
|
52
|
+
pillHeight: 40,
|
|
53
|
+
pillGap: 4,
|
|
54
|
+
cellPadding: 8,
|
|
55
|
+
textLineHeight: 17,
|
|
56
|
+
textCharWidth: 7.5,
|
|
57
|
+
cellTextPadX: 10,
|
|
58
|
+
cellTextPadY: 10,
|
|
74
59
|
};
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
60
|
+
function scaleBlueprint(base, widthScale, heightScale) {
|
|
61
|
+
const w = (n) => Math.round(n * widthScale);
|
|
62
|
+
const h = (n) => Math.round(n * heightScale);
|
|
63
|
+
return {
|
|
64
|
+
legendColumnWidth: w(base.legendColumnWidth),
|
|
65
|
+
stageColumnWidth: w(base.stageColumnWidth),
|
|
66
|
+
stageHeaderHeight: h(base.stageHeaderHeight),
|
|
67
|
+
goalRowHeight: h(base.goalRowHeight),
|
|
68
|
+
resultRowHeight: h(base.resultRowHeight),
|
|
69
|
+
aspectRowMinHeight: h(base.aspectRowMinHeight),
|
|
70
|
+
pillHeight: h(base.pillHeight),
|
|
71
|
+
pillGap: base.pillGap,
|
|
72
|
+
cellPadding: base.cellPadding,
|
|
73
|
+
textLineHeight: h(base.textLineHeight),
|
|
74
|
+
textCharWidth: base.textCharWidth,
|
|
75
|
+
cellTextPadX: base.cellTextPadX,
|
|
76
|
+
cellTextPadY: base.cellTextPadY,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const PB_WIDTH_SCALE = {
|
|
80
|
+
compact: ENTITY_NODE_WIDTH.compact / ENTITY_NODE_WIDTH.normal,
|
|
81
|
+
normal: 1,
|
|
82
|
+
wide: ENTITY_NODE_WIDTH.wide / ENTITY_NODE_WIDTH.normal,
|
|
83
|
+
};
|
|
84
|
+
const PB_HEIGHT_SCALE = {
|
|
85
|
+
compact: ENTITY_NODE_HEIGHT.compact / ENTITY_NODE_HEIGHT.normal,
|
|
86
|
+
normal: 1,
|
|
87
|
+
wide: ENTITY_NODE_HEIGHT.wide / ENTITY_NODE_HEIGHT.normal,
|
|
88
|
+
};
|
|
89
|
+
export const PROCESS_BLUEPRINT_SIZE = {
|
|
90
|
+
compact: scaleBlueprint(PROCESS_BLUEPRINT_NORMAL, PB_WIDTH_SCALE.compact, PB_HEIGHT_SCALE.compact),
|
|
91
|
+
normal: PROCESS_BLUEPRINT_NORMAL,
|
|
92
|
+
wide: scaleBlueprint(PROCESS_BLUEPRINT_NORMAL, PB_WIDTH_SCALE.wide, PB_HEIGHT_SCALE.wide),
|
|
79
93
|
};
|
|
80
94
|
/** Parse a VS Code setting value into a preset id (defaults to `normal`). */
|
|
81
95
|
export function parseNodeSizePreset(value) {
|
|
@@ -84,16 +98,16 @@ export function parseNodeSizePreset(value) {
|
|
|
84
98
|
return 'normal';
|
|
85
99
|
}
|
|
86
100
|
export function resolveGoalsNodeSize(preset) {
|
|
87
|
-
return
|
|
101
|
+
return ENTITY_NODE_SIZE[preset];
|
|
88
102
|
}
|
|
89
103
|
export function resolveDgcaNodeSize(preset) {
|
|
90
|
-
return
|
|
104
|
+
return ENTITY_NODE_SIZE[preset];
|
|
91
105
|
}
|
|
92
106
|
export function resolveBlocksLeafSize(preset) {
|
|
93
|
-
return
|
|
107
|
+
return ENTITY_NODE_SIZE[preset];
|
|
94
108
|
}
|
|
95
109
|
export function resolveActionNodeSize(preset) {
|
|
96
|
-
return
|
|
110
|
+
return ENTITY_NODE_SIZE[preset];
|
|
97
111
|
}
|
|
98
112
|
export function resolveProcessBlueprintSize(preset) {
|
|
99
113
|
return PROCESS_BLUEPRINT_SIZE[preset];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { layoutCenteredEntityText, maxCharsForInnerWidth, wrapWords, wrapTextLines, } from '../entity-text-layout.js';
|
|
2
|
+
import { layoutCenteredEntityText, layoutLeafBlockText, layoutHeaderBlockText, LINE_H_PRIMARY, LINE_H_SECONDARY, TEXT_MARGIN_Y, maxCharsForInnerWidth, ROW_GROUP_GAP, wrapWords, wrapTextLines, } from '../entity-text-layout.js';
|
|
3
|
+
import { ENTITY_NODE_SIZE } from '../../node-size-presets.js';
|
|
3
4
|
describe('entity-text-layout', () => {
|
|
4
5
|
it('wrapWords respects max lines and truncates with ellipsis', () => {
|
|
5
6
|
const long = 'one two three four five six seven eight nine ten eleven twelve';
|
|
@@ -10,12 +11,29 @@ describe('entity-text-layout', () => {
|
|
|
10
11
|
it('maxCharsForInnerWidth scales with inner width', () => {
|
|
11
12
|
expect(maxCharsForInnerWidth(160, 7)).toBeGreaterThan(maxCharsForInnerWidth(120, 7));
|
|
12
13
|
});
|
|
14
|
+
it('layoutCenteredEntityText keeps name and type labels separated', () => {
|
|
15
|
+
const specs = layoutCenteredEntityText({
|
|
16
|
+
boxX: 0,
|
|
17
|
+
boxY: 0,
|
|
18
|
+
boxWidth: 250,
|
|
19
|
+
boxHeight: 80,
|
|
20
|
+
name: 'Grow enterprise value',
|
|
21
|
+
type: 'Strategic',
|
|
22
|
+
id: 'GOAL-VALUE-1',
|
|
23
|
+
nameMaxLines: 2,
|
|
24
|
+
idMaxLines: 1,
|
|
25
|
+
});
|
|
26
|
+
const nameY = specs.find((l) => l.cls === 'text-primary').y;
|
|
27
|
+
const typeY = specs.find((l) => l.cls === 'text-secondary').y;
|
|
28
|
+
const minGap = typeY - nameY - LINE_H_PRIMARY / 2 - LINE_H_SECONDARY / 2;
|
|
29
|
+
expect(minGap).toBeGreaterThanOrEqual(ROW_GROUP_GAP - 1);
|
|
30
|
+
});
|
|
13
31
|
it('layoutCenteredEntityText emits more name capacity on wide boxes', () => {
|
|
14
32
|
const narrow = layoutCenteredEntityText({
|
|
15
33
|
boxX: 0,
|
|
16
34
|
boxY: 0,
|
|
17
35
|
boxWidth: 200,
|
|
18
|
-
boxHeight:
|
|
36
|
+
boxHeight: 80,
|
|
19
37
|
name: 'Improve customer onboarding experience across regions',
|
|
20
38
|
id: 'GOAL-001',
|
|
21
39
|
nameMaxLines: 2,
|
|
@@ -25,7 +43,7 @@ describe('entity-text-layout', () => {
|
|
|
25
43
|
boxX: 0,
|
|
26
44
|
boxY: 0,
|
|
27
45
|
boxWidth: 320,
|
|
28
|
-
boxHeight:
|
|
46
|
+
boxHeight: 80,
|
|
29
47
|
name: 'Improve customer onboarding experience across regions',
|
|
30
48
|
id: 'GOAL-001',
|
|
31
49
|
nameMaxLines: 2,
|
|
@@ -41,3 +59,97 @@ describe('entity-text-layout', () => {
|
|
|
41
59
|
expect(lines.every((l) => l.length <= 10)).toBe(true);
|
|
42
60
|
});
|
|
43
61
|
});
|
|
62
|
+
// Approximate half-extent (px) of each text class's glyphs around its centre
|
|
63
|
+
// y — font-size / 2, the same heuristic the render-blocks overlap regression
|
|
64
|
+
// test (#419) uses. Used below to assert text never visually crosses the box
|
|
65
|
+
// border, not just that the *nominal* line-height budget fits.
|
|
66
|
+
const HALF_EXTENT = {
|
|
67
|
+
'text-primary': 6, // 12px font
|
|
68
|
+
'text-secondary': 6, // 11px font, rounded up
|
|
69
|
+
'text-id': 5, // 10px font
|
|
70
|
+
};
|
|
71
|
+
describe('entity text layout — never overflows the box, always keeps padding', () => {
|
|
72
|
+
const PRESETS = Object.entries(ENTITY_NODE_SIZE); // compact/normal/wide
|
|
73
|
+
const NAMES = [
|
|
74
|
+
'Short',
|
|
75
|
+
'Achieve full GDPR & NIS2 compliance',
|
|
76
|
+
'A name so long it would need three or more lines to render in full without any height limit at all',
|
|
77
|
+
];
|
|
78
|
+
function assertNoOverflowAndPadded(specs, boxTop, boxBottom) {
|
|
79
|
+
for (const s of specs) {
|
|
80
|
+
const half = HALF_EXTENT[s.cls] ?? 6;
|
|
81
|
+
expect(s.y - half).toBeGreaterThanOrEqual(boxTop);
|
|
82
|
+
expect(s.y + half).toBeLessThanOrEqual(boxBottom);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const [presetName, size] of PRESETS) {
|
|
86
|
+
for (const withType of [false, true]) {
|
|
87
|
+
for (const name of NAMES) {
|
|
88
|
+
it(`layoutCenteredEntityText: ${presetName} preset, type=${withType}, name="${name.slice(0, 20)}…" stays inside the box`, () => {
|
|
89
|
+
const boxY = 0;
|
|
90
|
+
const specs = layoutCenteredEntityText({
|
|
91
|
+
boxX: 0,
|
|
92
|
+
boxY,
|
|
93
|
+
boxWidth: size.width,
|
|
94
|
+
boxHeight: size.height,
|
|
95
|
+
name,
|
|
96
|
+
type: withType ? 'Strategic Goal' : undefined,
|
|
97
|
+
id: 'GOAL-EU-COMPLIANCE-1',
|
|
98
|
+
nameMaxLines: 2,
|
|
99
|
+
idMaxLines: 1,
|
|
100
|
+
});
|
|
101
|
+
assertNoOverflowAndPadded(specs, boxY, boxY + size.height);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
it('reproduces the reported case (wide preset, 2-line name + type + long id) with real padding on all sides', () => {
|
|
107
|
+
const size = ENTITY_NODE_SIZE.normal;
|
|
108
|
+
const boxY = 100;
|
|
109
|
+
const specs = layoutCenteredEntityText({
|
|
110
|
+
boxX: 0,
|
|
111
|
+
boxY,
|
|
112
|
+
boxWidth: size.width,
|
|
113
|
+
boxHeight: size.height,
|
|
114
|
+
name: 'Achieve full GDPR & NIS2 compliance',
|
|
115
|
+
type: 'Project Goal',
|
|
116
|
+
id: 'GOAL-EU-COMPLIANCE-1',
|
|
117
|
+
nameMaxLines: 2,
|
|
118
|
+
idMaxLines: 1,
|
|
119
|
+
});
|
|
120
|
+
const nameLines = specs.filter((s) => s.cls === 'text-primary');
|
|
121
|
+
expect(nameLines.length).toBe(2); // normal preset is tall enough to keep both lines
|
|
122
|
+
assertNoOverflowAndPadded(specs, boxY, boxY + size.height);
|
|
123
|
+
// Explicit padding, not just "doesn't cross the border".
|
|
124
|
+
const firstTop = Math.min(...specs.map((s) => s.y - (HALF_EXTENT[s.cls] ?? 6)));
|
|
125
|
+
const lastBottom = Math.max(...specs.map((s) => s.y + (HALF_EXTENT[s.cls] ?? 6)));
|
|
126
|
+
expect(firstTop - boxY).toBeGreaterThanOrEqual(TEXT_MARGIN_Y);
|
|
127
|
+
expect(boxY + size.height - lastBottom).toBeGreaterThanOrEqual(TEXT_MARGIN_Y);
|
|
128
|
+
});
|
|
129
|
+
it('layoutLeafBlockText stays inside the box for compact preset with a long name and id', () => {
|
|
130
|
+
const size = ENTITY_NODE_SIZE.compact;
|
|
131
|
+
const boxY = 0;
|
|
132
|
+
const specs = layoutLeafBlockText({
|
|
133
|
+
boxX: 0,
|
|
134
|
+
boxY,
|
|
135
|
+
boxWidth: size.width,
|
|
136
|
+
boxHeight: size.height,
|
|
137
|
+
name: 'Personal data records that linger after consent withdrawal across many systems and archives',
|
|
138
|
+
id: 'VERY_LONG_BLOCK_IDENTIFIER_WITH_MANY_SEGMENTS_AND_MORE',
|
|
139
|
+
});
|
|
140
|
+
assertNoOverflowAndPadded(specs, boxY, boxY + size.height);
|
|
141
|
+
});
|
|
142
|
+
it('layoutHeaderBlockText stays inside a short header strip', () => {
|
|
143
|
+
const boxY = 0;
|
|
144
|
+
const headerHeight = 28;
|
|
145
|
+
const specs = layoutHeaderBlockText({
|
|
146
|
+
boxX: 0,
|
|
147
|
+
boxY,
|
|
148
|
+
boxWidth: 250,
|
|
149
|
+
headerHeight,
|
|
150
|
+
name: 'A fairly long container name here',
|
|
151
|
+
id: 'VERY_LONG_CONTAINER_IDENTIFIER',
|
|
152
|
+
});
|
|
153
|
+
assertNoOverflowAndPadded(specs, boxY, boxY + headerHeight);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
import { describe, expect, it } from 'vitest';
|
|
10
10
|
import { renderBlocksSvg } from '../render-blocks.js';
|
|
11
11
|
import { CHAR_W_PRIMARY as CHAR_W, CHAR_W_ID, TEXT_MARGIN_X } from '../entity-text-layout.js';
|
|
12
|
+
import { ENTITY_NODE_SIZE } from '../../node-size-presets.js';
|
|
13
|
+
const LEAF_W = ENTITY_NODE_SIZE.normal.width;
|
|
14
|
+
const LEAF_H = ENTITY_NODE_SIZE.normal.height;
|
|
12
15
|
function leafDoc(name, id) {
|
|
13
16
|
return {
|
|
14
17
|
notation: 'blocks',
|
|
@@ -69,7 +72,7 @@ describe('renderBlocksSvg — leaf labels', () => {
|
|
|
69
72
|
}
|
|
70
73
|
});
|
|
71
74
|
it('wraps a long ID to at most 2 lines and truncates with …', () => {
|
|
72
|
-
const longId = '
|
|
75
|
+
const longId = 'VERY_LONG_BLOCK_IDENTIFIER_WITH_MANY_SEGMENTS_AND_MORE_AND_MORE_EXTRA_TAIL_TO_FORCE_TRUNCATION';
|
|
73
76
|
const svg = renderBlocksSvg(leafDoc('Short name', longId));
|
|
74
77
|
const idLines = extractLabels(svg).filter((l) => l.cls === 'text-id');
|
|
75
78
|
expect(idLines.length).toBeGreaterThan(0);
|
|
@@ -78,10 +81,10 @@ describe('renderBlocksSvg — leaf labels', () => {
|
|
|
78
81
|
expect(idLines[1].text.endsWith('…')).toBe(true);
|
|
79
82
|
}
|
|
80
83
|
});
|
|
81
|
-
it(
|
|
84
|
+
it(`keeps every label inside the ${LEAF_W}px leaf width (approximated by CHAR_W * length)`, () => {
|
|
82
85
|
const svg = renderBlocksSvg(leafDoc('Personal data records that linger after consent withdrawal across many systems', 'PII_RECORDS_WITH_A_FAIRLY_LONG_IDENTIFIER_HERE'));
|
|
83
86
|
const labels = extractLabels(svg);
|
|
84
|
-
const inner =
|
|
87
|
+
const inner = LEAF_W - TEXT_MARGIN_X * 2;
|
|
85
88
|
for (const l of labels) {
|
|
86
89
|
const cw = l.cls === 'text-primary' ? CHAR_W : CHAR_W_ID;
|
|
87
90
|
expect(l.text.length * cw).toBeLessThanOrEqual(inner);
|
|
@@ -105,8 +108,8 @@ describe('renderBlocksSvg — container headers', () => {
|
|
|
105
108
|
it('truncates a long container ID so it does not overflow the block width', () => {
|
|
106
109
|
const svg = renderBlocksSvg(containerDoc('Short name', 'VERY_LONG_CONTAINER_IDENTIFIER_THAT_WOULD_OTHERWISE_OVERFLOW', 'CHILD', 'Child'));
|
|
107
110
|
const labels = extractLabels(svg);
|
|
108
|
-
//
|
|
109
|
-
const containerW =
|
|
111
|
+
// Container width derives from its single leaf child: leafW + 2×padding.
|
|
112
|
+
const containerW = LEAF_W + 24;
|
|
110
113
|
const inner = containerW - TEXT_MARGIN_X * 2;
|
|
111
114
|
for (const l of labels) {
|
|
112
115
|
const cw = l.cls === 'text-primary' ? CHAR_W : CHAR_W_ID;
|
|
@@ -121,7 +124,7 @@ describe('renderBlocksSvg — container headers', () => {
|
|
|
121
124
|
const longName = 'Active systems that are erasure-reachable within the standard 30-day operational window';
|
|
122
125
|
const svg = renderBlocksSvg(containerDoc(longName, 'ACTIVE'));
|
|
123
126
|
const labels = extractLabels(svg);
|
|
124
|
-
const containerW =
|
|
127
|
+
const containerW = LEAF_W + 24;
|
|
125
128
|
const inner = containerW - TEXT_MARGIN_X * 2;
|
|
126
129
|
const primaries = labels.filter((l) => l.cls === 'text-primary');
|
|
127
130
|
for (const l of primaries) {
|
|
@@ -58,6 +58,47 @@ describe('renderGoalsSvg', () => {
|
|
|
58
58
|
expect(svg).toContain('width="0"');
|
|
59
59
|
expect(svg).toContain('height="0"');
|
|
60
60
|
});
|
|
61
|
+
it('renders name, type, and id on separate lines without overlap', () => {
|
|
62
|
+
const tree = {
|
|
63
|
+
goal_types: [
|
|
64
|
+
{ name: 'Strategic', level: 0 },
|
|
65
|
+
{ name: 'Objective', level: 1 },
|
|
66
|
+
],
|
|
67
|
+
goals: [
|
|
68
|
+
{
|
|
69
|
+
id: 1,
|
|
70
|
+
canonical_id: 'GOAL-VALUE-1',
|
|
71
|
+
name: 'Grow enterprise value',
|
|
72
|
+
type: 'Strategic',
|
|
73
|
+
level: 0,
|
|
74
|
+
parent_id: 0,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 2,
|
|
78
|
+
canonical_id: 'GOAL-COST-1',
|
|
79
|
+
name: 'Reduce run cost 20%',
|
|
80
|
+
type: 'Objective',
|
|
81
|
+
level: 1,
|
|
82
|
+
parent_id: 1,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
const svg = renderGoalsSvg(tree);
|
|
87
|
+
expect(svg).toContain('Grow enterprise value');
|
|
88
|
+
expect(svg).toContain('Strategic');
|
|
89
|
+
expect(svg).toContain('GOAL-VALUE-1');
|
|
90
|
+
const typeMatches = [...svg.matchAll(/class="text-secondary"[^>]*y="(\d+)"/g)];
|
|
91
|
+
const nameMatches = [...svg.matchAll(/class="text-primary"[^>]*y="(\d+)"/g)];
|
|
92
|
+
expect(typeMatches.length).toBeGreaterThan(0);
|
|
93
|
+
for (const type of typeMatches) {
|
|
94
|
+
const typeY = Number(type[1]);
|
|
95
|
+
const nearestName = nameMatches
|
|
96
|
+
.map((m) => Number(m[1]))
|
|
97
|
+
.filter((y) => y < typeY)
|
|
98
|
+
.sort((a, b) => b - a)[0];
|
|
99
|
+
expect(typeY - nearestName).toBeGreaterThanOrEqual(18);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
61
102
|
it('shows canonical goal ids in node labels when present', () => {
|
|
62
103
|
const tree = {
|
|
63
104
|
goal_types: [{ name: 'Strategy', level: 0 }],
|