@transitrix/diagrams 1.0.0 → 1.1.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/LICENSE +21 -21
- package/README.md +47 -47
- package/dist/__tests__/edge-path.test.js +8 -0
- package/dist/activities/cpm.js +11 -2
- package/dist/activities/gantt.js +6 -3
- package/dist/activities/types.d.ts +1 -0
- package/dist/activities/validate.js +3 -3
- package/dist/activity-card/__tests__/example.test.js +20 -1
- package/dist/activity-card/__tests__/layout.test.js +102 -11
- package/dist/activity-card/__tests__/resolver.test.js +161 -7
- package/dist/activity-card/index.d.ts +1 -1
- package/dist/activity-card/layout.d.ts +1 -1
- package/dist/activity-card/layout.js +220 -103
- package/dist/activity-card/resolver.js +115 -28
- package/dist/activity-card/types.d.ts +150 -27
- package/dist/activity-card/validate.js +3 -0
- package/dist/actor/__tests__/validate.test.js +63 -31
- package/dist/actor/index.d.ts +1 -1
- package/dist/actor/index.js +1 -1
- package/dist/actor/types.d.ts +7 -3
- package/dist/actor/types.js +17 -3
- package/dist/actor/validate.js +55 -26
- package/dist/assertion/types.d.ts +3 -1
- package/dist/assertion/types.js +1 -1
- package/dist/change/__tests__/validate.test.js +40 -34
- package/dist/change/index.d.ts +1 -0
- package/dist/change/types.d.ts +1 -1
- package/dist/change/types.js +2 -2
- package/dist/change/validate.d.ts +6 -1
- package/dist/change/validate.js +68 -41
- package/dist/compliance/__tests__/classify.test.js +34 -0
- package/dist/compliance/__tests__/coverage-metric.test.js +146 -46
- package/dist/compliance/__tests__/html.test.js +2 -2
- package/dist/compliance/__tests__/impact.test.js +509 -6
- package/dist/compliance/classify.d.ts +6 -0
- package/dist/compliance/classify.js +9 -1
- package/dist/compliance/coverage-metric.d.ts +31 -11
- package/dist/compliance/coverage-metric.js +149 -39
- package/dist/compliance/html.js +78 -81
- package/dist/compliance/impact.d.ts +100 -16
- package/dist/compliance/impact.js +272 -28
- package/dist/compliance/index.d.ts +2 -2
- package/dist/compliance/index.js +1 -1
- package/dist/compliance/markdown.js +1 -0
- package/dist/compliance/types.d.ts +25 -6
- package/dist/edge-path.d.ts +8 -4
- package/dist/edge-path.js +11 -6
- package/dist/factor/__tests__/validate.test.js +75 -46
- package/dist/factor/index.d.ts +3 -2
- package/dist/factor/index.js +1 -1
- package/dist/factor/types.d.ts +5 -8
- package/dist/factor/types.js +10 -5
- package/dist/factor/validate.d.ts +6 -1
- package/dist/factor/validate.js +68 -50
- package/dist/fgca/__tests__/chain-table.test.js +2 -2
- package/dist/fgca/__tests__/layout.test.js +11 -11
- package/dist/fgca/__tests__/preview-layout.test.js +7 -7
- package/dist/fgca/__tests__/resolver.test.js +185 -0
- package/dist/fgca/chain-table.d.ts +1 -1
- package/dist/fgca/chain-table.js +4 -4
- package/dist/fgca/index.d.ts +4 -2
- package/dist/fgca/index.js +2 -1
- package/dist/fgca/layout.d.ts +2 -2
- package/dist/fgca/layout.js +9 -9
- package/dist/fgca/nodeTypes.d.ts +1 -1
- package/dist/fgca/nodeTypes.js +2 -2
- package/dist/fgca/nodes/FGCAFactorNode.d.ts +3 -3
- package/dist/fgca/nodes/FGCAFactorNode.js +5 -5
- package/dist/fgca/parse-canonical.js +15 -10
- package/dist/fgca/preview-layout.d.ts +1 -1
- package/dist/fgca/preview-layout.js +4 -4
- package/dist/fgca/resolver.d.ts +49 -0
- package/dist/fgca/resolver.js +126 -0
- package/dist/fgca/types.d.ts +11 -11
- package/dist/fgca/types.js +2 -2
- package/dist/fgca/validate.d.ts +2 -2
- package/dist/fgca/validate.js +17 -14
- package/dist/goals/index.d.ts +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/process-blueprint/__tests__/layout.test.js +37 -0
- package/dist/process-blueprint/index.d.ts +1 -1
- package/dist/process-blueprint/layout.js +66 -35
- package/dist/process-blueprint/types.d.ts +36 -1
- package/dist/scenarios/index.d.ts +1 -1
- package/dist/scenarios/types.d.ts +4 -4
- package/dist/stakeholder/__tests__/validate.test.js +49 -47
- package/dist/stakeholder/index.d.ts +1 -0
- package/dist/stakeholder/types.d.ts +3 -4
- package/dist/stakeholder/types.js +2 -3
- package/dist/stakeholder/validate.d.ts +6 -1
- package/dist/stakeholder/validate.js +71 -50
- package/dist/target-state/__tests__/validate.test.js +43 -27
- package/dist/target-state/index.d.ts +2 -0
- package/dist/target-state/index.js +1 -0
- package/dist/target-state/types.d.ts +6 -1
- package/dist/target-state/types.js +8 -7
- package/dist/target-state/validate.d.ts +6 -1
- package/dist/target-state/validate.js +69 -45
- package/dist/theme/themes.js +71 -69
- package/dist/theme/tokens.d.ts +7 -1
- package/dist/theme/tokens.js +3 -1
- package/dist/webview/__tests__/entry-notations.test.js +2 -0
- package/dist/webview/__tests__/entry.test.js +11 -11
- package/dist/webview/__tests__/render-process.test.js +463 -0
- package/dist/webview/__tests__/render-util.test.d.ts +1 -0
- package/dist/webview/__tests__/render-util.test.js +54 -0
- package/dist/webview/entry.d.ts +1 -1
- package/dist/webview/entry.js +9 -5
- package/dist/webview/render-activities.d.ts +24 -2
- package/dist/webview/render-activities.js +92 -57
- package/dist/webview/render-activity-card.d.ts +11 -1
- package/dist/webview/render-activity-card.js +115 -79
- package/dist/webview/render-blocks.d.ts +24 -1
- package/dist/webview/render-blocks.js +58 -24
- package/dist/webview/render-fgca.d.ts +31 -0
- package/dist/webview/render-fgca.js +30 -19
- package/dist/webview/render-goals.d.ts +27 -1
- package/dist/webview/render-goals.js +42 -32
- package/dist/webview/render-process-blueprint.d.ts +11 -1
- package/dist/webview/render-process-blueprint.js +118 -53
- package/dist/webview/render-process.d.ts +95 -0
- package/dist/webview/render-process.js +392 -0
- package/dist/webview/render-scenarios.js +2 -2
- package/dist/webview/render-util.d.ts +19 -6
- package/dist/webview/render-util.js +23 -15
- package/package.json +54 -40
- package/src/__tests__/edge-path.test.ts +58 -46
- package/src/__tests__/scope.test.ts +27 -27
- package/src/__tests__/yaml-normalize.test.ts +158 -158
- package/src/activities/__tests__/cpm.test.ts +164 -164
- package/src/activities/__tests__/gantt.test.ts +245 -245
- package/src/activities/__tests__/layout.test.ts +112 -112
- package/src/activities/__tests__/validate.test.ts +409 -409
- package/src/activities/cpm.ts +107 -95
- package/src/activities/gantt.ts +277 -276
- package/src/activities/index.ts +28 -28
- package/src/activities/layout.ts +112 -112
- package/src/activities/types.ts +149 -148
- package/src/activities/validate.ts +357 -357
- package/src/activity-card/__tests__/example.test.ts +85 -66
- package/src/activity-card/__tests__/layout.test.ts +169 -63
- package/src/activity-card/__tests__/resolver.test.ts +330 -161
- package/src/activity-card/__tests__/validate.test.ts +131 -131
- package/src/activity-card/index.ts +38 -32
- package/src/activity-card/layout.ts +324 -194
- package/src/activity-card/resolver.ts +422 -337
- package/src/activity-card/types.ts +376 -237
- package/src/activity-card/validate.ts +162 -158
- package/src/actor/__tests__/validate.test.ts +70 -31
- package/src/actor/index.ts +1 -1
- package/src/actor/types.ts +21 -7
- package/src/actor/validate.ts +59 -27
- package/src/applications/__tests__/validate.test.ts +256 -256
- package/src/applications/index.ts +11 -11
- package/src/applications/types.ts +43 -43
- package/src/applications/validate.ts +131 -131
- package/src/assertion/__tests__/example.test.ts +37 -37
- package/src/assertion/__tests__/validate.test.ts +142 -142
- package/src/assertion/index.ts +12 -12
- package/src/assertion/types.ts +60 -58
- package/src/assertion/validate.ts +145 -145
- package/src/blocks/__tests__/layout.test.ts +169 -169
- package/src/blocks/__tests__/validate.test.ts +260 -260
- package/src/blocks/index.ts +17 -17
- package/src/blocks/layout.ts +186 -186
- package/src/blocks/types.ts +62 -62
- package/src/blocks/validate.ts +206 -206
- package/src/capability-map/__tests__/validate.test.ts +238 -238
- package/src/capability-map/index.ts +8 -8
- package/src/capability-map/types.ts +29 -29
- package/src/capability-map/validate.ts +127 -127
- package/src/change/__tests__/validate.test.ts +43 -34
- package/src/change/index.ts +1 -0
- package/src/change/types.ts +5 -3
- package/src/change/validate.ts +75 -43
- package/src/compliance/__tests__/classify.test.ts +64 -27
- package/src/compliance/__tests__/confidence.test.ts +98 -98
- package/src/compliance/__tests__/coverage-metric.test.ts +300 -188
- package/src/compliance/__tests__/demo-scenario.test.ts +273 -273
- package/src/compliance/__tests__/example.test.ts +57 -57
- package/src/compliance/__tests__/gap-report.test.ts +128 -128
- package/src/compliance/__tests__/html.test.ts +141 -141
- package/src/compliance/__tests__/impact.test.ts +1059 -481
- package/src/compliance/__tests__/markdown.test.ts +94 -94
- package/src/compliance/__tests__/views.test.ts +64 -64
- package/src/compliance/classify.ts +100 -84
- package/src/compliance/confidence.ts +69 -69
- package/src/compliance/coverage-metric.ts +404 -263
- package/src/compliance/gap-report.ts +94 -94
- package/src/compliance/html.ts +291 -295
- package/src/compliance/impact.ts +979 -677
- package/src/compliance/index.ts +57 -56
- package/src/compliance/markdown.ts +136 -135
- package/src/compliance/reverse-index.ts +33 -33
- package/src/compliance/types.ts +136 -120
- package/src/compliance/views.ts +46 -46
- package/src/compliance-matrix/__tests__/build.test.ts +200 -200
- package/src/compliance-matrix/__tests__/example.test.ts +57 -57
- package/src/compliance-matrix/index.ts +11 -11
- package/src/compliance-matrix/types.ts +102 -102
- package/src/edge-path.ts +50 -44
- package/src/factor/__tests__/validate.test.ts +81 -46
- package/src/factor/index.ts +3 -7
- package/src/factor/types.ts +15 -19
- package/src/factor/validate.ts +77 -51
- package/src/fgca/__tests__/chain-table.test.ts +172 -172
- package/src/fgca/__tests__/layout.test.ts +179 -179
- package/src/fgca/__tests__/parse-canonical.test.ts +248 -248
- package/src/fgca/__tests__/preview-layout.test.ts +124 -124
- package/src/fgca/__tests__/resolver.test.ts +209 -0
- package/src/fgca/__tests__/validate.test.ts +142 -142
- package/src/fgca/chain-table.ts +225 -225
- package/src/fgca/index.ts +47 -45
- package/src/fgca/layout.ts +204 -204
- package/src/fgca/nodeTypes.ts +11 -11
- package/src/fgca/nodes/FGCAActivityNode.tsx +65 -65
- package/src/fgca/nodes/FGCAChangeNode.tsx +64 -64
- package/src/fgca/nodes/FGCAFactorNode.tsx +64 -64
- package/src/fgca/nodes/FGCAGoalNode.tsx +72 -72
- package/src/fgca/parse-canonical.ts +405 -400
- package/src/fgca/preview-layout.ts +217 -217
- package/src/fgca/resolver.ts +159 -0
- package/src/fgca/types.ts +50 -50
- package/src/fgca/validate.ts +129 -126
- package/src/geometry.ts +9 -9
- package/src/goals/__tests__/layout.test.ts +175 -175
- package/src/goals/__tests__/mutations.test.ts +111 -111
- package/src/goals/__tests__/parse-canonical.test.ts +145 -145
- package/src/goals/__tests__/validate.test.ts +202 -202
- package/src/goals/index.ts +20 -21
- package/src/goals/layout.ts +172 -172
- package/src/goals/mutations.ts +123 -123
- package/src/goals/parse-canonical.ts +286 -286
- package/src/goals/types.ts +85 -85
- package/src/goals/validate.ts +136 -136
- package/src/index.ts +28 -23
- package/src/process-blueprint/__tests__/compliance-lane.test.ts +320 -320
- package/src/process-blueprint/__tests__/layout.test.ts +240 -195
- package/src/process-blueprint/__tests__/validate.test.ts +310 -310
- package/src/process-blueprint/index.ts +32 -31
- package/src/process-blueprint/layout.ts +564 -531
- package/src/process-blueprint/types.ts +278 -242
- package/src/process-blueprint/validate.ts +181 -181
- package/src/process-map/__tests__/validate.test.ts +223 -223
- package/src/process-map/index.ts +10 -10
- package/src/process-map/types.ts +36 -36
- package/src/process-map/validate.ts +135 -135
- package/src/products/__tests__/validate.test.ts +239 -239
- package/src/products/index.ts +9 -9
- package/src/products/types.ts +31 -31
- package/src/products/validate.ts +112 -112
- package/src/repo-validate/__tests__/validate-repo.test.ts +149 -149
- package/src/repo-validate/index.ts +2 -2
- package/src/repo-validate/types.ts +50 -50
- package/src/repo-validate/validate-repo.ts +196 -196
- package/src/requirement/__tests__/example.test.ts +24 -24
- package/src/requirement/__tests__/validate.test.ts +96 -96
- package/src/requirement/index.ts +4 -4
- package/src/requirement/types.ts +35 -35
- package/src/requirement/validate.ts +92 -92
- package/src/scenarios/__tests__/validate.test.ts +209 -209
- package/src/scenarios/index.ts +15 -15
- package/src/scenarios/types.ts +37 -37
- package/src/scenarios/validate.ts +122 -122
- package/src/schema-version.ts +16 -16
- package/src/scope.ts +46 -46
- package/src/stakeholder/__tests__/validate.test.ts +51 -47
- package/src/stakeholder/index.ts +1 -0
- package/src/stakeholder/types.ts +7 -7
- package/src/stakeholder/validate.ts +79 -52
- package/src/target-state/__tests__/validate.test.ts +48 -27
- package/src/target-state/index.ts +2 -0
- package/src/target-state/types.ts +11 -7
- package/src/target-state/validate.ts +76 -46
- package/src/theme/index.ts +4 -4
- package/src/theme/themes.ts +239 -237
- package/src/theme/tokens.ts +135 -133
- package/src/typed-id.ts +56 -56
- package/src/validation-types.ts +36 -36
- package/src/webview/__tests__/entry-notations.test.ts +79 -77
- package/src/webview/__tests__/entry.test.ts +64 -64
- package/src/webview/__tests__/render-goals.test.ts +72 -72
- package/src/webview/__tests__/render-process.test.ts +538 -0
- package/src/webview/__tests__/render-util.test.ts +66 -0
- package/src/webview/entry.ts +298 -292
- package/src/webview/render-activities.ts +111 -69
- package/src/webview/render-activity-card.ts +275 -234
- package/src/webview/render-blocks.ts +78 -28
- package/src/webview/render-fgca.ts +53 -28
- package/src/webview/render-goals.ts +133 -110
- package/src/webview/render-process-blueprint.ts +281 -207
- package/src/webview/render-process.ts +507 -0
- package/src/webview/render-scenarios.ts +3 -3
- package/src/webview/render-util.ts +27 -14
- package/src/webview/styles.css +322 -323
- package/src/yaml-normalize.ts +56 -56
- package/dist/element-envelope.d.ts +0 -15
- package/dist/element-envelope.js +0 -55
- package/dist/issues/__tests__/layout.test.js +0 -62
- package/dist/issues/__tests__/validate.test.js +0 -82
- package/dist/issues/index.d.ts +0 -4
- package/dist/issues/index.js +0 -2
- package/dist/issues/layout.d.ts +0 -10
- package/dist/issues/layout.js +0 -82
- package/dist/issues/types.d.ts +0 -61
- package/dist/issues/types.js +0 -6
- package/dist/issues/validate.d.ts +0 -3
- package/dist/issues/validate.js +0 -143
- package/dist/webview/transitrix-render.css +0 -323
- package/dist/webview/transitrix-render.js +0 -6452
- package/dist/webview/transitrix-render.js.map +0 -7
- package/src/element-envelope.ts +0 -76
- /package/dist/{issues/__tests__/layout.test.d.ts → fgca/__tests__/resolver.test.d.ts} +0 -0
- /package/dist/{issues/__tests__/validate.test.d.ts → webview/__tests__/render-process.test.d.ts} +0 -0
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
* Only the DEFAULT network view (Project Schedule Network Diagram) is ported.
|
|
12
12
|
* The Gantt view, the CSS-only tab switcher and the interactive spacing /
|
|
13
13
|
* curvature controls stay in the VS Code preview.
|
|
14
|
+
*
|
|
15
|
+
* Single-emitter unification (review C): the canonical network body lives here
|
|
16
|
+
* in `renderActivitiesNetworkBody` and is shared verbatim with the VS Code
|
|
17
|
+
* preview's `networkSvg`.
|
|
14
18
|
*/
|
|
15
19
|
import { layoutActivities } from '../activities/layout.js';
|
|
16
20
|
import { computeCpm } from '../activities/cpm.js';
|
|
@@ -20,54 +24,46 @@ import { escXml } from './render-util.js';
|
|
|
20
24
|
const N_NODE_W = 200;
|
|
21
25
|
const N_NODE_H = 80;
|
|
22
26
|
const N_PAD = 24;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Network-view diagram CSS (critical path, milestones, edge colours) — the
|
|
29
|
+
* canonical rules shared with the VS Code preview's `ACTIVITIES_DIAGRAM_CSS`
|
|
30
|
+
* network subset. Embedded alongside the shared theme CSS so the SVG is
|
|
31
|
+
* self-contained for the JCEF host.
|
|
32
|
+
*/
|
|
33
|
+
export const ACTIVITIES_NETWORK_CSS = `
|
|
34
|
+
.act-node { fill: var(--ts-layer-activity, #d4edda); stroke: var(--ts-node-stroke, #94a3b8); stroke-width: 1; }
|
|
29
35
|
.critical-node { fill: #fff7ed; stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 2.5; }
|
|
30
36
|
.milestone-node { fill: #ecfeff; stroke: var(--ts-text-muted, #64748b); stroke-dasharray: 4 2; }
|
|
31
37
|
.critical-edge { stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 2; }
|
|
32
38
|
.arrow-fill-critical { fill: var(--ts-brand-orange, #ff4d00); }
|
|
33
39
|
`;
|
|
40
|
+
/** Arrowhead marker defs shared by both hosts for the network view. */
|
|
41
|
+
export const ACTIVITIES_NETWORK_DEFS = `<defs>
|
|
42
|
+
<marker id="arrow" markerWidth="8" markerHeight="8" refX="8" refY="3" orient="auto">
|
|
43
|
+
<path d="M0,0 L0,6 L8,3 z" class="arrow-fill"/>
|
|
44
|
+
</marker>
|
|
45
|
+
<marker id="arrow-crit" markerWidth="8" markerHeight="8" refX="8" refY="3" orient="auto">
|
|
46
|
+
<path d="M0,0 L0,6 L8,3 z" class="arrow-fill-critical"/>
|
|
47
|
+
</marker>
|
|
48
|
+
</defs>`;
|
|
34
49
|
function truncate(s, maxLen) {
|
|
35
50
|
return s.length > maxLen ? s.slice(0, maxLen - 1) + '…' : s;
|
|
36
51
|
}
|
|
37
52
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* `validateActivities` returns valid (the activities module has no parsed
|
|
43
|
-
* field, so the dispatcher does `doc as ActivityDoc`).
|
|
53
|
+
* Canonical Network (PSND) body — the node rects and edge paths that go inside
|
|
54
|
+
* the `<svg>`, shared verbatim by the VS Code preview and the host-neutral
|
|
55
|
+
* wrapper below. Excludes the host-specific title block and the (identical)
|
|
56
|
+
* marker defs (`ACTIVITIES_NETWORK_DEFS`).
|
|
44
57
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* critical-path computation, which `computeCpm` backfills with neutral CPM
|
|
48
|
-
* values), so this renderer simply renders whatever the layout returns rather
|
|
49
|
-
* than short-circuiting.
|
|
58
|
+
* `ox`/`oy` are the canvas offsets (the caller folds in padding + any title
|
|
59
|
+
* height); `curvature`/`entryCurvature` scale the exit/entry edge handles.
|
|
50
60
|
*/
|
|
51
|
-
export function
|
|
52
|
-
const { title = '', gaps = {}, curvature = DEFAULT_EDGE_CURVATURE } = options;
|
|
53
|
-
const layout = layoutActivities(doc, gaps);
|
|
54
|
-
if (layout.nodes.length === 0) {
|
|
55
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
|
|
56
|
-
}
|
|
57
|
-
const cpm = computeCpm(doc.activities ?? []);
|
|
58
|
-
const titleH = title ? 24 : 0;
|
|
59
|
-
const w = layout.bounds.width + N_PAD * 2;
|
|
60
|
-
const h = layout.bounds.height + N_PAD * 2 + titleH;
|
|
61
|
-
const ox = -layout.bounds.x + N_PAD;
|
|
62
|
-
const oy = -layout.bounds.y + N_PAD + titleH;
|
|
61
|
+
export function renderActivitiesNetworkBody(layout, cpm, ox, oy, curvature, entryCurvature) {
|
|
63
62
|
const nodeMap = new Map(layout.nodes.map((n) => [n.id, n]));
|
|
64
63
|
// SVG paints later siblings over earlier ones. Sort non-critical first so the
|
|
65
64
|
// bright orange critical-path edges land on top — a gray edge crossing an
|
|
66
65
|
// orange one shouldn't bury the critical signal.
|
|
67
66
|
const orderedEdges = [...layout.edges].sort((a, b) => Number(Boolean(a.isCritical)) - Number(Boolean(b.isCritical)));
|
|
68
|
-
// Edge path = a single cubic Bézier with horizontal control handles (shared
|
|
69
|
-
// geometry in `edge-path.js`), so the curve meets the vertical node edge at a
|
|
70
|
-
// right angle and the marker-end arrow reads as perpendicular.
|
|
71
67
|
const edgeSvg = orderedEdges
|
|
72
68
|
.map((e) => {
|
|
73
69
|
const s = nodeMap.get(e.sourceId);
|
|
@@ -80,7 +76,7 @@ export function renderActivitiesSvg(doc, options = {}) {
|
|
|
80
76
|
const ty = t.y + oy + t.height / 2;
|
|
81
77
|
const cls = e.isCritical ? 'diagram-edge critical-edge' : 'diagram-edge';
|
|
82
78
|
const marker = `url(#${e.isCritical ? 'arrow-crit' : 'arrow'})`;
|
|
83
|
-
return `<path d="${horizontalCubicEdgePath(sx, sy, tx, ty, curvature)}" class="${cls}" marker-end="${marker}"/>`;
|
|
79
|
+
return `<path d="${horizontalCubicEdgePath(sx, sy, tx, ty, curvature, entryCurvature)}" class="${cls}" marker-end="${marker}"/>`;
|
|
84
80
|
})
|
|
85
81
|
.join('\n');
|
|
86
82
|
const nodeSvg = layout.nodes
|
|
@@ -88,40 +84,79 @@ export function renderActivitiesSvg(doc, options = {}) {
|
|
|
88
84
|
const x = n.x + ox;
|
|
89
85
|
const y = n.y + oy;
|
|
90
86
|
const isCritical = cpm.get(n.id)?.isCritical ?? false;
|
|
91
|
-
const
|
|
87
|
+
const durVal = n.data.duration;
|
|
88
|
+
const isMilestone = (durVal ?? -1) === 0;
|
|
92
89
|
const cls = `diagram-node act-node ${isCritical ? 'critical-node' : ''} ${isMilestone ? 'milestone-node' : ''}`.trim();
|
|
93
90
|
const idLabel = escXml(n.id);
|
|
94
|
-
const
|
|
95
|
-
|
|
91
|
+
const words = n.data.name.split(' ');
|
|
92
|
+
let line1 = '';
|
|
93
|
+
let line2 = '';
|
|
94
|
+
for (const w of words) {
|
|
95
|
+
if ((line1 + ' ' + w).trim().length <= 24)
|
|
96
|
+
line1 = (line1 + ' ' + w).trim();
|
|
97
|
+
else if ((line2 + ' ' + w).trim().length <= 24)
|
|
98
|
+
line2 = (line2 + ' ' + w).trim();
|
|
99
|
+
else if (!line2) {
|
|
100
|
+
line2 = w.slice(0, 22) + '…';
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const twoLines = line2.length > 0;
|
|
105
|
+
const nameY1 = twoLines ? y + 18 : y + 28;
|
|
106
|
+
const nameY2 = y + 34;
|
|
107
|
+
const idY = twoLines ? y + 54 : y + 50;
|
|
108
|
+
const durLabel = (durVal !== undefined && durVal > 0) ? `${durVal}d` : '';
|
|
96
109
|
return [
|
|
97
|
-
`<rect class="${cls}" x="${x}" y="${y}" width="${N_NODE_W}" height="${N_NODE_H}" rx="
|
|
98
|
-
`<text class="text-
|
|
99
|
-
`<text class="text-primary" x="${x + N_NODE_W / 2}" y="${
|
|
100
|
-
`<text class="text-
|
|
101
|
-
|
|
110
|
+
`<rect class="${cls}" x="${x}" y="${y}" width="${N_NODE_W}" height="${N_NODE_H}" rx="8"/>`,
|
|
111
|
+
`<text class="text-primary" x="${x + N_NODE_W / 2}" y="${nameY1}" text-anchor="middle" dominant-baseline="central">${escXml(line1)}</text>`,
|
|
112
|
+
twoLines ? `<text class="text-primary" x="${x + N_NODE_W / 2}" y="${nameY2}" text-anchor="middle" dominant-baseline="central">${escXml(line2)}</text>` : '',
|
|
113
|
+
`<text class="text-id" x="${x + N_NODE_W / 2}" y="${idY}" text-anchor="middle" dominant-baseline="central">${idLabel}</text>`,
|
|
114
|
+
durLabel ? `<text class="text-secondary" x="${x + N_NODE_W - 8}" y="${y + N_NODE_H - 8}" text-anchor="end">${durLabel}</text>` : '',
|
|
115
|
+
].filter(Boolean).join('\n');
|
|
102
116
|
})
|
|
103
117
|
.join('\n');
|
|
118
|
+
return `${nodeSvg}\n${edgeSvg}`;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Render the network (PSND) view of an already-validated ActivityDoc to a
|
|
122
|
+
* self-contained SVG string.
|
|
123
|
+
*
|
|
124
|
+
* The caller passes a doc cast from the yaml-parsed `unknown` after
|
|
125
|
+
* `validateActivities` returns valid (the activities module has no parsed
|
|
126
|
+
* field, so the dispatcher does `doc as ActivityDoc`).
|
|
127
|
+
*
|
|
128
|
+
* Cyclic graphs degrade gracefully: `layoutActivities` / `computeCpm` defend
|
|
129
|
+
* against cycles internally (Kahn's topo-order omits cyclic nodes from the
|
|
130
|
+
* critical-path computation, which `computeCpm` backfills with neutral CPM
|
|
131
|
+
* values), so this renderer simply renders whatever the layout returns rather
|
|
132
|
+
* than short-circuiting.
|
|
133
|
+
*/
|
|
134
|
+
export function renderActivitiesSvg(doc, options = {}) {
|
|
135
|
+
const { title = '', gaps = {}, curvature = DEFAULT_EDGE_CURVATURE, entryCurvature } = options;
|
|
136
|
+
const layout = layoutActivities(doc, gaps);
|
|
137
|
+
if (layout.nodes.length === 0) {
|
|
138
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
|
|
139
|
+
}
|
|
140
|
+
const cpm = computeCpm(doc.activities ?? []);
|
|
141
|
+
const titleH = title ? 24 : 0;
|
|
142
|
+
const w = layout.bounds.width + N_PAD * 2;
|
|
143
|
+
const h = layout.bounds.height + N_PAD * 2 + titleH;
|
|
144
|
+
const ox = -layout.bounds.x + N_PAD;
|
|
145
|
+
const oy = -layout.bounds.y + N_PAD + titleH;
|
|
146
|
+
const body = renderActivitiesNetworkBody(layout, cpm, ox, oy, curvature, entryCurvature);
|
|
104
147
|
const titleSvg = title
|
|
105
148
|
? `<text class="text-header" x="${N_PAD}" y="${N_PAD - 6}">${escXml(title)}</text>`
|
|
106
149
|
: '';
|
|
107
|
-
// Embed the shared theme CSS plus the
|
|
108
|
-
// the
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
const embedCss = generateSvgEmbedCss('transitrix') +
|
|
150
|
+
// Embed the shared theme CSS plus the network diagram CSS inside the SVG so
|
|
151
|
+
// the rendered output is self-contained — the JCEF host page only needs to
|
|
152
|
+
// drop the SVG into the DOM and styling resolves without any cooperation from
|
|
153
|
+
// the host stylesheet. Matches what the VS Code path produces via
|
|
154
|
+
// `prepareSvgForExport`.
|
|
155
|
+
const embedCss = generateSvgEmbedCss('transitrix') + ACTIVITIES_NETWORK_CSS;
|
|
113
156
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
|
|
114
157
|
<style>${embedCss}</style>
|
|
115
|
-
|
|
116
|
-
<marker id="arrow" markerWidth="8" markerHeight="8" refX="8" refY="3" orient="auto">
|
|
117
|
-
<path d="M0,0 L0,6 L8,3 z" class="arrow-fill"/>
|
|
118
|
-
</marker>
|
|
119
|
-
<marker id="arrow-crit" markerWidth="8" markerHeight="8" refX="8" refY="3" orient="auto">
|
|
120
|
-
<path d="M0,0 L0,6 L8,3 z" class="arrow-fill-critical"/>
|
|
121
|
-
</marker>
|
|
122
|
-
</defs>
|
|
158
|
+
${ACTIVITIES_NETWORK_DEFS}
|
|
123
159
|
${titleSvg}
|
|
124
|
-
${
|
|
125
|
-
${edgeSvg}
|
|
160
|
+
${body}
|
|
126
161
|
</svg>`;
|
|
127
162
|
}
|
|
@@ -1,5 +1,15 @@
|
|
|
1
|
-
import type { ActivityCardDoc } from '../activity-card/types.js';
|
|
1
|
+
import type { ActivityCardDoc, ActivityCardLayout } from '../activity-card/types.js';
|
|
2
2
|
export interface RenderActivityCardOptions {
|
|
3
3
|
title?: string;
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* The single Activity Card SVG body emitter shared by every host. Takes an
|
|
7
|
+
* already-laid-out {@link ActivityCardLayout} plus the canvas offset and emits
|
|
8
|
+
* the body elements (card, header, badges, dates, chain sections, milestones,
|
|
9
|
+
* child activities, footer) — everything except the host chrome (`<defs>`
|
|
10
|
+
* marker, title, embedded CSS, `<svg>` wrapper). The VS Code preview and the
|
|
11
|
+
* host-neutral {@link renderActivityCardSvg} both build their own chrome around
|
|
12
|
+
* this single source of truth.
|
|
13
|
+
*/
|
|
14
|
+
export declare function renderActivityCardBody(layout: ActivityCardLayout, ox: number, oy: number): string;
|
|
5
15
|
export declare function renderActivityCardSvg(doc: ActivityCardDoc, options?: RenderActivityCardOptions): string;
|
|
@@ -12,18 +12,17 @@
|
|
|
12
12
|
* CROSS-DOCUMENT RESOLUTION CAVEAT
|
|
13
13
|
* The Activity Card is the only MULTI-DOCUMENT Studio notation: the card YAML
|
|
14
14
|
* names a project Activity, and the project's name, dates, motivation chain
|
|
15
|
-
* (
|
|
16
|
-
* the canonical element + relation store (`canon/elements/**`,
|
|
15
|
+
* (Drivers → Assessments → Goals → Changes) and child activities are pulled BY
|
|
16
|
+
* REFERENCE from the canonical element + relation store (`canon/elements/**`,
|
|
17
17
|
* `canon/relations/**`; see `../activity-card/resolver.ts`). That resolution
|
|
18
18
|
* requires reading those files from disk, which is NOT available inside the
|
|
19
19
|
* JCEF host. We therefore resolve from the single in-memory card document
|
|
20
|
-
* ONLY: we synthesise
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* still renders its title, dates band and milestone timeline.
|
|
20
|
+
* ONLY: we synthesise a minimal `ResolvedActivityCard` whose project carries
|
|
21
|
+
* just the referenced project id (no resolved name/dates), whose milestones
|
|
22
|
+
* come straight from the card's own `milestones[]`, and whose chain sections
|
|
23
|
+
* and child activities are empty. The SVG layout is shared with the VS Code
|
|
24
|
+
* path via `layoutActivityCard`, so a single-document card still renders its
|
|
25
|
+
* title, dates band and milestone section.
|
|
27
26
|
*/
|
|
28
27
|
import { layoutActivityCard, ARCHIMATE_CLASS } from '../activity-card/layout.js';
|
|
29
28
|
import { generateSvgEmbedCss } from '../theme/index.js';
|
|
@@ -35,13 +34,6 @@ function truncate(text, maxChars) {
|
|
|
35
34
|
return text;
|
|
36
35
|
return text.slice(0, Math.max(0, maxChars - 1)) + '…';
|
|
37
36
|
}
|
|
38
|
-
/**
|
|
39
|
-
* Build a single-document resolution of the card: everything that can be read
|
|
40
|
-
* straight off the card YAML, with the cross-document fields left empty. This
|
|
41
|
-
* mirrors what `resolveActivityCard` produces, minus the parts that need the
|
|
42
|
-
* canon element + relation store (project name/dates, motivation chain, child
|
|
43
|
-
* activities).
|
|
44
|
-
*/
|
|
45
37
|
function resolveSingleDoc(doc) {
|
|
46
38
|
const card = doc.activity_card;
|
|
47
39
|
const rawMilestones = Array.isArray(card?.milestones) ? card.milestones : [];
|
|
@@ -62,74 +54,122 @@ function resolveSingleDoc(doc) {
|
|
|
62
54
|
// only. The layout falls back to '—' for the unknown date fields.
|
|
63
55
|
project: { id: card?.project ?? '', name: card?.project ?? '' },
|
|
64
56
|
milestones,
|
|
65
|
-
motivation: {
|
|
57
|
+
motivation: { drivers: [], goals: [], changes: [] },
|
|
58
|
+
assessments: [],
|
|
66
59
|
childActivities: [],
|
|
60
|
+
notes: card?.notes,
|
|
67
61
|
};
|
|
68
62
|
}
|
|
69
|
-
|
|
63
|
+
/**
|
|
64
|
+
* The single Activity Card SVG body emitter shared by every host. Takes an
|
|
65
|
+
* already-laid-out {@link ActivityCardLayout} plus the canvas offset and emits
|
|
66
|
+
* the body elements (card, header, badges, dates, chain sections, milestones,
|
|
67
|
+
* child activities, footer) — everything except the host chrome (`<defs>`
|
|
68
|
+
* marker, title, embedded CSS, `<svg>` wrapper). The VS Code preview and the
|
|
69
|
+
* host-neutral {@link renderActivityCardSvg} both build their own chrome around
|
|
70
|
+
* this single source of truth.
|
|
71
|
+
*/
|
|
72
|
+
export function renderActivityCardBody(layout, ox, oy) {
|
|
70
73
|
const parts = [];
|
|
71
74
|
// Outer card.
|
|
72
75
|
parts.push(`<rect class="diagram-node level-0" x="${ox}" y="${oy}" width="${layout.bounds.width}" height="${layout.bounds.height}" rx="8"/>`);
|
|
73
|
-
// Title
|
|
74
|
-
parts.push(`<text class="text-header" x="${layout.
|
|
75
|
-
//
|
|
76
|
+
// Title row.
|
|
77
|
+
parts.push(`<text class="text-header" x="${layout.titleRow.x + ox}" y="${layout.titleRow.y + oy}" style="dominant-baseline:central" font-size="18">${escXml(truncate(layout.titleRow.name, 60))}</text>`);
|
|
78
|
+
// Activity type badge.
|
|
79
|
+
if (layout.activityTypeBadge) {
|
|
80
|
+
const b = layout.activityTypeBadge;
|
|
81
|
+
parts.push(`<rect class="diagram-node level-2" x="${b.x + ox}" y="${b.y + oy}" width="${b.width}" height="${b.height}" rx="4"/>`);
|
|
82
|
+
parts.push(`<text class="text-secondary" style="dominant-baseline:central;text-anchor:middle" x="${b.x + ox + b.width / 2}" y="${b.y + oy + b.height / 2}">${escXml(b.label)}</text>`);
|
|
83
|
+
}
|
|
84
|
+
// Status badge.
|
|
85
|
+
if (layout.statusBadge) {
|
|
86
|
+
const b = layout.statusBadge;
|
|
87
|
+
parts.push(`<rect class="diagram-node level-3" x="${b.x + ox}" y="${b.y + oy}" width="${b.width}" height="${b.height}" rx="4"/>`);
|
|
88
|
+
parts.push(`<text class="text-secondary" style="dominant-baseline:central;text-anchor:middle" x="${b.x + ox + b.width / 2}" y="${b.y + oy + b.height / 2}">${escXml(b.label)}</text>`);
|
|
89
|
+
}
|
|
90
|
+
// Date fields.
|
|
76
91
|
for (const d of layout.dateFields) {
|
|
77
92
|
parts.push(`<rect class="diagram-node level-2" x="${d.x + ox}" y="${d.y + oy}" width="${d.width}" height="${d.height}" rx="6"/>`);
|
|
78
|
-
parts.push(`<text class="text-secondary" x="${d.x + ox + 12}" y="${d.y + oy + 18}" dominant-baseline
|
|
79
|
-
parts.push(`<text class="text-primary" x="${d.x + ox + 12}" y="${d.y + oy + 40}" dominant-baseline
|
|
93
|
+
parts.push(`<text class="text-secondary" x="${d.x + ox + 12}" y="${d.y + oy + 18}" style="dominant-baseline:central">${escXml(d.label)}</text>`);
|
|
94
|
+
parts.push(`<text class="text-primary" x="${d.x + ox + 12}" y="${d.y + oy + 40}" style="dominant-baseline:central">${escXml(d.value)}</text>`);
|
|
95
|
+
}
|
|
96
|
+
// Stakeholder role slots (2-column grid).
|
|
97
|
+
for (const s of layout.stakeholderRoleSlots) {
|
|
98
|
+
parts.push(`<rect class="diagram-node level-2" x="${s.x + ox}" y="${s.y + oy}" width="${s.width}" height="${s.height}" rx="6"/>`);
|
|
99
|
+
parts.push(`<text class="text-secondary" x="${s.x + ox + 10}" y="${s.y + oy + 16}" style="dominant-baseline:central">${escXml(s.role)}</text>`);
|
|
100
|
+
parts.push(`<text class="text-primary" x="${s.x + ox + 10}" y="${s.y + oy + 36}" style="dominant-baseline:central">${escXml(truncate(s.name, 40))}</text>`);
|
|
80
101
|
}
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
102
|
+
// Description row.
|
|
103
|
+
if (layout.descriptionRow) {
|
|
104
|
+
const r = layout.descriptionRow;
|
|
105
|
+
parts.push(`<rect class="diagram-node level-2" x="${r.x + ox}" y="${r.y + oy}" width="${r.width}" height="${r.height}" rx="6"/>`);
|
|
106
|
+
parts.push(`<text class="text-secondary" x="${r.x + ox + 12}" y="${r.y + oy + 22}" style="dominant-baseline:central">${escXml(r.label)}</text>`);
|
|
107
|
+
r.valueLines.forEach((line, i) => {
|
|
108
|
+
parts.push(`<text class="text-primary" x="${r.x + ox + 12}" y="${r.y + oy + 44 + i * 18}" style="dominant-baseline:central">${escXml(line)}</text>`);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// Chain sections (Drivers → Assessments → Goals → Changes).
|
|
112
|
+
const SECTION_LEVEL = {
|
|
113
|
+
drivers: 4,
|
|
114
|
+
assessments: 5,
|
|
115
|
+
goals: 5,
|
|
116
|
+
changes: 6,
|
|
117
|
+
};
|
|
118
|
+
for (let si = 0; si < layout.chainSections.length; si++) {
|
|
119
|
+
const section = layout.chainSections[si];
|
|
120
|
+
const level = SECTION_LEVEL[section.type] ?? 5;
|
|
121
|
+
// Section outer box.
|
|
122
|
+
parts.push(`<rect class="diagram-node level-1" x="${section.x + ox}" y="${section.y + oy}" width="${section.width}" height="${section.height}" rx="6"/>`);
|
|
123
|
+
// Section header label + subtitle.
|
|
124
|
+
parts.push(`<text class="text-header" x="${section.x + ox + 12}" y="${section.y + oy + 14}" style="dominant-baseline:central">${escXml(section.label)}<tspan class="text-secondary" font-size="11"> (${escXml(section.subtitle)})</tspan></text>`);
|
|
125
|
+
if (section.isEmpty) {
|
|
126
|
+
// Gap indicator.
|
|
127
|
+
parts.push(`<text class="text-secondary" x="${section.x + ox + 12}" y="${section.y + oy + 24 + 8 + 16}" style="dominant-baseline:central">— not on file</text>`);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
for (const n of section.nodes) {
|
|
131
|
+
parts.push(`<rect class="diagram-node level-${level}" x="${n.x + ox}" y="${n.y + oy}" width="${n.width}" height="${n.height}" rx="4"/>`);
|
|
132
|
+
parts.push(`<text class="text-primary" x="${n.x + ox + 10}" y="${n.y + oy + (n.meta ? 16 : n.height / 2)}" style="dominant-baseline:central">${escXml(truncate(n.name, 80))}</text>`);
|
|
133
|
+
if (n.meta) {
|
|
134
|
+
parts.push(`<text class="text-secondary" x="${n.x + ox + 10}" y="${n.y + oy + 30}" style="dominant-baseline:central">${escXml(n.meta)}</text>`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Connector arrow to next section.
|
|
139
|
+
if (si < layout.chainSections.length - 1) {
|
|
140
|
+
const nextSection = layout.chainSections[si + 1];
|
|
141
|
+
const arrowX = section.x + ox + section.width / 2;
|
|
142
|
+
const arrowTopY = section.y + oy + section.height;
|
|
143
|
+
const arrowBotY = nextSection.y + oy;
|
|
144
|
+
const midY = (arrowTopY + arrowBotY) / 2;
|
|
145
|
+
parts.push(`<path class="diagram-edge" d="M${arrowX},${arrowTopY} L${arrowX},${arrowBotY}" fill="none" marker-end="url(#ac-arrow)"/>`);
|
|
146
|
+
void midY; // midpoint available for future label placement
|
|
147
|
+
}
|
|
84
148
|
}
|
|
85
149
|
// Milestones.
|
|
86
150
|
for (const m of layout.milestones) {
|
|
87
151
|
parts.push(`<rect class="diagram-node level-3" x="${m.x + ox}" y="${m.y + oy}" width="${m.width}" height="${m.height}" rx="6"/>`);
|
|
88
|
-
parts.push(`<text class="text-secondary" x="${m.x + ox + 10}" y="${m.y + oy + 16}" dominant-baseline
|
|
89
|
-
parts.push(`<text class="text-primary" x="${m.x + ox + 10}" y="${m.y + oy + 36}" dominant-baseline
|
|
90
|
-
parts.push(`<text class="text-secondary" x="${m.x + ox + 10}" y="${m.y + oy + 52}" dominant-baseline
|
|
91
|
-
}
|
|
92
|
-
// Motivation chain — edges first (under nodes).
|
|
93
|
-
const nodeById = new Map();
|
|
94
|
-
for (const col of [
|
|
95
|
-
layout.chainColumns.factors,
|
|
96
|
-
layout.chainColumns.goals,
|
|
97
|
-
layout.chainColumns.changes,
|
|
98
|
-
]) {
|
|
99
|
-
for (const n of col)
|
|
100
|
-
nodeById.set(n.id, n);
|
|
101
|
-
}
|
|
102
|
-
for (const e of layout.chainEdges) {
|
|
103
|
-
const s = nodeById.get(e.sourceId);
|
|
104
|
-
const t = nodeById.get(e.targetId);
|
|
105
|
-
if (!s || !t)
|
|
106
|
-
continue;
|
|
107
|
-
const x1 = s.x + s.width + ox;
|
|
108
|
-
const y1 = s.y + s.height / 2 + oy;
|
|
109
|
-
const x2 = t.x + ox;
|
|
110
|
-
const y2 = t.y + t.height / 2 + oy;
|
|
111
|
-
const mx = (x1 + x2) / 2;
|
|
112
|
-
parts.push(`<path class="diagram-edge" d="M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}" fill="none" marker-end="url(#ac-arrow)"/>`);
|
|
113
|
-
}
|
|
114
|
-
const chainLevels = [
|
|
115
|
-
[layout.chainColumns.factors, 4],
|
|
116
|
-
[layout.chainColumns.goals, 5],
|
|
117
|
-
[layout.chainColumns.changes, 6],
|
|
118
|
-
];
|
|
119
|
-
for (const [col, level] of chainLevels) {
|
|
120
|
-
for (const n of col) {
|
|
121
|
-
parts.push(`<rect class="diagram-node level-${level}" x="${n.x + ox}" y="${n.y + oy}" width="${n.width}" height="${n.height}" rx="6"/>`);
|
|
122
|
-
parts.push(`<text class="text-pill" x="${n.x + ox + n.width / 2}" y="${n.y + oy + n.height / 2}" text-anchor="middle" dominant-baseline="central">${escXml(truncate(n.name, Math.floor(n.width / 7)))}</text>`);
|
|
123
|
-
}
|
|
152
|
+
parts.push(`<text class="text-secondary" x="${m.x + ox + 10}" y="${m.y + oy + 16}" style="dominant-baseline:central">${escXml(m.date)}</text>`);
|
|
153
|
+
parts.push(`<text class="text-primary" x="${m.x + ox + 10}" y="${m.y + oy + 36}" style="dominant-baseline:central">${escXml(truncate(m.name, 22))}</text>`);
|
|
154
|
+
parts.push(`<text class="text-secondary" x="${m.x + ox + 10}" y="${m.y + oy + 52}" style="dominant-baseline:central">(${escXml(m.archimateClass)})</text>`);
|
|
124
155
|
}
|
|
125
156
|
// Child activities.
|
|
126
157
|
for (const a of layout.childActivities) {
|
|
127
158
|
parts.push(`<rect class="diagram-node level-1" x="${a.x + ox}" y="${a.y + oy}" width="${a.width}" height="${a.height}" rx="6"/>`);
|
|
128
|
-
parts.push(`<text class="text-primary" x="${a.x + ox + 12}" y="${a.y + oy + a.height / 2}" dominant-baseline
|
|
159
|
+
parts.push(`<text class="text-primary" x="${a.x + ox + 12}" y="${a.y + oy + a.height / 2}" style="dominant-baseline:central">${escXml(truncate(a.name, 48))} <tspan class="text-secondary">(${escXml(a.archimateClass)})</tspan></text>`);
|
|
129
160
|
if (a.meta) {
|
|
130
|
-
parts.push(`<text class="text-secondary" x="${a.x + a.width + ox - 12}" y="${a.y + oy + a.height / 2}"
|
|
161
|
+
parts.push(`<text class="text-secondary" x="${a.x + a.width + ox - 12}" y="${a.y + oy + a.height / 2}" style="dominant-baseline:central;text-anchor:end">${escXml(truncate(a.meta, 40))}</text>`);
|
|
131
162
|
}
|
|
132
163
|
}
|
|
164
|
+
// Footer — notes.
|
|
165
|
+
if (layout.footerRow) {
|
|
166
|
+
const r = layout.footerRow;
|
|
167
|
+
parts.push(`<rect class="diagram-node level-2" x="${r.x + ox}" y="${r.y + oy}" width="${r.width}" height="${r.height}" rx="6"/>`);
|
|
168
|
+
parts.push(`<text class="text-secondary" x="${r.x + ox + 12}" y="${r.y + oy + 22}" style="dominant-baseline:central">${escXml(r.label)}</text>`);
|
|
169
|
+
r.valueLines.forEach((line, i) => {
|
|
170
|
+
parts.push(`<text class="text-primary" x="${r.x + ox + 12}" y="${r.y + oy + 44 + i * 18}" style="dominant-baseline:central">${escXml(line)}</text>`);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
133
173
|
return parts.join('\n');
|
|
134
174
|
}
|
|
135
175
|
export function renderActivityCardSvg(doc, options = {}) {
|
|
@@ -139,30 +179,26 @@ export function renderActivityCardSvg(doc, options = {}) {
|
|
|
139
179
|
const { title = '' } = options;
|
|
140
180
|
const resolved = resolveSingleDoc(doc);
|
|
141
181
|
const layout = layoutActivityCard(resolved);
|
|
142
|
-
if (layout.bounds.width <= 0 || layout.bounds.height <= 0)
|
|
182
|
+
if (layout.bounds.width <= 0 || layout.bounds.height <= 0)
|
|
143
183
|
return EMPTY_SVG;
|
|
144
|
-
}
|
|
145
184
|
const titleH = title ? 28 : 0;
|
|
146
185
|
const w = layout.bounds.width + PAD * 2;
|
|
147
186
|
const h = layout.bounds.height + PAD * 2 + titleH;
|
|
148
187
|
const ox = PAD;
|
|
149
188
|
const oy = PAD + titleH;
|
|
150
|
-
const body =
|
|
189
|
+
const body = renderActivityCardBody(layout, ox, oy);
|
|
151
190
|
const titleSvg = title
|
|
152
191
|
? `<text class="text-header" x="${PAD}" y="${PAD + 14}">${escXml(`Activity Card — ${title}`)}</text>`
|
|
153
192
|
: '';
|
|
154
|
-
// Embed the shared theme CSS inside the SVG so the rendered output is
|
|
155
|
-
// self-contained — the JCEF host page only needs to drop the SVG into the
|
|
156
|
-
// DOM and styling resolves without any cooperation from the host stylesheet.
|
|
157
193
|
const embedCss = generateSvgEmbedCss('transitrix');
|
|
158
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
|
|
159
|
-
<style>${embedCss}</style>
|
|
160
|
-
<defs>
|
|
161
|
-
<marker id="ac-arrow" markerWidth="8" markerHeight="8" refX="8" refY="3" orient="auto">
|
|
162
|
-
<path d="M0,0 L0,6 L8,3 z" class="arrow-fill"/>
|
|
163
|
-
</marker>
|
|
164
|
-
</defs>
|
|
165
|
-
${titleSvg}
|
|
166
|
-
${body}
|
|
194
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
|
|
195
|
+
<style>${embedCss}</style>
|
|
196
|
+
<defs>
|
|
197
|
+
<marker id="ac-arrow" markerWidth="8" markerHeight="8" refX="8" refY="3" orient="auto">
|
|
198
|
+
<path d="M0,0 L0,6 L8,3 z" class="arrow-fill"/>
|
|
199
|
+
</marker>
|
|
200
|
+
</defs>
|
|
201
|
+
${titleSvg}
|
|
202
|
+
${body}
|
|
167
203
|
</svg>`;
|
|
168
204
|
}
|
|
@@ -1,5 +1,28 @@
|
|
|
1
|
-
import type { BlocksFile } from '../blocks/types.js';
|
|
1
|
+
import type { BlocksFile, BlocksLayout } from '../blocks/types.js';
|
|
2
|
+
import { type ThemeId } from '../theme/index.js';
|
|
2
3
|
export interface RenderBlocksOptions {
|
|
3
4
|
title?: string;
|
|
4
5
|
}
|
|
6
|
+
export interface RenderBlocksLayoutOptions {
|
|
7
|
+
/** Extra vertical space reserved at the top of the canvas (e.g. for a title block). */
|
|
8
|
+
topInset?: number;
|
|
9
|
+
/** Raw SVG injected immediately after the opening tag — a header line or a full title block. */
|
|
10
|
+
title?: string;
|
|
11
|
+
/** When set, the theme CSS is embedded as `<style>` so the SVG is self-contained. */
|
|
12
|
+
embedCssTheme?: ThemeId;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The single Nested Blocks SVG emitter shared by every host. Takes an
|
|
16
|
+
* already-computed {@link BlocksLayout} (callers decide the layout options) and
|
|
17
|
+
* produces the `<svg>`. Hosts wrap it with their own chrome:
|
|
18
|
+
* - IntelliJ/UI via {@link renderBlocksSvg} (embedded CSS + simple header);
|
|
19
|
+
* - VS Code's blocks preview (rich title block, no embedded CSS — the webview
|
|
20
|
+
* and the export path own styling).
|
|
21
|
+
*/
|
|
22
|
+
export declare function renderBlocksLayoutSvg(layout: BlocksLayout, options?: RenderBlocksLayoutOptions): string;
|
|
23
|
+
/**
|
|
24
|
+
* Host-neutral blocks renderer (IntelliJ/UI). Lays the doc out with the default
|
|
25
|
+
* spacing, then delegates the actual SVG emission to {@link renderBlocksLayoutSvg}
|
|
26
|
+
* with the shared theme CSS embedded so the output is self-contained.
|
|
27
|
+
*/
|
|
5
28
|
export declare function renderBlocksSvg(doc: BlocksFile, options?: RenderBlocksOptions): string;
|
|
@@ -16,6 +16,10 @@ import { layoutNestedBlocks } from '../blocks/layout.js';
|
|
|
16
16
|
import { generateSvgEmbedCss } from '../theme/index.js';
|
|
17
17
|
import { escXml } from './render-util.js';
|
|
18
18
|
const PAD = 24;
|
|
19
|
+
// Approximate character width (px) at text-primary size (12px/600).
|
|
20
|
+
const CHAR_W = 7;
|
|
21
|
+
// Approximate character width (px) at text-id size (10px/600) — used for ID suffix.
|
|
22
|
+
const CHAR_W_ID = 6;
|
|
19
23
|
/**
|
|
20
24
|
* Pick the diagram-frame level class for a block at the given depth.
|
|
21
25
|
*
|
|
@@ -28,44 +32,74 @@ function levelClassForDepth(depth) {
|
|
|
28
32
|
const idx = Math.min(Math.max(depth - 1, 0), 6);
|
|
29
33
|
return `level-${idx}`;
|
|
30
34
|
}
|
|
31
|
-
|
|
35
|
+
/** Truncate at a word boundary so labels never break in the middle of a word. */
|
|
36
|
+
function wordTruncate(text, maxChars) {
|
|
32
37
|
if (text.length <= maxChars)
|
|
33
38
|
return text;
|
|
34
|
-
|
|
39
|
+
const cut = text.slice(0, maxChars - 1);
|
|
40
|
+
const lastSpace = cut.lastIndexOf(' ');
|
|
41
|
+
return (lastSpace > maxChars / 2 ? cut.slice(0, lastSpace) : cut) + '…';
|
|
35
42
|
}
|
|
36
43
|
function emitBlockSvg(b, ox, oy, parts) {
|
|
37
44
|
const cls = levelClassForDepth(b.depth);
|
|
45
|
+
const cx = b.x + ox + b.width / 2;
|
|
38
46
|
parts.push(`<rect class="diagram-node ${cls}" x="${b.x + ox}" y="${b.y + oy}" width="${b.width}" height="${b.height}" rx="6"/>`);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
const isLeaf = b.children.length === 0;
|
|
48
|
+
if (isLeaf) {
|
|
49
|
+
// Leaf block: name on upper line, ID on lower line — both centred.
|
|
50
|
+
const nameY = b.y + oy + Math.round(b.height * 0.38);
|
|
51
|
+
const idY = b.y + oy + Math.round(b.height * 0.66);
|
|
52
|
+
const maxChars = Math.max(4, Math.floor(b.width / CHAR_W));
|
|
53
|
+
parts.push(`<text class="text-primary" x="${cx}" y="${nameY}" text-anchor="middle" dominant-baseline="central">${escXml(wordTruncate(b.name, maxChars))}</text>`, `<text class="text-id" x="${cx}" y="${idY}" text-anchor="middle" dominant-baseline="central">${escXml(b.id)}</text>`);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
// Container block: name + (ID) on the single header line; ID in grey.
|
|
57
|
+
const headerY = b.y + oy + b.headerHeight / 2;
|
|
58
|
+
const idSuffix = ` (${b.id})`;
|
|
59
|
+
const idSuffixW = idSuffix.length * CHAR_W_ID;
|
|
60
|
+
const nameMaxChars = Math.max(4, Math.floor((b.width - idSuffixW) / CHAR_W));
|
|
61
|
+
const nameText = wordTruncate(b.name, nameMaxChars);
|
|
62
|
+
parts.push(`<text class="text-primary" x="${cx}" y="${headerY}" text-anchor="middle" dominant-baseline="central">${escXml(nameText)}<tspan fill="var(--ts-text-secondary, #64748b)" font-size="10" font-weight="600">${escXml(idSuffix)}</tspan></text>`);
|
|
63
|
+
for (const c of b.children)
|
|
64
|
+
emitBlockSvg(c, ox, oy, parts);
|
|
65
|
+
}
|
|
45
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* The single Nested Blocks SVG emitter shared by every host. Takes an
|
|
69
|
+
* already-computed {@link BlocksLayout} (callers decide the layout options) and
|
|
70
|
+
* produces the `<svg>`. Hosts wrap it with their own chrome:
|
|
71
|
+
* - IntelliJ/UI via {@link renderBlocksSvg} (embedded CSS + simple header);
|
|
72
|
+
* - VS Code's blocks preview (rich title block, no embedded CSS — the webview
|
|
73
|
+
* and the export path own styling).
|
|
74
|
+
*/
|
|
75
|
+
export function renderBlocksLayoutSvg(layout, options = {}) {
|
|
76
|
+
const { topInset = 0, title = '', embedCssTheme } = options;
|
|
77
|
+
const w = layout.bounds.width + PAD * 2;
|
|
78
|
+
const h = layout.bounds.height + PAD * 2 + topInset;
|
|
79
|
+
const ox = -layout.bounds.x + PAD;
|
|
80
|
+
const oy = -layout.bounds.y + PAD + topInset;
|
|
81
|
+
const parts = [];
|
|
82
|
+
for (const top of layout.blocks)
|
|
83
|
+
emitBlockSvg(top, ox, oy, parts);
|
|
84
|
+
const styleLine = embedCssTheme ? `\n<style>${generateSvgEmbedCss(embedCssTheme)}</style>` : '';
|
|
85
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">${styleLine}
|
|
86
|
+
${title}
|
|
87
|
+
${parts.join('\n')}
|
|
88
|
+
</svg>`;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Host-neutral blocks renderer (IntelliJ/UI). Lays the doc out with the default
|
|
92
|
+
* spacing, then delegates the actual SVG emission to {@link renderBlocksLayoutSvg}
|
|
93
|
+
* with the shared theme CSS embedded so the output is self-contained.
|
|
94
|
+
*/
|
|
46
95
|
export function renderBlocksSvg(doc, options = {}) {
|
|
47
96
|
const { title = '' } = options;
|
|
48
97
|
const layout = layoutNestedBlocks(doc);
|
|
49
98
|
if (layout.blocks.length === 0) {
|
|
50
99
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
|
|
51
100
|
}
|
|
52
|
-
const w = layout.bounds.width + PAD * 2;
|
|
53
|
-
const h = layout.bounds.height + PAD * 2;
|
|
54
|
-
const ox = -layout.bounds.x + PAD;
|
|
55
|
-
const oy = -layout.bounds.y + PAD;
|
|
56
|
-
const parts = [];
|
|
57
|
-
for (const top of layout.blocks)
|
|
58
|
-
emitBlockSvg(top, ox, oy, parts);
|
|
59
101
|
const titleSvg = title
|
|
60
102
|
? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(`Nested Blocks — ${title}`)}</text>`
|
|
61
103
|
: '';
|
|
62
|
-
|
|
63
|
-
// self-contained — the JCEF host page only needs to drop the SVG into the
|
|
64
|
-
// DOM and styling resolves without any cooperation from the host stylesheet.
|
|
65
|
-
const embedCss = generateSvgEmbedCss('transitrix');
|
|
66
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
|
|
67
|
-
<style>${embedCss}</style>
|
|
68
|
-
${titleSvg}
|
|
69
|
-
${parts.join('\n')}
|
|
70
|
-
</svg>`;
|
|
104
|
+
return renderBlocksLayoutSvg(layout, { title: titleSvg, embedCssTheme: 'transitrix' });
|
|
71
105
|
}
|