@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.
Files changed (68) hide show
  1. package/dist/activities/resolver.d.ts +3 -0
  2. package/dist/activities/resolver.js +39 -18
  3. package/dist/activities/validate.js +59 -0
  4. package/dist/canon-resolver-utils.d.ts +3 -0
  5. package/dist/canon-resolver-utils.js +16 -0
  6. package/dist/compliance/classify.js +1 -0
  7. package/dist/compliance/impact.d.ts +3 -1
  8. package/dist/compliance/impact.js +41 -13
  9. package/dist/compliance/types.d.ts +6 -0
  10. package/dist/compliance-verification-matrix/build.d.ts +17 -0
  11. package/dist/compliance-verification-matrix/build.js +65 -0
  12. package/dist/compliance-verification-matrix/csv.d.ts +6 -0
  13. package/dist/compliance-verification-matrix/csv.js +41 -0
  14. package/dist/compliance-verification-matrix/index.d.ts +3 -0
  15. package/dist/compliance-verification-matrix/index.js +2 -0
  16. package/dist/compliance-verification-matrix/types.d.ts +75 -0
  17. package/dist/compliance-verification-matrix/types.js +4 -0
  18. package/dist/fgca/action-change-ids.d.ts +7 -0
  19. package/dist/fgca/action-change-ids.js +11 -0
  20. package/dist/fgca/parse-canonical.d.ts +3 -1
  21. package/dist/fgca/parse-canonical.js +13 -8
  22. package/dist/fgca/resolver.d.ts +6 -2
  23. package/dist/fgca/resolver.js +12 -6
  24. package/dist/fgca/validate.d.ts +2 -0
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.js +1 -0
  27. package/dist/process-blueprint/index.d.ts +3 -1
  28. package/dist/process-blueprint/index.js +1 -0
  29. package/dist/process-blueprint/layout.js +33 -29
  30. package/dist/process-blueprint/resolve-columns.d.ts +52 -0
  31. package/dist/process-blueprint/resolve-columns.js +138 -0
  32. package/dist/process-blueprint/types.d.ts +23 -5
  33. package/dist/process-blueprint/validate.d.ts +19 -1
  34. package/dist/process-blueprint/validate.js +73 -21
  35. package/dist/repo-validate/check-process-parent.d.ts +8 -0
  36. package/dist/repo-validate/check-process-parent.js +172 -0
  37. package/dist/repo-validate/index.d.ts +1 -0
  38. package/dist/repo-validate/index.js +1 -0
  39. package/dist/repo-validate/validate-repo.js +3 -0
  40. package/dist/webview/render-fgca.js +1 -1
  41. package/dist/webview/render-process.d.ts +19 -1
  42. package/dist/webview/render-process.js +75 -42
  43. package/package.json +1 -1
  44. package/src/activities/resolver.ts +40 -20
  45. package/src/activities/validate.ts +58 -0
  46. package/src/canon-resolver-utils.ts +19 -0
  47. package/src/compliance/classify.ts +1 -0
  48. package/src/compliance/impact.ts +41 -12
  49. package/src/compliance/types.ts +6 -0
  50. package/src/compliance-verification-matrix/build.ts +84 -0
  51. package/src/compliance-verification-matrix/csv.ts +47 -0
  52. package/src/compliance-verification-matrix/index.ts +7 -0
  53. package/src/compliance-verification-matrix/types.ts +85 -0
  54. package/src/fgca/action-change-ids.ts +13 -0
  55. package/src/fgca/parse-canonical.ts +12 -8
  56. package/src/fgca/resolver.ts +15 -6
  57. package/src/fgca/validate.ts +2 -0
  58. package/src/index.ts +1 -0
  59. package/src/process-blueprint/index.ts +11 -0
  60. package/src/process-blueprint/layout.ts +48 -29
  61. package/src/process-blueprint/resolve-columns.ts +147 -0
  62. package/src/process-blueprint/types.ts +19 -5
  63. package/src/process-blueprint/validate.ts +107 -21
  64. package/src/repo-validate/check-process-parent.ts +184 -0
  65. package/src/repo-validate/index.ts +5 -0
  66. package/src/repo-validate/validate-repo.ts +3 -0
  67. package/src/webview/render-fgca.ts +1 -1
  68. 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
- /** Estimated horizontal advance (px) per character at the header label font size. */
96
- const HEADER_LABEL_CHAR_W = 6.5;
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
- /** Line height for wrapped task-name text (px). */
102
- const TASK_LINE_H = 14;
103
-
104
- /** Maximum characters per line for below-element labels (events, gateways). */
105
- const LABEL_CHARS = 18;
106
-
107
- /** Line height for multi-line below-element labels (px). */
108
- const LABEL_LINE_H = 13;
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 const BPMN_PROCESS_CSS = `
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: 11px; font-weight: 700; }
120
- .bpmn-lane-label { fill: var(--ts-text-primary,#0d2b35); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size: 11px; font-weight: 600; }
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: 11px; text-anchor: middle; }
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: 10px; text-anchor: middle; }
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: 10px; text-anchor: middle; }
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: 10px; text-anchor: middle; }
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 / HEADER_LABEL_CHAR_W));
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 > TASK_CHARS ? w.slice(0, TASK_CHARS - 1) + '…' : w;
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 <= TASK_CHARS) {
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 > TASK_CHARS - 1) lines[2] = lines[2].slice(0, TASK_CHARS - 1) + '…';
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, TASK_CHARS));
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 * TASK_LINE_H;
189
- const startY = cy - totalH / 2 + TASK_LINE_H * 0.8;
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 * TASK_LINE_H)}">${escXml(ln)}</tspan>`)
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 > LABEL_CHARS ? word.slice(0, LABEL_CHARS - 1) + '…' : word;
243
+ const tok = word.length > limit ? word.slice(0, limit - 1) + '…' : word;
202
244
  const test = cur ? `${cur} ${tok}` : tok;
203
- if (test.length <= LABEL_CHARS) {
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, LABEL_CHARS)];
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 <= LABEL_CHARS ? last + '…' : last.slice(0, LABEL_CHARS - 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 : LABEL_LINE_H}">${escXml(ln)}</tspan>`)
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 pb = layout.poolBounds;
492
- const PAD = 16;
493
-
494
- const svgW = r(pb.x + pb.width + PAD);
495
- const svgH = r(pb.y + pb.height + PAD + topInset);
496
-
497
- const embedCss = generateSvgEmbedCss('transitrix') + BPMN_PROCESS_CSS;
498
- const body = renderProcessBody(layout, 0, topInset);
499
-
500
- return (
501
- `<svg xmlns="http://www.w3.org/2000/svg" width="${svgW}" height="${svgH}" viewBox="0 0 ${svgW} ${svgH}">\n` +
502
- `<style>${embedCss}</style>\n` +
503
- (title ? `${title}\n` : '') +
504
- `${body}\n` +
505
- `</svg>`
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
  }