@transitrix/diagrams 1.12.1 → 1.13.0
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/edge-path.d.ts +17 -0
- package/dist/edge-path.js +33 -4
- package/dist/fgca/index.d.ts +1 -1
- package/dist/fgca/index.js +1 -1
- package/dist/fgca/parse-canonical.d.ts +10 -2
- package/dist/fgca/parse-canonical.js +9 -4
- package/dist/fgca/preview-layout.d.ts +35 -4
- package/dist/fgca/preview-layout.js +214 -14
- package/dist/fgca/types.d.ts +4 -0
- package/dist/scope.d.ts +27 -0
- package/dist/scope.js +40 -0
- package/dist/theme/themes.js +3 -1
- package/dist/theme/tokens.d.ts +12 -0
- package/dist/theme/tokens.js +12 -0
- package/dist/webview/entry.js +8 -2
- package/dist/webview/render-fgca.d.ts +3 -0
- package/dist/webview/render-fgca.js +28 -5
- package/dist/webview/render-goals.d.ts +3 -0
- package/dist/webview/render-goals.js +10 -5
- package/package.json +1 -1
- package/src/edge-path.ts +45 -4
- package/src/fgca/index.ts +2 -0
- package/src/fgca/parse-canonical.ts +9 -2
- package/src/fgca/preview-layout.ts +255 -19
- package/src/fgca/types.ts +4 -0
- package/src/scope.ts +61 -0
- package/src/theme/themes.ts +3 -0
- package/src/theme/tokens.ts +13 -0
- package/src/webview/entry.ts +8 -2
- package/src/webview/render-fgca.ts +30 -4
- package/src/webview/render-goals.ts +11 -5
package/dist/theme/themes.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LAYER_COLORS, LEVEL_COLORS, MATURITY_COLORS, TREE_LEVEL_COLORS, TREE_MATURITY_COLORS, STRUCTURAL, BRAND_EMPHASIS, TYPOGRAPHY, CSS_VAR, getBaseResetCss, } from './tokens.js';
|
|
1
|
+
import { LAYER_COLORS, LEVEL_COLORS, MATURITY_COLORS, TREE_LEVEL_COLORS, TREE_MATURITY_COLORS, VIRTUAL_ENTITY, STRUCTURAL, BRAND_EMPHASIS, TYPOGRAPHY, CSS_VAR, getBaseResetCss, } from './tokens.js';
|
|
2
2
|
const TRANSITRIX_LIGHT = {
|
|
3
3
|
bg: '#ffffff',
|
|
4
4
|
bgSurface: '#f8fafc',
|
|
@@ -79,6 +79,7 @@ function diagramVars(variant) {
|
|
|
79
79
|
`${CSS_VAR.layerGoal}:${lc.goal[variant]}`,
|
|
80
80
|
`${CSS_VAR.layerChange}:${lc.change[variant]}`,
|
|
81
81
|
`${CSS_VAR.layerActivity}:${lc.activity[variant]}`,
|
|
82
|
+
`${CSS_VAR.virtualEntity}:${VIRTUAL_ENTITY[variant]}`,
|
|
82
83
|
`${CSS_VAR.nodeStroke}:${sc.nodeStroke[variant]}`,
|
|
83
84
|
`${CSS_VAR.edgeStroke}:${sc.edgeStroke[variant]}`,
|
|
84
85
|
`${CSS_VAR.textPrimary}:${sc.textPrimary[variant]}`,
|
|
@@ -133,6 +134,7 @@ function diagramClassCss() {
|
|
|
133
134
|
.layer-goal{fill:var(${cv.layerGoal});}
|
|
134
135
|
.layer-change{fill:var(${cv.layerChange});}
|
|
135
136
|
.layer-activity{fill:var(${cv.layerActivity});}
|
|
137
|
+
.layer-virtual{fill:var(${cv.virtualEntity});stroke-dasharray:4,3;}
|
|
136
138
|
${levelRules}
|
|
137
139
|
.diagram-edge{stroke:var(${cv.edgeStroke});stroke-width:1.5;fill:none;}
|
|
138
140
|
.arrow-fill{fill:var(${cv.edgeStroke});}
|
package/dist/theme/tokens.d.ts
CHANGED
|
@@ -70,6 +70,17 @@ export declare const LAYER_COLORS: {
|
|
|
70
70
|
readonly hc: "#4ade80";
|
|
71
71
|
};
|
|
72
72
|
};
|
|
73
|
+
/**
|
|
74
|
+
* Virtual/synthesized entity fill — used for placeholder nodes that represent
|
|
75
|
+
* absence (e.g. a synthesized Change box when an Action skips straight to its Goal).
|
|
76
|
+
* Pale neutral tint, distinctly muted from real entity colors; border dashed to
|
|
77
|
+
* signal non-reality.
|
|
78
|
+
*/
|
|
79
|
+
export declare const VIRTUAL_ENTITY: {
|
|
80
|
+
readonly light: "#f3f4f6";
|
|
81
|
+
readonly dark: "#2a3134";
|
|
82
|
+
readonly hc: "#e2e8f0";
|
|
83
|
+
};
|
|
73
84
|
/**
|
|
74
85
|
* Level fill colors for Goals tree nodes — 8 slots, cycled by level index.
|
|
75
86
|
* A single-hue petrol tint ramp (level-0 lightest/root → level-7 deepest);
|
|
@@ -241,6 +252,7 @@ export declare const CSS_VAR: {
|
|
|
241
252
|
readonly layerGoal: "--ts-layer-goal";
|
|
242
253
|
readonly layerChange: "--ts-layer-change";
|
|
243
254
|
readonly layerActivity: "--ts-layer-activity";
|
|
255
|
+
readonly virtualEntity: "--ts-virtual-entity";
|
|
244
256
|
readonly nodeStroke: "--ts-node-stroke";
|
|
245
257
|
readonly edgeStroke: "--ts-edge-stroke";
|
|
246
258
|
readonly textPrimary: "--ts-text-primary";
|
package/dist/theme/tokens.js
CHANGED
|
@@ -38,6 +38,17 @@ export const LAYER_COLORS = {
|
|
|
38
38
|
change: { light: '#c7e5ef', dark: '#1a3842', hc: '#60a5fa' },
|
|
39
39
|
activity: { light: '#b4e0ee', dark: '#1a414e', hc: '#4ade80' },
|
|
40
40
|
};
|
|
41
|
+
/**
|
|
42
|
+
* Virtual/synthesized entity fill — used for placeholder nodes that represent
|
|
43
|
+
* absence (e.g. a synthesized Change box when an Action skips straight to its Goal).
|
|
44
|
+
* Pale neutral tint, distinctly muted from real entity colors; border dashed to
|
|
45
|
+
* signal non-reality.
|
|
46
|
+
*/
|
|
47
|
+
export const VIRTUAL_ENTITY = {
|
|
48
|
+
light: '#f3f4f6',
|
|
49
|
+
dark: '#2a3134',
|
|
50
|
+
hc: '#e2e8f0',
|
|
51
|
+
};
|
|
41
52
|
/**
|
|
42
53
|
* Level fill colors for Goals tree nodes — 8 slots, cycled by level index.
|
|
43
54
|
* A single-hue petrol tint ramp (level-0 lightest/root → level-7 deepest);
|
|
@@ -158,6 +169,7 @@ export const CSS_VAR = {
|
|
|
158
169
|
layerGoal: '--ts-layer-goal',
|
|
159
170
|
layerChange: '--ts-layer-change',
|
|
160
171
|
layerActivity: '--ts-layer-activity',
|
|
172
|
+
virtualEntity: '--ts-virtual-entity',
|
|
161
173
|
nodeStroke: '--ts-node-stroke',
|
|
162
174
|
edgeStroke: '--ts-edge-stroke',
|
|
163
175
|
textPrimary: '--ts-text-primary',
|
package/dist/webview/entry.js
CHANGED
|
@@ -124,7 +124,10 @@ function dispatchValidate(kind, doc) {
|
|
|
124
124
|
: Array.isArray(meta['__outOfScopeFactorIds'])
|
|
125
125
|
? new Set(meta['__outOfScopeFactorIds'])
|
|
126
126
|
: undefined;
|
|
127
|
-
const
|
|
127
|
+
const progressData = meta['__progressData'] instanceof Map
|
|
128
|
+
? meta['__progressData']
|
|
129
|
+
: undefined;
|
|
130
|
+
const v = parseCanonicalFGCA(doc, outOfScopeGoalIds, outOfScopeFactorIds, progressData);
|
|
128
131
|
const r = emptyResult(kind, v.valid ? 'ok' : 'error');
|
|
129
132
|
r.errors.push(...v.errors);
|
|
130
133
|
r.warnings.push(...v.warnings);
|
|
@@ -145,7 +148,10 @@ function dispatchValidate(kind, doc) {
|
|
|
145
148
|
: Array.isArray(meta['__outOfScopeFactorIds'])
|
|
146
149
|
? new Set(meta['__outOfScopeFactorIds'])
|
|
147
150
|
: undefined;
|
|
148
|
-
const
|
|
151
|
+
const progressData = meta['__progressData'] instanceof Map
|
|
152
|
+
? meta['__progressData']
|
|
153
|
+
: undefined;
|
|
154
|
+
const v = parseCanonicalFGA(doc, outOfScopeGoalIds, outOfScopeFactorIds, progressData);
|
|
149
155
|
const r = emptyResult(kind, v.valid ? 'ok' : 'error');
|
|
150
156
|
r.errors.push(...v.errors);
|
|
151
157
|
r.warnings.push(...v.warnings);
|
|
@@ -14,11 +14,13 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { layoutFGCAPreview } from '../fgca/preview-layout.js';
|
|
16
16
|
import type { FGCADoc } from '../fgca/validate.js';
|
|
17
|
+
import { type EdgeStyle } from '../edge-path.js';
|
|
17
18
|
import { type NodeSizePreset } from '../node-size-presets.js';
|
|
18
19
|
type FgcaLayout = ReturnType<typeof layoutFGCAPreview>;
|
|
19
20
|
export interface RenderFgcaBodyOptions {
|
|
20
21
|
nodeWidth?: number;
|
|
21
22
|
nodeHeight?: number;
|
|
23
|
+
edgeStyle?: EdgeStyle;
|
|
22
24
|
}
|
|
23
25
|
/**
|
|
24
26
|
* Canonical DGCA/DGA body — the column headers, node rects and edge paths that
|
|
@@ -39,6 +41,7 @@ export interface RenderFgcaOptions {
|
|
|
39
41
|
curvature?: number;
|
|
40
42
|
/** Entry curvature at the target node; defaults to `curvature` when omitted. */
|
|
41
43
|
entryCurvature?: number;
|
|
44
|
+
edgeStyle?: EdgeStyle;
|
|
42
45
|
nodeSizePreset?: NodeSizePreset;
|
|
43
46
|
layoutOptions?: Parameters<typeof layoutFGCAPreview>[1];
|
|
44
47
|
/** Show scope caption when goal-scoped projection omits goals that reference out-of-scope goals. */
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* exactly as the extension preview does.
|
|
14
14
|
*/
|
|
15
15
|
import { layoutFGCAPreview, FGCA_NODE_W, FGCA_NODE_H, FGCA_HEADER_H as HEADER_H, FGCA_PAD as PAD, } from '../fgca/preview-layout.js';
|
|
16
|
-
import {
|
|
16
|
+
import { previewEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
|
|
17
17
|
import { parseNodeSizePreset, resolveDgcaNodeSize } from '../node-size-presets.js';
|
|
18
18
|
import { generateSvgEmbedCss } from '../theme/index.js';
|
|
19
19
|
import { emitCenteredTextSvg, layoutCenteredEntityText, truncateLine } from './entity-text-layout.js';
|
|
@@ -36,16 +36,19 @@ const COL_LABELS = {
|
|
|
36
36
|
export function renderFgcaBody(columns, nodes, edges, curvature, entryCurvature, bodyOptions = {}) {
|
|
37
37
|
const nodeWidth = bodyOptions.nodeWidth ?? FGCA_NODE_W;
|
|
38
38
|
const nodeHeight = bodyOptions.nodeHeight ?? FGCA_NODE_H;
|
|
39
|
+
const edgeStyle = bodyOptions.edgeStyle;
|
|
39
40
|
const headerTruncate = Math.max(8, Math.floor((nodeWidth - 16) / 7));
|
|
41
|
+
const STALENESS_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000;
|
|
40
42
|
const headerSvg = columns
|
|
41
43
|
.map(({ col, x }) => `<text class="text-header" x="${x + nodeWidth / 2}" y="${PAD + HEADER_H / 2}" text-anchor="middle" dominant-baseline="central">${escXml(truncateLine(COL_LABELS[col], headerTruncate))}</text>`)
|
|
42
44
|
.join('\n');
|
|
43
45
|
const edgeSvg = edges
|
|
44
|
-
.map((e) => `<path d="${
|
|
46
|
+
.map((e) => `<path d="${previewEdgePath(e.sx, e.sy, e.tx, e.ty, edgeStyle, curvature, entryCurvature)}" class="diagram-edge" marker-end="url(#arrow)"/>`)
|
|
45
47
|
.join('\n');
|
|
46
48
|
const nodeSvg = nodes
|
|
47
49
|
.map((n) => {
|
|
48
50
|
const entityId = n.id.slice(n.id.indexOf('_') + 1);
|
|
51
|
+
const nodeClass = n.virtual ? 'diagram-node layer-virtual' : `diagram-node layer-${n.col}`;
|
|
49
52
|
const specs = layoutCenteredEntityText({
|
|
50
53
|
boxX: n.x,
|
|
51
54
|
boxY: n.y,
|
|
@@ -57,16 +60,35 @@ export function renderFgcaBody(columns, nodes, edges, curvature, entryCurvature,
|
|
|
57
60
|
idMaxLines: 1,
|
|
58
61
|
});
|
|
59
62
|
const textSvg = emitCenteredTextSvg(specs, n.x + nodeWidth / 2, escXml);
|
|
63
|
+
const titleContent = `${escXml(n.label)} (${escXml(entityId)})`;
|
|
64
|
+
const typeBadge = n.type && n.col === 'activity'
|
|
65
|
+
? `<text class="text-id" x="${n.x + nodeWidth - 6}" y="${n.y + 10}" text-anchor="end" dominant-baseline="hanging" font-size="10">${escXml(n.type)}</text>`
|
|
66
|
+
: '';
|
|
67
|
+
const progressBadge = n.progress && n.col === 'activity'
|
|
68
|
+
? (() => {
|
|
69
|
+
const computedAt = new Date(n.progress.computedAt);
|
|
70
|
+
const now = new Date();
|
|
71
|
+
const isStale = now.getTime() - computedAt.getTime() > STALENESS_THRESHOLD_MS;
|
|
72
|
+
const className = isStale ? 'text-id' : 'text-id';
|
|
73
|
+
const opacity = isStale ? ' opacity="0.5"' : '';
|
|
74
|
+
return `<text class="${className}" x="${n.x + nodeWidth - 6}" y="${n.y + 22}" text-anchor="end" dominant-baseline="hanging" font-size="10"${opacity}>${escXml(n.progress.percent + '%')}</text>`;
|
|
75
|
+
})()
|
|
76
|
+
: '';
|
|
60
77
|
return [
|
|
61
|
-
`<
|
|
78
|
+
`<g>`,
|
|
79
|
+
`<title>${titleContent}</title>`,
|
|
80
|
+
`<rect class="${nodeClass}" x="${n.x}" y="${n.y}" width="${nodeWidth}" height="${nodeHeight}" rx="8"/>`,
|
|
62
81
|
textSvg,
|
|
63
|
-
|
|
82
|
+
typeBadge,
|
|
83
|
+
progressBadge,
|
|
84
|
+
`</g>`,
|
|
85
|
+
].filter(Boolean).join('\n');
|
|
64
86
|
})
|
|
65
87
|
.join('\n');
|
|
66
88
|
return `${headerSvg}\n${nodeSvg}\n${edgeSvg}`;
|
|
67
89
|
}
|
|
68
90
|
export function renderFgcaSvg(doc, options = {}) {
|
|
69
|
-
const { variant = 'dgca', title = '', curvature = DEFAULT_EDGE_CURVATURE, entryCurvature, nodeSizePreset = 'normal', layoutOptions, scopeCaption = false, } = options;
|
|
91
|
+
const { variant = 'dgca', title = '', curvature = DEFAULT_EDGE_CURVATURE, entryCurvature, edgeStyle, nodeSizePreset = 'normal', layoutOptions, scopeCaption = false, } = options;
|
|
70
92
|
const hideChanges = variant === 'dga' || doc.hideChanges === true;
|
|
71
93
|
const nodeSize = resolveDgcaNodeSize(parseNodeSizePreset(nodeSizePreset));
|
|
72
94
|
const { nodes, edges, columns, width, height } = layoutFGCAPreview(doc, {
|
|
@@ -81,6 +103,7 @@ export function renderFgcaSvg(doc, options = {}) {
|
|
|
81
103
|
const body = renderFgcaBody(columns, nodes, edges, curvature, entryCurvature, {
|
|
82
104
|
nodeWidth: nodeSize.width,
|
|
83
105
|
nodeHeight: nodeSize.height,
|
|
106
|
+
edgeStyle,
|
|
84
107
|
});
|
|
85
108
|
const titleSvg = title
|
|
86
109
|
? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(title)}</text>`
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { GoalTree, GoalTreeLayout } from '../goals/types.js';
|
|
2
|
+
import { type EdgeStyle } from '../edge-path.js';
|
|
2
3
|
import { type NodeSizePreset } from '../node-size-presets.js';
|
|
3
4
|
import { type ThemeId } from '../theme/index.js';
|
|
4
5
|
export interface RenderGoalsOptions {
|
|
5
6
|
treeName?: string;
|
|
6
7
|
curvature?: number;
|
|
7
8
|
entryCurvature?: number;
|
|
9
|
+
edgeStyle?: EdgeStyle;
|
|
8
10
|
nodeSizePreset?: NodeSizePreset;
|
|
9
11
|
}
|
|
10
12
|
/**
|
|
@@ -16,6 +18,7 @@ export declare function renderGoalsSvg(tree: GoalTree, options?: RenderGoalsOpti
|
|
|
16
18
|
export interface RenderGoalsLayoutOptions {
|
|
17
19
|
curvature?: number;
|
|
18
20
|
entryCurvature?: number;
|
|
21
|
+
edgeStyle?: EdgeStyle;
|
|
19
22
|
/** Extra vertical space reserved at the top of the canvas (e.g. for a title block). */
|
|
20
23
|
topInset?: number;
|
|
21
24
|
/** Raw SVG injected immediately after `<defs>` — a header line or a full title block. */
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { layoutGoalTree } from '../goals/layout.js';
|
|
14
14
|
import { displayGoalId } from '../goals/parse-canonical.js';
|
|
15
|
-
import {
|
|
15
|
+
import { previewEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
|
|
16
16
|
import { parseNodeSizePreset, resolveGoalsNodeSize } from '../node-size-presets.js';
|
|
17
17
|
import { generateSvgEmbedCss } from '../theme/index.js';
|
|
18
18
|
import { emitCenteredTextSvg, layoutCenteredEntityText } from './entity-text-layout.js';
|
|
@@ -27,7 +27,7 @@ const PAD = 24;
|
|
|
27
27
|
* with the shared theme CSS embedded so the output is self-contained.
|
|
28
28
|
*/
|
|
29
29
|
export function renderGoalsSvg(tree, options = {}) {
|
|
30
|
-
const { treeName = '', curvature = DEFAULT_EDGE_CURVATURE, entryCurvature, nodeSizePreset = 'normal' } = options;
|
|
30
|
+
const { treeName = '', curvature = DEFAULT_EDGE_CURVATURE, entryCurvature, edgeStyle, nodeSizePreset = 'normal' } = options;
|
|
31
31
|
const nodeSize = resolveGoalsNodeSize(parseNodeSizePreset(nodeSizePreset));
|
|
32
32
|
const layout = layoutGoalTree(tree, {
|
|
33
33
|
nodeWidth: nodeSize.width,
|
|
@@ -41,7 +41,7 @@ export function renderGoalsSvg(tree, options = {}) {
|
|
|
41
41
|
const title = treeName
|
|
42
42
|
? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(`Goal tree — ${treeName}`)}</text>`
|
|
43
43
|
: '';
|
|
44
|
-
return renderGoalsLayoutSvg(layout, { curvature, entryCurvature, title, embedCssTheme: 'transitrix' });
|
|
44
|
+
return renderGoalsLayoutSvg(layout, { curvature, entryCurvature, edgeStyle, title, embedCssTheme: 'transitrix' });
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
47
|
* The single goals SVG emitter shared by every host. Takes an already-computed
|
|
@@ -52,7 +52,7 @@ export function renderGoalsSvg(tree, options = {}) {
|
|
|
52
52
|
* and the export path own styling).
|
|
53
53
|
*/
|
|
54
54
|
export function renderGoalsLayoutSvg(layout, options = {}) {
|
|
55
|
-
const { curvature = DEFAULT_EDGE_CURVATURE, entryCurvature, topInset = 0, title = '', embedCssTheme } = options;
|
|
55
|
+
const { curvature = DEFAULT_EDGE_CURVATURE, entryCurvature, edgeStyle, topInset = 0, title = '', embedCssTheme } = options;
|
|
56
56
|
const w = layout.bounds.width + PAD * 2;
|
|
57
57
|
const h = layout.bounds.height + PAD * 2 + topInset;
|
|
58
58
|
const ox = -layout.bounds.x + PAD;
|
|
@@ -67,7 +67,7 @@ export function renderGoalsLayoutSvg(layout, options = {}) {
|
|
|
67
67
|
const sy = s.y + oy + s.height / 2;
|
|
68
68
|
const tx = t.x + ox;
|
|
69
69
|
const ty = t.y + oy + t.height / 2;
|
|
70
|
-
return
|
|
70
|
+
return previewEdgePath(sx, sy, tx, ty, edgeStyle, curvature, entryCurvature);
|
|
71
71
|
}
|
|
72
72
|
const edgeSvg = layout.edges
|
|
73
73
|
.map((e) => `<path d="${edgePath(e)}" class="diagram-edge" marker-end="url(#arrow)"/>`)
|
|
@@ -92,7 +92,12 @@ export function renderGoalsLayoutSvg(layout, options = {}) {
|
|
|
92
92
|
idMaxLines: 1,
|
|
93
93
|
});
|
|
94
94
|
const textSvg = emitCenteredTextSvg(specs, x + n.width / 2, escXml);
|
|
95
|
+
const titleParts = [escXml(labelText), escXml(idText)];
|
|
96
|
+
if (n.data.level !== undefined)
|
|
97
|
+
titleParts.push(`level ${n.data.level}`);
|
|
98
|
+
const titleContent = titleParts.join(' — ');
|
|
95
99
|
return `<g>
|
|
100
|
+
<title>${titleContent}</title>
|
|
96
101
|
<rect class="diagram-node level-${level}" x="${x}" y="${y}" width="${n.width}" height="${n.height}" rx="${ENTITY_NODE_RX}"/>
|
|
97
102
|
${textSvg}
|
|
98
103
|
</g>`;
|
package/package.json
CHANGED
package/src/edge-path.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// Shared edge geometry for the static notation previews (Goals, FGCA, FGA,
|
|
2
|
-
// Activities).
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// arrow reads as perpendicular to the node's vertical edge.
|
|
2
|
+
// Activities). The default path is a cubic Bézier with horizontal control
|
|
3
|
+
// handles so the marker-end arrow sits flush on the node's vertical edge.
|
|
4
|
+
// Goals / DGCA / DGA can switch to a straight chord or an orthogonal polyline.
|
|
6
5
|
//
|
|
7
6
|
// Pulling the path math here (rather than duplicating it inline in each
|
|
8
7
|
// preview) makes the configurable-curvature behaviour
|
|
@@ -30,6 +29,30 @@ const MAX_HANDLE = 160;
|
|
|
30
29
|
/** Multiplier applied to the base handle length. 1 = historical appearance. */
|
|
31
30
|
export const DEFAULT_EDGE_CURVATURE = 1;
|
|
32
31
|
|
|
32
|
+
/** Path shape for Goals / DGCA / DGA preview edges. Default matches today's cubic. */
|
|
33
|
+
export type EdgeStyle = 'straight' | 'bezier' | 'polyline';
|
|
34
|
+
export const DEFAULT_EDGE_STYLE: EdgeStyle = 'bezier';
|
|
35
|
+
|
|
36
|
+
export function parseEdgeStyle(v: unknown): EdgeStyle {
|
|
37
|
+
if (v === 'straight' || v === 'bezier' || v === 'polyline') return v;
|
|
38
|
+
return DEFAULT_EDGE_STYLE;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Straight segment — the arrowhead follows the chord, not the node edge. */
|
|
42
|
+
export function straightEdgePath(sx: number, sy: number, tx: number, ty: number): string {
|
|
43
|
+
return `M${sx},${sy} L${tx},${ty}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Orthogonal (Manhattan) polyline: horizontal out, vertical, horizontal in.
|
|
48
|
+
* The last segment is horizontal so `marker-end` still meets the node's
|
|
49
|
+
* vertical edge the same way the cubic does.
|
|
50
|
+
*/
|
|
51
|
+
export function orthogonalEdgePath(sx: number, sy: number, tx: number, ty: number): string {
|
|
52
|
+
const midX = sx + (tx - sx) / 2;
|
|
53
|
+
return `M${sx},${sy} H${midX} V${ty} H${tx}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
33
56
|
/**
|
|
34
57
|
* Builds the SVG `d` for a horizontal-tangent cubic Bézier from (sx,sy) to
|
|
35
58
|
* (tx,ty).
|
|
@@ -89,3 +112,21 @@ export function bowedCubicEdgePath(
|
|
|
89
112
|
const handle = Math.max(EDGE_MIN_HANDLE, Math.min(Math.abs(dx) * DX_FACTOR, MAX_HANDLE)) * curvature;
|
|
90
113
|
return `M${sx},${sy} C${sx + handle},${bowY} ${tx - handle},${bowY} ${tx},${ty}`;
|
|
91
114
|
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Picks the SVG `d` for a Goals / DGCA / DGA preview edge.
|
|
118
|
+
* Unknown styles fall back to the historical cubic.
|
|
119
|
+
*/
|
|
120
|
+
export function previewEdgePath(
|
|
121
|
+
sx: number,
|
|
122
|
+
sy: number,
|
|
123
|
+
tx: number,
|
|
124
|
+
ty: number,
|
|
125
|
+
style: EdgeStyle = DEFAULT_EDGE_STYLE,
|
|
126
|
+
curvature: number = DEFAULT_EDGE_CURVATURE,
|
|
127
|
+
entryCurvature?: number,
|
|
128
|
+
): string {
|
|
129
|
+
if (style === 'straight') return straightEdgePath(sx, sy, tx, ty);
|
|
130
|
+
if (style === 'polyline') return orthogonalEdgePath(sx, sy, tx, ty);
|
|
131
|
+
return horizontalCubicEdgePath(sx, sy, tx, ty, curvature, entryCurvature);
|
|
132
|
+
}
|
package/src/fgca/index.ts
CHANGED
|
@@ -89,11 +89,14 @@ export function isDgcaChangesLayerOff(input: unknown): boolean {
|
|
|
89
89
|
* one action or change references an out-of-scope goal, `scopeCaption` is set true.
|
|
90
90
|
* @param outOfScopeFactorIds When a goal-scoped projection omits factors that exist
|
|
91
91
|
* in the catalogue, pass their IDs here to suppress FGCA-008 errors.
|
|
92
|
+
* @param progressData Optional map of action ID to progress data (percent + computedAt timestamp).
|
|
93
|
+
* Consumed from `__progressData` metadata field if provided there.
|
|
92
94
|
*/
|
|
93
95
|
export function parseCanonicalFGCA(
|
|
94
96
|
input: unknown,
|
|
95
97
|
outOfScopeGoalIds?: Set<string> | null,
|
|
96
98
|
outOfScopeFactorIds?: Set<string> | null,
|
|
99
|
+
progressData?: Map<string, { percent: number; computedAt: string }> | null,
|
|
97
100
|
): CanonicalFGCAResult {
|
|
98
101
|
const errors: ValidationError[] = [];
|
|
99
102
|
const warnings: ValidationWarning[] = [];
|
|
@@ -322,11 +325,14 @@ export function parseCanonicalFGCA(
|
|
|
322
325
|
const internalActivities: ActivityItem[] = activities.map((el, i) => {
|
|
323
326
|
const goalRefs = checkRefArray(el!, 'goals', goalIds, GOAL_ID_RE, 'FGCA-011', `actions[${i}]`, outOfScopeGoalIds);
|
|
324
327
|
const typeVal = typeof el!['type'] === 'string' ? el!['type'] : undefined;
|
|
328
|
+
const actId = String(el!['id']);
|
|
329
|
+
const progressVal = progressData?.get(actId);
|
|
325
330
|
return {
|
|
326
|
-
id:
|
|
331
|
+
id: actId,
|
|
327
332
|
name: String(el!['name'] ?? ''),
|
|
328
333
|
goal_id: goalRefs.length > 0 ? goalRefs[0] : null,
|
|
329
334
|
...(typeVal !== undefined ? { type: typeVal } : {}),
|
|
335
|
+
...(progressVal !== undefined ? { progress: progressVal } : {}),
|
|
330
336
|
};
|
|
331
337
|
});
|
|
332
338
|
|
|
@@ -400,6 +406,7 @@ export function parseCanonicalFGA(
|
|
|
400
406
|
input: unknown,
|
|
401
407
|
outOfScopeGoalIds?: Set<string> | null,
|
|
402
408
|
outOfScopeFactorIds?: Set<string> | null,
|
|
409
|
+
progressData?: Map<string, { percent: number; computedAt: string }> | null,
|
|
403
410
|
): CanonicalFGAResult {
|
|
404
411
|
if (!input || typeof input !== 'object') {
|
|
405
412
|
return { valid: false, errors: [{ code: 'FGA-001', message: 'document root is not an object' }], warnings: [] };
|
|
@@ -428,7 +435,7 @@ export function parseCanonicalFGA(
|
|
|
428
435
|
// codes are remapped on the way out. FGCA-009 / 010 / 014 (changes-related)
|
|
429
436
|
// are unreachable here because `changes` is empty.
|
|
430
437
|
const synth = { ...raw, notation: 'dgca', id: 'DGCA-FROM-DGA-1', changes: [] };
|
|
431
|
-
const r = parseCanonicalFGCA(synth, outOfScopeGoalIds, outOfScopeFactorIds);
|
|
438
|
+
const r = parseCanonicalFGCA(synth, outOfScopeGoalIds, outOfScopeFactorIds, progressData);
|
|
432
439
|
const remap: Record<string, string> = {
|
|
433
440
|
'FGCA-001': 'FGA-001',
|
|
434
441
|
'FGCA-002': 'FGA-002',
|