@transitrix/diagrams 1.10.0 → 1.11.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/activities/resolver.d.ts +3 -0
- package/dist/activities/resolver.js +39 -18
- package/dist/activities/validate.js +59 -0
- package/dist/canon-resolver-utils.d.ts +3 -0
- package/dist/canon-resolver-utils.js +16 -0
- package/dist/compliance/classify.js +1 -0
- package/dist/compliance/impact.d.ts +3 -1
- package/dist/compliance/impact.js +41 -13
- package/dist/compliance/types.d.ts +6 -0
- package/dist/compliance-verification-matrix/build.d.ts +17 -0
- package/dist/compliance-verification-matrix/build.js +65 -0
- package/dist/compliance-verification-matrix/csv.d.ts +6 -0
- package/dist/compliance-verification-matrix/csv.js +41 -0
- package/dist/compliance-verification-matrix/index.d.ts +3 -0
- package/dist/compliance-verification-matrix/index.js +2 -0
- package/dist/compliance-verification-matrix/types.d.ts +75 -0
- package/dist/compliance-verification-matrix/types.js +4 -0
- package/dist/fgca/action-change-ids.d.ts +7 -0
- package/dist/fgca/action-change-ids.js +11 -0
- package/dist/fgca/parse-canonical.d.ts +3 -1
- package/dist/fgca/parse-canonical.js +13 -8
- package/dist/fgca/resolver.d.ts +6 -2
- package/dist/fgca/resolver.js +12 -6
- package/dist/fgca/validate.d.ts +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/process-blueprint/index.d.ts +3 -1
- package/dist/process-blueprint/index.js +1 -0
- package/dist/process-blueprint/layout.js +33 -29
- package/dist/process-blueprint/resolve-columns.d.ts +52 -0
- package/dist/process-blueprint/resolve-columns.js +138 -0
- package/dist/process-blueprint/types.d.ts +23 -5
- package/dist/process-blueprint/validate.d.ts +19 -1
- package/dist/process-blueprint/validate.js +73 -21
- package/dist/repo-validate/check-process-parent.d.ts +8 -0
- package/dist/repo-validate/check-process-parent.js +172 -0
- package/dist/repo-validate/index.d.ts +1 -0
- package/dist/repo-validate/index.js +1 -0
- package/dist/repo-validate/validate-repo.js +3 -0
- package/dist/webview/render-fgca.js +1 -1
- package/dist/webview/render-process.d.ts +19 -1
- package/dist/webview/render-process.js +75 -42
- package/package.json +1 -1
- package/src/activities/resolver.ts +40 -20
- package/src/activities/validate.ts +58 -0
- package/src/canon-resolver-utils.ts +19 -0
- package/src/compliance/classify.ts +1 -0
- package/src/compliance/impact.ts +41 -12
- package/src/compliance/types.ts +6 -0
- package/src/compliance-verification-matrix/build.ts +84 -0
- package/src/compliance-verification-matrix/csv.ts +47 -0
- package/src/compliance-verification-matrix/index.ts +7 -0
- package/src/compliance-verification-matrix/types.ts +85 -0
- package/src/fgca/action-change-ids.ts +13 -0
- package/src/fgca/parse-canonical.ts +12 -8
- package/src/fgca/resolver.ts +15 -6
- package/src/fgca/validate.ts +2 -0
- package/src/index.ts +1 -0
- package/src/process-blueprint/index.ts +11 -0
- package/src/process-blueprint/layout.ts +48 -29
- package/src/process-blueprint/resolve-columns.ts +147 -0
- package/src/process-blueprint/types.ts +19 -5
- package/src/process-blueprint/validate.ts +107 -21
- package/src/repo-validate/check-process-parent.ts +184 -0
- package/src/repo-validate/index.ts +5 -0
- package/src/repo-validate/validate-repo.ts +3 -0
- package/src/webview/render-fgca.ts +1 -1
- package/src/webview/render-process.ts +94 -49
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// process_parent relation checks — 17-relations.md §3 / §5 (REL-007, REL-008)
|
|
2
|
+
// and endpoint TYPE (PROCESS → PROCESS), plus the in-effect edge collector
|
|
3
|
+
// that process-blueprint BP-014 consumes.
|
|
4
|
+
//
|
|
5
|
+
// REL-007 is single-file (from === to). REL-008 is cross-cutting over the
|
|
6
|
+
// admitted process_parent graph and is a warning, parallel to REL-006.
|
|
7
|
+
|
|
8
|
+
import { docId, endpointId } from './validate-repo.js';
|
|
9
|
+
import type { ProcessParentEdge } from '../process-blueprint/validate.js';
|
|
10
|
+
import type { RepoFinding, RepoModelInput } from './types.js';
|
|
11
|
+
|
|
12
|
+
const PScope: RepoFinding['scope'] = 'repo';
|
|
13
|
+
|
|
14
|
+
function isProcessNotation(n: unknown): boolean {
|
|
15
|
+
return n === 'process';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isoDate(value: unknown): string | undefined {
|
|
19
|
+
return typeof value === 'string' && /^\d{4}-\d{2}-\d{2}/.test(value) ? value.slice(0, 10) : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Inclusive window: in effect at `asOf` iff valid_from ≤ asOf and (valid_to is null or asOf ≤ valid_to). */
|
|
23
|
+
export function relationInEffectAt(data: Record<string, unknown>, asOf: string): boolean {
|
|
24
|
+
const from = isoDate(data['valid_from']);
|
|
25
|
+
if (from && from > asOf) return false;
|
|
26
|
+
const to = data['valid_to'];
|
|
27
|
+
if (to === null || to === undefined) return true;
|
|
28
|
+
const toDate = isoDate(to);
|
|
29
|
+
if (!toDate) return true;
|
|
30
|
+
return asOf <= toDate;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function todayIsoDate(now: Date = new Date()): string {
|
|
34
|
+
return now.toISOString().slice(0, 10);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** In-effect `process_parent` edges (child → parent) among loaded REL files. */
|
|
38
|
+
export function collectInEffectProcessParentEdges(
|
|
39
|
+
input: RepoModelInput,
|
|
40
|
+
asOf: string = todayIsoDate(),
|
|
41
|
+
): ProcessParentEdge[] {
|
|
42
|
+
const edges: ProcessParentEdge[] = [];
|
|
43
|
+
for (const doc of input.relations) {
|
|
44
|
+
if (!doc.data) continue;
|
|
45
|
+
if (doc.data['type'] !== 'process_parent') continue;
|
|
46
|
+
if (!relationInEffectAt(doc.data, asOf)) continue;
|
|
47
|
+
const fromId = endpointId(doc.data['from']) ?? endpointId(doc.data['source']);
|
|
48
|
+
const toId = endpointId(doc.data['to']) ?? endpointId(doc.data['target']);
|
|
49
|
+
if (!fromId || !toId) continue;
|
|
50
|
+
edges.push({ from: fromId, to: toId });
|
|
51
|
+
}
|
|
52
|
+
return edges;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface ParentRel {
|
|
56
|
+
relId: string;
|
|
57
|
+
fromId: string;
|
|
58
|
+
toId: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function processParentRels(input: RepoModelInput): ParentRel[] {
|
|
62
|
+
const out: ParentRel[] = [];
|
|
63
|
+
for (const doc of input.relations) {
|
|
64
|
+
if (!doc.data) continue;
|
|
65
|
+
if (doc.data['type'] !== 'process_parent') continue;
|
|
66
|
+
const fromId = endpointId(doc.data['from']) ?? endpointId(doc.data['source']);
|
|
67
|
+
const toId = endpointId(doc.data['to']) ?? endpointId(doc.data['target']);
|
|
68
|
+
if (!fromId || !toId) continue;
|
|
69
|
+
out.push({ relId: docId(doc) ?? '', fromId, toId });
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** REL-007 — process_parent self-reference. */
|
|
75
|
+
function checkSelfReference(rels: ParentRel[], findings: RepoFinding[]): void {
|
|
76
|
+
for (const rel of rels) {
|
|
77
|
+
if (rel.fromId === rel.toId) {
|
|
78
|
+
findings.push({
|
|
79
|
+
scope: PScope,
|
|
80
|
+
id: rel.relId,
|
|
81
|
+
ruleId: 'REL-007',
|
|
82
|
+
message: `REL-007: process_parent relation '${rel.relId}' has from equal to to ('${rel.fromId}').`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** REL-008 — cycle in the process_parent graph (warning). */
|
|
89
|
+
function checkCycle(rels: ParentRel[], findings: RepoFinding[]): void {
|
|
90
|
+
const childrenOf = new Map<string, string[]>();
|
|
91
|
+
const relByChildParent = new Map<string, string>();
|
|
92
|
+
const nodes = new Set<string>();
|
|
93
|
+
for (const rel of rels) {
|
|
94
|
+
if (rel.fromId === rel.toId) continue;
|
|
95
|
+
nodes.add(rel.fromId);
|
|
96
|
+
nodes.add(rel.toId);
|
|
97
|
+
const list = childrenOf.get(rel.fromId);
|
|
98
|
+
if (list) list.push(rel.toId);
|
|
99
|
+
else childrenOf.set(rel.fromId, [rel.toId]);
|
|
100
|
+
relByChildParent.set(`${rel.fromId}\0${rel.toId}`, rel.relId);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const UNVISITED = 0;
|
|
104
|
+
const VISITING = 1;
|
|
105
|
+
const DONE = 2;
|
|
106
|
+
const state = new Map<string, number>();
|
|
107
|
+
let hit: string | undefined;
|
|
108
|
+
|
|
109
|
+
function walk(id: string): boolean {
|
|
110
|
+
const s = state.get(id) ?? UNVISITED;
|
|
111
|
+
if (s === DONE) return false;
|
|
112
|
+
if (s === VISITING) {
|
|
113
|
+
hit = id;
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
state.set(id, VISITING);
|
|
117
|
+
for (const parent of childrenOf.get(id) ?? []) {
|
|
118
|
+
if (nodes.has(parent) && walk(parent)) return true;
|
|
119
|
+
}
|
|
120
|
+
state.set(id, DONE);
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const id of nodes) {
|
|
125
|
+
if (walk(id)) break;
|
|
126
|
+
}
|
|
127
|
+
if (!hit) return;
|
|
128
|
+
|
|
129
|
+
const relId =
|
|
130
|
+
[...relByChildParent.entries()].find(([key]) => key.startsWith(`${hit}\0`))?.[1] ?? hit;
|
|
131
|
+
findings.push({
|
|
132
|
+
scope: PScope,
|
|
133
|
+
id: relId,
|
|
134
|
+
ruleId: 'REL-008',
|
|
135
|
+
severity: 'warning',
|
|
136
|
+
message: `REL-008: process_parent graph contains a cycle involving '${hit}'.`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Endpoint TYPE for process_parent: both ends must be PROCESS when they
|
|
142
|
+
* resolve. Unknown endpoints stay with referential integrity; this pass only
|
|
143
|
+
* fires when the element exists and is the wrong notation.
|
|
144
|
+
*/
|
|
145
|
+
function checkEndpointTypes(input: RepoModelInput, rels: ParentRel[], findings: RepoFinding[]): void {
|
|
146
|
+
const elementById = new Map<string, Record<string, unknown>>();
|
|
147
|
+
for (const doc of input.elements) {
|
|
148
|
+
const id = docId(doc);
|
|
149
|
+
if (id && doc.data) elementById.set(id, doc.data);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const rel of rels) {
|
|
153
|
+
const from = elementById.get(rel.fromId);
|
|
154
|
+
if (from && !isProcessNotation(from['notation'])) {
|
|
155
|
+
findings.push({
|
|
156
|
+
scope: PScope,
|
|
157
|
+
id: rel.relId,
|
|
158
|
+
ruleId: 'REL-002',
|
|
159
|
+
message:
|
|
160
|
+
`Layer-semantics: '${rel.relId}' type 'process_parent' requires from to be ` +
|
|
161
|
+
`PROCESS; got notation='${from['notation']}'.`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
const to = elementById.get(rel.toId);
|
|
165
|
+
if (to && !isProcessNotation(to['notation'])) {
|
|
166
|
+
findings.push({
|
|
167
|
+
scope: PScope,
|
|
168
|
+
id: rel.relId,
|
|
169
|
+
ruleId: 'REL-002',
|
|
170
|
+
message:
|
|
171
|
+
`Layer-semantics: '${rel.relId}' type 'process_parent' requires to to be ` +
|
|
172
|
+
`PROCESS; got notation='${to['notation']}'.`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function checkProcessParentSemantics(input: RepoModelInput, findings: RepoFinding[]): void {
|
|
179
|
+
const rels = processParentRels(input);
|
|
180
|
+
if (rels.length === 0) return;
|
|
181
|
+
checkSelfReference(rels, findings);
|
|
182
|
+
checkCycle(rels, findings);
|
|
183
|
+
checkEndpointTypes(input, rels, findings);
|
|
184
|
+
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
export { validateRepoModel } from './validate-repo.js';
|
|
2
2
|
export { resolveRepoModel } from './resolve-model.js';
|
|
3
|
+
export {
|
|
4
|
+
checkProcessParentSemantics,
|
|
5
|
+
collectInEffectProcessParentEdges,
|
|
6
|
+
todayIsoDate,
|
|
7
|
+
} from './check-process-parent.js';
|
|
3
8
|
export type { RepoDoc, RepoFinding, RepoModelInput, ResolvedElementRecord, ResolvedRelationRecord, ResolvedRepoModel } from './types.js';
|
|
@@ -31,6 +31,7 @@ import { checkStrategyChainSemantics } from './check-strategy-chain.js';
|
|
|
31
31
|
import { checkElementHygiene } from './check-element-hygiene.js';
|
|
32
32
|
import { checkVersionedAttributes } from './check-versioned-attributes.js';
|
|
33
33
|
import { checkCandidateFields } from './check-candidate-fields.js';
|
|
34
|
+
import { checkProcessParentSemantics } from './check-process-parent.js';
|
|
34
35
|
import type { RepoDoc, RepoFinding, RepoModelInput } from './types.js';
|
|
35
36
|
|
|
36
37
|
const PScope: RepoFinding['scope'] = 'repo';
|
|
@@ -161,6 +162,7 @@ function checkReferentialIntegrity(input: RepoModelInput, findings: RepoFinding[
|
|
|
161
162
|
* realizes — BUSINESS_SERVICE → CAPABILITY (25-business-services.md)
|
|
162
163
|
* hosts — NODE → TECHNOLOGY_SERVICE (25-nodes.md / 26-technology-services.md)
|
|
163
164
|
* uses — APPLICATION → TECHNOLOGY_SERVICE (26-technology-services.md)
|
|
165
|
+
* process_parent — PROCESS → PROCESS (17-relations.md §3; REL-007/008)
|
|
164
166
|
*
|
|
165
167
|
* Implemented element checks:
|
|
166
168
|
* TSVC-003 — TECHNOLOGY_SERVICE.node must resolve to NODE notation
|
|
@@ -423,6 +425,7 @@ export function validateRepoModel(input: RepoModelInput): RepoFinding[] {
|
|
|
423
425
|
checkAtomicity(input, findings);
|
|
424
426
|
checkReferentialIntegrity(input, findings);
|
|
425
427
|
checkLayerSemantics(input, findings);
|
|
428
|
+
checkProcessParentSemantics(input, findings);
|
|
426
429
|
checkPolicy(input, findings);
|
|
427
430
|
// Phase 7 — strategy-chain semantic rules ported from DSM's Go Validate*
|
|
428
431
|
// functions (GOALS-010, ACT-006..009, FGCA-008..011).
|
|
@@ -121,7 +121,7 @@ export function renderFgcaSvg(doc: FGCADoc, options: RenderFgcaOptions = {}): st
|
|
|
121
121
|
nodeSizePreset = 'normal',
|
|
122
122
|
layoutOptions,
|
|
123
123
|
} = options;
|
|
124
|
-
const hideChanges = variant === 'dga';
|
|
124
|
+
const hideChanges = variant === 'dga' || doc.hideChanges === true;
|
|
125
125
|
const nodeSize = resolveDgcaNodeSize(parseNodeSizePreset(nodeSizePreset));
|
|
126
126
|
|
|
127
127
|
const { nodes, edges, columns, width, height } = layoutFGCAPreview(doc, {
|
|
@@ -75,11 +75,48 @@ export interface ProcessDiagramLayout {
|
|
|
75
75
|
associations: ProcessAssociation[];
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
export interface BpmnTypography {
|
|
79
|
+
taskFontPx: number;
|
|
80
|
+
headerFontPx: number;
|
|
81
|
+
labelFontPx: number;
|
|
82
|
+
taskChars: number;
|
|
83
|
+
taskLineH: number;
|
|
84
|
+
labelChars: number;
|
|
85
|
+
labelLineH: number;
|
|
86
|
+
headerCharW: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Interactive-preview / default-export type sizes. */
|
|
90
|
+
export const DEFAULT_BPMN_TYPOGRAPHY: BpmnTypography = {
|
|
91
|
+
taskFontPx: 11,
|
|
92
|
+
headerFontPx: 11,
|
|
93
|
+
labelFontPx: 10,
|
|
94
|
+
taskChars: 14,
|
|
95
|
+
taskLineH: 14,
|
|
96
|
+
labelChars: 18,
|
|
97
|
+
labelLineH: 13,
|
|
98
|
+
headerCharW: 6.5,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** Presentation-export type sizes (20 px floor, wrap tuned to default node boxes). */
|
|
102
|
+
export const PRESENTATION_BPMN_TYPOGRAPHY: BpmnTypography = {
|
|
103
|
+
taskFontPx: 20,
|
|
104
|
+
headerFontPx: 20,
|
|
105
|
+
labelFontPx: 20,
|
|
106
|
+
taskChars: 9,
|
|
107
|
+
taskLineH: 22,
|
|
108
|
+
labelChars: 14,
|
|
109
|
+
labelLineH: 22,
|
|
110
|
+
headerCharW: 11.5,
|
|
111
|
+
};
|
|
112
|
+
|
|
78
113
|
export interface RenderProcessOptions {
|
|
79
114
|
/** Raw SVG injected after `<style>` — a title line or full title block. */
|
|
80
115
|
title?: string;
|
|
81
116
|
/** Extra vertical space (px) reserved above the pool for a title block. */
|
|
82
117
|
topInset?: number;
|
|
118
|
+
/** Label metrics. Omit for the interactive-preview defaults. */
|
|
119
|
+
typography?: BpmnTypography;
|
|
83
120
|
}
|
|
84
121
|
|
|
85
122
|
// ---------------------------------------------------------------------------
|
|
@@ -92,49 +129,52 @@ const LANE_LABEL_BAND = 44;
|
|
|
92
129
|
/** End margin (px) along the header height axis for rotated pool/lane captions. */
|
|
93
130
|
const HEADER_LABEL_AXIS_PAD = 32;
|
|
94
131
|
|
|
95
|
-
/**
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
/** Maximum characters per wrapped line inside a task box. */
|
|
99
|
-
const TASK_CHARS = 14;
|
|
132
|
+
/** Active wrap/font metrics for the current render call (sync, restored after). */
|
|
133
|
+
let typo: BpmnTypography = DEFAULT_BPMN_TYPOGRAPHY;
|
|
100
134
|
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
135
|
+
function runWithTypography<T>(t: BpmnTypography, fn: () => T): T {
|
|
136
|
+
const prev = typo;
|
|
137
|
+
typo = t;
|
|
138
|
+
try {
|
|
139
|
+
return fn();
|
|
140
|
+
} finally {
|
|
141
|
+
typo = prev;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
109
144
|
|
|
110
145
|
// ---------------------------------------------------------------------------
|
|
111
146
|
// BPMN-specific CSS — no font-style:italic per CLAUDE.md design rule
|
|
112
147
|
// ---------------------------------------------------------------------------
|
|
113
148
|
|
|
114
|
-
export
|
|
149
|
+
export function bpmnProcessCss(t: BpmnTypography = DEFAULT_BPMN_TYPOGRAPHY): string {
|
|
150
|
+
return `
|
|
115
151
|
.bpmn-pool { fill: var(--ts-bg-surface,#f8fafc); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }
|
|
116
152
|
.bpmn-pool-name { fill: var(--ts-bg-elevated,#f1f5f9); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }
|
|
117
153
|
.bpmn-lane { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }
|
|
118
154
|
.bpmn-lane-header { fill: var(--ts-bg-elevated,#f1f5f9); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }
|
|
119
|
-
.bpmn-pool-label { fill: var(--ts-text-primary,#0d2b35); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size:
|
|
120
|
-
.bpmn-lane-label { fill: var(--ts-text-primary,#0d2b35); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size:
|
|
155
|
+
.bpmn-pool-label { fill: var(--ts-text-primary,#0d2b35); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size: ${t.headerFontPx}px; font-weight: 700; }
|
|
156
|
+
.bpmn-lane-label { fill: var(--ts-text-primary,#0d2b35); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size: ${t.headerFontPx}px; font-weight: 600; }
|
|
121
157
|
.bpmn-task { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }
|
|
122
|
-
.bpmn-task-name { fill: var(--ts-text-primary,#0d2b35); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size:
|
|
158
|
+
.bpmn-task-name { fill: var(--ts-text-primary,#0d2b35); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size: ${t.taskFontPx}px; text-anchor: middle; }
|
|
123
159
|
.bpmn-event { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); }
|
|
124
160
|
.bpmn-event-start { stroke-width: 1.5; }
|
|
125
161
|
.bpmn-event-end { stroke-width: 4; }
|
|
126
|
-
.bpmn-event-label { fill: var(--ts-text-secondary,#516970); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size:
|
|
162
|
+
.bpmn-event-label { fill: var(--ts-text-secondary,#516970); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size: ${t.labelFontPx}px; text-anchor: middle; }
|
|
127
163
|
.bpmn-gateway { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }
|
|
128
164
|
.bpmn-gateway-marker { stroke: var(--ts-node-stroke,#004d67); stroke-width: 2.5; stroke-linecap: round; fill: none; }
|
|
129
|
-
.bpmn-gateway-label { fill: var(--ts-text-secondary,#516970); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size:
|
|
165
|
+
.bpmn-gateway-label { fill: var(--ts-text-secondary,#516970); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size: ${t.labelFontPx}px; text-anchor: middle; }
|
|
130
166
|
.bpmn-seq-flow { fill: none; stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1.5; }
|
|
131
167
|
.bpmn-default-mark { stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1.5; stroke-linecap: round; }
|
|
132
168
|
.bpmn-assoc { fill: none; stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1; stroke-dasharray: 4 2; }
|
|
133
169
|
.bpmn-data-obj { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1; }
|
|
134
|
-
.bpmn-data-obj-label { fill: var(--ts-text-secondary,#516970); font-size:
|
|
170
|
+
.bpmn-data-obj-label { fill: var(--ts-text-secondary,#516970); font-size: ${t.labelFontPx}px; text-anchor: middle; }
|
|
135
171
|
.bpmn-event-intermediate { fill: none; stroke: var(--ts-node-stroke,#004d67); stroke-width: 1; }
|
|
136
172
|
.bpmn-event-icon { fill: none; stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
|
|
137
173
|
`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Default-preview CSS (11 px / 10 px). Presentation export builds its own via `bpmnProcessCss`. */
|
|
177
|
+
export const BPMN_PROCESS_CSS = bpmnProcessCss(DEFAULT_BPMN_TYPOGRAPHY);
|
|
138
178
|
|
|
139
179
|
// ---------------------------------------------------------------------------
|
|
140
180
|
// Internal helpers
|
|
@@ -153,54 +193,56 @@ function wordTruncate(text: string, maxChars: number): string {
|
|
|
153
193
|
}
|
|
154
194
|
|
|
155
195
|
function fitRotatedHeaderText(name: string, spanPx: number): string {
|
|
156
|
-
const maxChars = Math.max(4, Math.floor(spanPx /
|
|
196
|
+
const maxChars = Math.max(4, Math.floor(spanPx / typo.headerCharW));
|
|
157
197
|
return wordTruncate(name, maxChars);
|
|
158
198
|
}
|
|
159
199
|
|
|
160
200
|
function wrapTaskName(name: string): string[] {
|
|
161
201
|
if (!name) return [];
|
|
202
|
+
const limit = typo.taskChars;
|
|
162
203
|
const words = name.split(/\s+/);
|
|
163
204
|
const lines: string[] = [];
|
|
164
205
|
let cur = '';
|
|
165
206
|
for (const w of words) {
|
|
166
|
-
const token = w.length >
|
|
207
|
+
const token = w.length > limit ? w.slice(0, limit - 1) + '…' : w;
|
|
167
208
|
if (!cur) {
|
|
168
209
|
cur = token;
|
|
169
|
-
} else if (cur.length + 1 + token.length <=
|
|
210
|
+
} else if (cur.length + 1 + token.length <= limit) {
|
|
170
211
|
cur += ' ' + token;
|
|
171
212
|
} else {
|
|
172
213
|
lines.push(cur);
|
|
173
214
|
if (lines.length === 3) {
|
|
174
|
-
if (lines[2].length >
|
|
215
|
+
if (lines[2].length > limit - 1) lines[2] = lines[2].slice(0, limit - 1) + '…';
|
|
175
216
|
return lines;
|
|
176
217
|
}
|
|
177
218
|
cur = token;
|
|
178
219
|
}
|
|
179
220
|
}
|
|
180
221
|
if (cur && lines.length < 3) lines.push(cur);
|
|
181
|
-
if (lines.length === 0) lines.push(name.slice(0,
|
|
222
|
+
if (lines.length === 0) lines.push(name.slice(0, limit));
|
|
182
223
|
return lines;
|
|
183
224
|
}
|
|
184
225
|
|
|
185
226
|
function taskNameSvg(name: string | undefined, cx: number, cy: number): string {
|
|
186
227
|
if (!name) return '';
|
|
187
228
|
const lines = wrapTaskName(name);
|
|
188
|
-
const totalH = lines.length *
|
|
189
|
-
const startY = cy - totalH / 2 +
|
|
229
|
+
const totalH = lines.length * typo.taskLineH;
|
|
230
|
+
const startY = cy - totalH / 2 + typo.taskLineH * 0.8;
|
|
190
231
|
const tspans = lines
|
|
191
|
-
.map((ln, i) => `<tspan x="${r(cx)}" y="${r(startY + i *
|
|
232
|
+
.map((ln, i) => `<tspan x="${r(cx)}" y="${r(startY + i * typo.taskLineH)}">${escXml(ln)}</tspan>`)
|
|
192
233
|
.join('');
|
|
193
234
|
return `<text class="bpmn-task-name">${tspans}</text>`;
|
|
194
235
|
}
|
|
195
236
|
|
|
196
237
|
function wrapBelowLabel(name: string): string[] {
|
|
238
|
+
const limit = typo.labelChars;
|
|
197
239
|
const words = name.split(/\s+/).filter(Boolean);
|
|
198
240
|
const allLines: string[] = [];
|
|
199
241
|
let cur = '';
|
|
200
242
|
for (const word of words) {
|
|
201
|
-
const tok = word.length >
|
|
243
|
+
const tok = word.length > limit ? word.slice(0, limit - 1) + '…' : word;
|
|
202
244
|
const test = cur ? `${cur} ${tok}` : tok;
|
|
203
|
-
if (test.length <=
|
|
245
|
+
if (test.length <= limit) {
|
|
204
246
|
cur = test;
|
|
205
247
|
} else {
|
|
206
248
|
if (cur) allLines.push(cur);
|
|
@@ -208,11 +250,11 @@ function wrapBelowLabel(name: string): string[] {
|
|
|
208
250
|
}
|
|
209
251
|
}
|
|
210
252
|
if (cur) allLines.push(cur);
|
|
211
|
-
if (allLines.length === 0) return [name.slice(0,
|
|
253
|
+
if (allLines.length === 0) return [name.slice(0, limit)];
|
|
212
254
|
if (allLines.length <= 3) return allLines;
|
|
213
255
|
const lines = allLines.slice(0, 3);
|
|
214
256
|
const last = lines[2];
|
|
215
|
-
lines[2] = last.length + 1 <=
|
|
257
|
+
lines[2] = last.length + 1 <= limit ? last + '…' : last.slice(0, limit - 1) + '…';
|
|
216
258
|
return lines;
|
|
217
259
|
}
|
|
218
260
|
|
|
@@ -226,7 +268,7 @@ function belowLabelSvg(name: string | undefined, cls: string, cx: number, belowY
|
|
|
226
268
|
return `<text class="${cls}" x="${r(textX)}" y="${startY}">${escXml(lines[0])}</text>`;
|
|
227
269
|
}
|
|
228
270
|
const tspans = lines
|
|
229
|
-
.map((ln, i) => `<tspan x="${r(textX)}" dy="${i === 0 ? 0 :
|
|
271
|
+
.map((ln, i) => `<tspan x="${r(textX)}" dy="${i === 0 ? 0 : typo.labelLineH}">${escXml(ln)}</tspan>`)
|
|
230
272
|
.join('');
|
|
231
273
|
return `<text class="${cls}" x="${r(textX)}" y="${startY}">${tspans}</text>`;
|
|
232
274
|
}
|
|
@@ -488,20 +530,23 @@ export function renderProcessLayoutSvg(
|
|
|
488
530
|
options: RenderProcessOptions = {},
|
|
489
531
|
): string {
|
|
490
532
|
const { title = '', topInset = title ? 32 : 0 } = options;
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
533
|
+
const t = options.typography ?? DEFAULT_BPMN_TYPOGRAPHY;
|
|
534
|
+
return runWithTypography(t, () => {
|
|
535
|
+
const pb = layout.poolBounds;
|
|
536
|
+
const PAD = 16;
|
|
537
|
+
|
|
538
|
+
const svgW = r(pb.x + pb.width + PAD);
|
|
539
|
+
const svgH = r(pb.y + pb.height + PAD + topInset);
|
|
540
|
+
|
|
541
|
+
const embedCss = generateSvgEmbedCss('transitrix') + bpmnProcessCss(t);
|
|
542
|
+
const body = renderProcessBody(layout, 0, topInset);
|
|
543
|
+
|
|
544
|
+
return (
|
|
545
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${svgW}" height="${svgH}" viewBox="0 0 ${svgW} ${svgH}">\n` +
|
|
546
|
+
`<style>${embedCss}</style>\n` +
|
|
547
|
+
(title ? `${title}\n` : '') +
|
|
548
|
+
`${body}\n` +
|
|
549
|
+
`</svg>`
|
|
550
|
+
);
|
|
551
|
+
});
|
|
507
552
|
}
|