@bpmn-nova/studio 0.3.4-preview → 0.3.6-preview

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.
@@ -2,6 +2,7 @@ import { NODE_DEFINITIONS, PALETTE_GROUPS, PARALLEL_GATEWAY_PRESETS, edgeWaypoin
2
2
  import { createRuntimePresentation } from '../runtime/index.js';
3
3
  import { createDefaultIconRegistry, createIconElement, resolveNodeVisual } from '../icons/index.js';
4
4
  import { resolveNodeSubtitle, supportsNodeSubtitle } from '../node-presentation/index.js';
5
+ import { resolveNodeGeometry, resolvePresentationWaypoints } from '../node-geometry/index.js';
5
6
  import { ThemeController, applyRuntimeTone } from '../theme/index.js';
6
7
  import { exportDiagramSvg, openSvgExportPreview } from '../export-svg/index.js';
7
8
 
@@ -276,6 +277,26 @@ function nodeAccessibleLabel(title, subtitle) {
276
277
  return subtitle === null || subtitle === '' ? title : `${title}\n${subtitle}`;
277
278
  }
278
279
 
280
+ function applyNodeGeometry(shape, geometry) {
281
+ shape.classList.add('mb-node-scaled-shape');
282
+ shape.style.width = `${geometry.outline.width}px`;
283
+ shape.style.height = `${geometry.outline.height}px`;
284
+ shape.style.setProperty('--mb-node-shape-scale', String(geometry.scale));
285
+ shape.style.setProperty('--mb-node-shape-rotation', `${geometry.outline.rotation}rad`);
286
+ // CSS may quantize a 1.7px border to 1.5px; keep the document fold inside the outer outline.
287
+ if (geometry.kind === 'data') shape.style.clipPath = `inset(0 round ${geometry.outline.radiusX}px)`;
288
+ }
289
+
290
+ function geometryLabel(node, geometry) {
291
+ const label = el('div', 'mb-node-floating-label', node.name);
292
+ if (geometry) {
293
+ label.style.left = `${geometry.label.x - node.x}px`;
294
+ label.style.top = `${geometry.label.y - node.y}px`;
295
+ label.style.lineHeight = `${geometry.label.lineHeight}px`;
296
+ }
297
+ return label;
298
+ }
299
+
279
300
  function runtimeStatusIconId(presentation) {
280
301
  if (presentation?.status === 'rejected') return 'ui.statusRejected';
281
302
  if (presentation?.status === 'completed') return 'ui.statusCompleted';
@@ -364,17 +385,21 @@ export class DiagramRenderer {
364
385
  ['arrow-failed', 'var(--mb-danger)'],
365
386
  ];
366
387
  for (const [id] of markers) {
367
- const marker = svgEl('marker', { id, viewBox: '0 0 10 10', refX: '8.5', refY: '5', markerWidth: '7', markerHeight: '7', orient: 'auto-start-reverse', markerUnits: 'strokeWidth' });
368
- marker.appendChild(svgEl('path', { d: 'M 0 0 L 10 5 L 0 10 z', fill: 'context-stroke' }));
369
- defs.appendChild(marker);
388
+ for (const docked of [false, true]) {
389
+ const marker = svgEl('marker', { id: `${id}${docked ? '-docked' : ''}`, viewBox: '0 0 10 10', refX: docked ? '10' : '8.5', refY: '5', markerWidth: '7', markerHeight: '7', orient: 'auto-start-reverse', markerUnits: 'strokeWidth' });
390
+ marker.appendChild(svgEl('path', { d: 'M 0 0 L 10 5 L 0 10 z', fill: 'context-stroke' }));
391
+ defs.appendChild(marker);
392
+ }
370
393
  }
371
394
  this.edgeSvg.appendChild(defs);
372
395
 
373
396
  const runtimeDefs = svgEl('defs');
374
397
  for (const id of ['runtime-arrow-forward', 'runtime-arrow-reject', 'runtime-arrow-return', 'runtime-arrow-skip']) {
375
- const marker = svgEl('marker', { id, viewBox: '0 0 10 10', refX: '8.5', refY: '5', markerWidth: '7', markerHeight: '7', orient: 'auto-start-reverse', markerUnits: 'strokeWidth' });
376
- marker.appendChild(svgEl('path', { d: 'M 0 0 L 10 5 L 0 10 z', fill: 'context-stroke' }));
377
- runtimeDefs.appendChild(marker);
398
+ for (const docked of [false, true]) {
399
+ const marker = svgEl('marker', { id: `${id}${docked ? '-docked' : ''}`, viewBox: '0 0 10 10', refX: docked ? '10' : '8.5', refY: '5', markerWidth: '7', markerHeight: '7', orient: 'auto-start-reverse', markerUnits: 'strokeWidth' });
400
+ marker.appendChild(svgEl('path', { d: 'M 0 0 L 10 5 L 0 10 z', fill: 'context-stroke' }));
401
+ runtimeDefs.appendChild(marker);
402
+ }
378
403
  }
379
404
  this.runtimeTransitionSvg.appendChild(runtimeDefs);
380
405
 
@@ -712,6 +737,7 @@ export class DiagramRenderer {
712
737
  const boundsW = maxX - minX || 1;
713
738
  const boundsH = maxY - minY || 1;
714
739
  const rect = this.viewport.getBoundingClientRect();
740
+ if (rect.width < 1 || rect.height < 1) return;
715
741
  const maxZoom = options.maxZoom ?? 1.15;
716
742
  const minZoom = options.minZoom ?? 0.25;
717
743
  const rawZoom = Math.min((rect.width - padding * 2) / boundsW, (rect.height - padding * 2) / boundsH, maxZoom);
@@ -767,6 +793,7 @@ export class DiagramRenderer {
767
793
  ? presenter({ model: this.model, runtime: this.runtime, appearance: this.options.runtimeAppearance })
768
794
  : createRuntimePresentation({ model: this.model, runtime: null, appearance: this.options.runtimeAppearance });
769
795
  const subtitles = this._resolveDefinitionSubtitles();
796
+ this._nodeGeometries = new Map(this.model.nodes.map((node) => [node.id, resolveNodeGeometry(node, NODE_DEFINITIONS[node.type])]));
770
797
  this._syncSceneBounds();
771
798
  this._renderEdges();
772
799
  this._renderRuntimeTransitions();
@@ -832,8 +859,9 @@ export class DiagramRenderer {
832
859
  labelSize: { width: metrics.width, height: metrics.height },
833
860
  });
834
861
  if (!route) return;
835
- const points = route.points;
836
- occupiedRuntimeRoutes.push(points);
862
+ const display = resolvePresentationWaypoints(route.points, this._nodeGeometries.get(transition.sourceElementId), this._nodeGeometries.get(transition.targetElementId));
863
+ const points = display.points;
864
+ occupiedRuntimeRoutes.push(route.points);
837
865
  labelObstacles.push({
838
866
  x: route.labelPoint.x - metrics.width / 2,
839
867
  y: route.labelPoint.y - metrics.height / 2,
@@ -860,7 +888,7 @@ export class DiagramRenderer {
860
888
  d: pathData,
861
889
  class: 'mb-runtime-transition-path',
862
890
  fill: 'none',
863
- 'marker-end': `url(#runtime-arrow-${transition.type})`,
891
+ 'marker-end': `url(#runtime-arrow-${transition.type}${display.targetDocked ? '-docked' : ''})`,
864
892
  }));
865
893
  const labelX = route.labelPoint.x;
866
894
  const labelY = route.labelPoint.y;
@@ -900,9 +928,10 @@ export class DiagramRenderer {
900
928
  for (const edge of this.model.edges) {
901
929
  const points = edgeWaypoints(this.model, edge);
902
930
  if (points.length < 2) continue;
931
+ const display = resolvePresentationWaypoints(points, this._nodeGeometries.get(edge.source), this._nodeGeometries.get(edge.target));
903
932
  const corner = edge.cornerRadius ?? this.model.settings?.cornerRadius ?? 14;
904
933
  const routeStyle = edge.routeStyle || this.model.settings?.edgeStyle || 'rounded';
905
- const pathData = routePathData(points, routeStyle, corner);
934
+ const pathData = routePathData(display.points, routeStyle, corner);
906
935
 
907
936
  const edgeStatus = this.runtimePresentation.getEdge(edge.id).status;
908
937
  const selected = this.selection?.kind === 'edge' && this.selection.id === edge.id;
@@ -916,7 +945,7 @@ export class DiagramRenderer {
916
945
  class: 'mb-edge-path',
917
946
  fill: 'none',
918
947
  };
919
- if ((edge.type || 'sequenceFlow') === 'sequenceFlow') pathAttrs['marker-end'] = `url(#arrow${edgeStatus === 'idle' ? '' : `-${edgeStatus}`})`;
948
+ if ((edge.type || 'sequenceFlow') === 'sequenceFlow') pathAttrs['marker-end'] = `url(#arrow${edgeStatus === 'idle' ? '' : `-${edgeStatus}`}${display.targetDocked ? '-docked' : ''})`;
920
949
  const path = svgEl('path', pathAttrs);
921
950
  const hit = svgEl('path', { d: pathData, class: 'mb-edge-hit', fill: 'none' });
922
951
  let labelPosition = edgeLabelPoint(points);
@@ -1109,7 +1138,13 @@ export class DiagramRenderer {
1109
1138
  }
1110
1139
 
1111
1140
  if (def.kind === 'gateway') {
1141
+ const geometry = this._nodeGeometries.get(node.id);
1142
+ if (!geometry) {
1143
+ nodeEl.appendChild(geometryLabel(node, geometry));
1144
+ return;
1145
+ }
1112
1146
  const shape = el('div', 'mb-gateway-shape');
1147
+ applyNodeGeometry(shape, geometry);
1113
1148
  const symbol = el('span', 'mb-gateway-symbol');
1114
1149
  if (!this._renderCustomNodeContent(symbol, node, def, runtimeState, visual)) {
1115
1150
  const iconNode = visual.iconId
@@ -1131,23 +1166,19 @@ export class DiagramRenderer {
1131
1166
  nodeEl.appendChild(badge);
1132
1167
  }
1133
1168
  }
1134
- nodeEl.appendChild(el('div', 'mb-node-floating-label', node.name));
1169
+ nodeEl.appendChild(geometryLabel(node, geometry));
1135
1170
  return;
1136
1171
  }
1137
1172
 
1138
- if (def.kind === 'data') {
1139
- const page = el('div', 'mb-data-object-shape');
1140
- page.appendChild(el('span', 'mb-data-object-lines', '≡'));
1141
- nodeEl.appendChild(page);
1142
- nodeEl.appendChild(el('div', 'mb-node-floating-label', node.name));
1143
- return;
1144
- }
1145
-
1146
- if (def.kind === 'dataStore') {
1147
- const store = el('div', 'mb-data-store-shape');
1148
- store.appendChild(el('span', '', '≡'));
1149
- nodeEl.appendChild(store);
1150
- nodeEl.appendChild(el('div', 'mb-node-floating-label', node.name));
1173
+ if (def.kind === 'data' || def.kind === 'dataStore') {
1174
+ const geometry = this._nodeGeometries.get(node.id);
1175
+ if (geometry) {
1176
+ const shape = el('div', def.kind === 'data' ? 'mb-data-object-shape' : 'mb-data-store-shape');
1177
+ applyNodeGeometry(shape, geometry);
1178
+ shape.appendChild(el('span', def.kind === 'data' ? 'mb-data-object-lines' : '', '≡'));
1179
+ nodeEl.appendChild(shape);
1180
+ }
1181
+ nodeEl.appendChild(geometryLabel(node, geometry));
1151
1182
  return;
1152
1183
  }
1153
1184
 
@@ -1308,6 +1339,14 @@ export class DiagramRenderer {
1308
1339
 
1309
1340
  const appendPort = (side, { target = false, onClick } = {}) => {
1310
1341
  const port = el('button', `mb-port mb-port-${side}${target ? ' mb-port-target' : ''}`);
1342
+ const anchor = this._nodeGeometries.get(node.id)?.ports[side];
1343
+ if (anchor) {
1344
+ port.classList.add('mb-port-geometry');
1345
+ port.style.left = `${anchor.x - node.x}px`;
1346
+ port.style.top = `${anchor.y - node.y}px`;
1347
+ port.style.right = 'auto';
1348
+ port.style.bottom = 'auto';
1349
+ }
1311
1350
  port.type = 'button';
1312
1351
  port.title = target ? '连接到此节点' : (side === 'right' ? '创建连接' : '输入连接点');
1313
1352
  port.tabIndex = -1;
@@ -0,0 +1,22 @@
1
+ const DIAGNOSTIC_FIELDS = ['code', 'level', 'message', 'fieldPath', 'elementId', 'activityId', 'visitId', 'actionId', 'transitionId', 'edgeId'];
2
+
3
+ export function createDiagnostic(input = {}) {
4
+ const diagnostic = {
5
+ code: String(input.code || 'runtime-issue'),
6
+ level: input.level === 'error' || input.level === 'info' ? input.level : 'warning',
7
+ message: String(input.message || ''),
8
+ };
9
+ for (const field of DIAGNOSTIC_FIELDS.slice(3)) {
10
+ if (input[field]) diagnostic[field] = String(input[field]);
11
+ }
12
+ return Object.freeze(diagnostic);
13
+ }
14
+
15
+ export function pushDiagnostic(list, input) {
16
+ if (!list) return null;
17
+ const diagnostic = createDiagnostic(input);
18
+ if (!list.some((item) => item.code === diagnostic.code && item.fieldPath === diagnostic.fieldPath && item.activityId === diagnostic.activityId && item.visitId === diagnostic.visitId && item.actionId === diagnostic.actionId && item.transitionId === diagnostic.transitionId)) {
19
+ list.push(diagnostic);
20
+ }
21
+ return diagnostic;
22
+ }
@@ -51,9 +51,27 @@ export interface RuntimeApprovalActionPresentation extends Omit<RuntimeApprovalA
51
51
  assetCount: number
52
52
  order: number
53
53
  }
54
+ export interface RuntimeDiagnostic {
55
+ code: string
56
+ level: 'error' | 'warning' | 'info'
57
+ message: string
58
+ fieldPath?: string
59
+ elementId?: string
60
+ activityId?: string
61
+ visitId?: string
62
+ actionId?: string
63
+ transitionId?: string
64
+ edgeId?: string
65
+ }
66
+ export interface RuntimeInstant {
67
+ instant: number | null
68
+ raw: string
69
+ kind: 'offset' | 'z' | 'naive' | 'invalid' | 'missing'
70
+ }
54
71
  export interface RuntimeApprovalActionSummary {
55
72
  actions: RuntimeApprovalActionPresentation[]
56
73
  latestAction: RuntimeApprovalActionPresentation | null
74
+ latestNonEmptyComment: RuntimeApprovalActionPresentation | null
57
75
  actionText: string
58
76
  actionSummary: string
59
77
  imageCount: number
@@ -70,6 +88,7 @@ export interface ActivityInstance {
70
88
  assigneeId?: string
71
89
  participant?: RuntimeParticipant
72
90
  visitId?: string
91
+ supersedesVisitId?: string
73
92
  multiInstanceId?: string
74
93
  approvalMode?: 'single' | 'all' | 'any'
75
94
  multiInstanceMode?: 'parallel' | 'sequential'
@@ -92,6 +111,7 @@ export interface RuntimeTransition {
92
111
  state?: 'active' | 'resolved'
93
112
  resolvedAt?: string
94
113
  resolvedByActivityId?: string
114
+ invalidatedElementIds?: string[]
95
115
  invalidatedActivityIds?: string[]
96
116
  invalidatedEdgeIds?: string[]
97
117
  }
@@ -120,10 +140,18 @@ export interface RuntimeTransitionPresentation extends RuntimeTransition {
120
140
  label: string
121
141
  latest: boolean
122
142
  issues: string[]
143
+ invalidatedElementIds: string[]
123
144
  invalidatedActivityIds: string[]
124
145
  invalidatedEdgeIds: string[]
125
146
  action: RuntimeApprovalActionPresentation | null
126
147
  }
148
+ export interface ActivityVisitState {
149
+ status: ActivityStatus
150
+ records: ActivityInstance[]
151
+ latestRecords: ActivityInstance[]
152
+ visits: Array<{ id: string; round: number; records: ActivityInstance[]; effective?: boolean; superseded?: boolean }>
153
+ effectiveVisits: Array<{ id: string; round: number; records: ActivityInstance[]; effective?: boolean; superseded?: boolean }>
154
+ }
127
155
  export interface NodeRuntimePresentation extends RuntimeApprovalActionSummary {
128
156
  elementId: string
129
157
  status: ActivityStatus | 'rejected'
@@ -141,13 +169,15 @@ export interface NodeRuntimePresentation extends RuntimeApprovalActionSummary {
141
169
  round: number
142
170
  records: ActivityInstance[]
143
171
  latestRecords: ActivityInstance[]
144
- visits: Array<{ id: string; round: number; records: ActivityInstance[] } & RuntimeApprovalActionSummary>
172
+ visits: Array<{ id: string; round: number; records: ActivityInstance[]; effective: boolean; superseded: boolean } & RuntimeApprovalActionSummary>
173
+ effectiveVisits: Array<{ id: string; round: number; records: ActivityInstance[]; effective: boolean; superseded: boolean } & RuntimeApprovalActionSummary>
145
174
  transitions: RuntimeTransitionPresentation[]
146
175
  isReentry: boolean
147
176
  hasDetails: boolean
148
177
  }
149
178
  export interface RuntimePresentation {
150
179
  runtime: ProcessInstanceSnapshot
180
+ diagnostics: readonly RuntimeDiagnostic[]
151
181
  getNode(elementId: string): NodeRuntimePresentation
152
182
  getEdge(edgeId: string): { edgeId: string; status: ActivityStatus; visited: boolean; historicallyVisited: boolean; superseded: boolean }
153
183
  getAction(actionId: string): RuntimeApprovalActionPresentation | null
@@ -161,9 +191,13 @@ export interface RuntimeAppearanceLike {
161
191
  resolveTransition(transition?: { type?: string }): { tone: string }
162
192
  }
163
193
 
164
- export function normalizeRuntime(snapshot?: Partial<ProcessInstanceSnapshot> | null): ProcessInstanceSnapshot
165
- export function activityState(runtime: ProcessInstanceSnapshot | null | undefined, elementId: string): ActivityStatus
166
- export function createRuntimePresentation(context: { model: ProcessModel; runtime?: ProcessInstanceSnapshot | null; appearance?: RuntimeAppearanceLike | null }): RuntimePresentation
194
+ export function parseRuntimeInstant(value?: string | null): RuntimeInstant
195
+ export function formatRuntimeInstant(value?: string | RuntimeInstant | null): string
196
+ export function compareRuntimeOrder(left: { instant?: number | null; index?: number } | string | null | undefined, right: { instant?: number | null; index?: number } | string | null | undefined): number
197
+ export function normalizeRuntime(snapshot?: Partial<ProcessInstanceSnapshot> | null, options?: { diagnostics?: RuntimeDiagnostic[]; model?: ProcessModel | null }): ProcessInstanceSnapshot
198
+ export function activityState(runtime: ProcessInstanceSnapshot | null | undefined, elementId: string, options?: { diagnostics?: RuntimeDiagnostic[] }): ActivityVisitState
199
+ export function inspectRuntime(snapshot?: Partial<ProcessInstanceSnapshot> | null, options?: { model?: ProcessModel | null; appearance?: RuntimeAppearanceLike | null }): { runtime: ProcessInstanceSnapshot; diagnostics: readonly RuntimeDiagnostic[] }
200
+ export function createRuntimePresentation(context: { model: ProcessModel; runtime?: ProcessInstanceSnapshot | null; appearance?: RuntimeAppearanceLike | null; diagnostics?: RuntimeDiagnostic[] }): RuntimePresentation
167
201
  export function demoRuntime(): ProcessInstanceSnapshot
168
202
 
169
203
  export type RuntimeElement = BpmnNode | BpmnEdge