@bpmn-nova/studio 0.3.0-preview → 0.3.2-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.
- package/README.md +245 -20
- package/dist/canvas.js +7 -4
- package/dist/context-menu.js +2 -2
- package/dist/controller.js +84 -6
- package/dist/index.d.ts +138 -38
- package/dist/index.js +12 -11
- package/dist/interactions.js +1 -1
- package/dist/modules/bpmn-model/index.d.ts +11 -0
- package/dist/modules/bpmn-model/index.js +703 -0
- package/dist/modules/core/containment.js +590 -0
- package/dist/modules/core/gateway.js +72 -0
- package/dist/modules/core/history.js +32 -0
- package/dist/modules/core/index.d.ts +268 -0
- package/dist/modules/core/index.js +7 -0
- package/dist/modules/core/layout.js +644 -0
- package/dist/modules/core/model.js +287 -0
- package/dist/modules/core/runtime-transition-route.js +360 -0
- package/dist/modules/core/scope.js +99 -0
- package/dist/modules/designer/index.d.ts +79 -0
- package/dist/modules/designer/index.js +607 -0
- package/dist/modules/engine-activiti/index.d.ts +19 -0
- package/dist/modules/engine-activiti/index.js +160 -0
- package/dist/modules/engine-flowable/index.d.ts +19 -0
- package/dist/modules/engine-flowable/index.js +160 -0
- package/dist/modules/export-svg/index.d.ts +112 -0
- package/dist/modules/export-svg/index.js +2 -0
- package/dist/modules/export-svg/preview.js +327 -0
- package/dist/modules/export-svg/render.js +718 -0
- package/dist/modules/icons/index.d.ts +24 -0
- package/dist/modules/icons/index.js +264 -0
- package/dist/modules/palette/index.d.ts +74 -0
- package/dist/modules/palette/index.js +99 -0
- package/dist/modules/palette/panel.js +99 -0
- package/dist/modules/properties/index.d.ts +20 -0
- package/dist/modules/properties/index.js +19 -0
- package/dist/modules/properties-activiti/index.d.ts +3 -0
- package/dist/modules/properties-activiti/index.js +97 -0
- package/dist/modules/properties-bpmn/index.d.ts +3 -0
- package/dist/modules/properties-bpmn/index.js +518 -0
- package/dist/modules/properties-core/index.d.ts +124 -0
- package/dist/modules/properties-core/index.js +312 -0
- package/dist/modules/properties-flowable/index.d.ts +3 -0
- package/dist/modules/properties-flowable/index.js +114 -0
- package/dist/modules/properties-renderer/index.d.ts +25 -0
- package/dist/modules/properties-renderer/index.js +491 -0
- package/dist/modules/renderer-svg/index.d.ts +118 -0
- package/dist/modules/renderer-svg/index.js +1460 -0
- package/dist/modules/runtime/index.d.ts +169 -0
- package/dist/modules/runtime/index.js +535 -0
- package/dist/modules/theme/index.d.ts +95 -0
- package/dist/modules/theme/index.js +368 -0
- package/dist/modules/viewer/index.d.ts +265 -0
- package/dist/modules/viewer/index.js +1011 -0
- package/dist/modules/viewer/runtime-content.js +123 -0
- package/dist/modules/viewer/runtime-details-motion.js +228 -0
- package/dist/modules/viewer/runtime-trace.js +574 -0
- package/dist/modules/viewer/timeline.js +276 -0
- package/dist/selection-layout.js +1 -1
- package/dist/shell.js +210 -26
- package/dist/styles.css +116 -7
- package/llms-full.txt +3082 -0
- package/llms.txt +225 -0
- package/package.json +39 -16
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
import { NODE_DEFINITIONS } from './model.js';
|
|
2
|
+
|
|
3
|
+
export const CONTAINMENT_LIMITS = Object.freeze({
|
|
4
|
+
groupMinWidth: 120,
|
|
5
|
+
groupMinHeight: 80,
|
|
6
|
+
groupPadding: 28,
|
|
7
|
+
participantMinWidth: 320,
|
|
8
|
+
participantMinHeight: 120,
|
|
9
|
+
laneMinWidth: 240,
|
|
10
|
+
laneMinHeight: 72,
|
|
11
|
+
swimlaneTitleSize: 38,
|
|
12
|
+
participantRailWidth: 38,
|
|
13
|
+
participantContentPadding: 24,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const SWIMLANE_LABEL_PLACEMENTS = Object.freeze({
|
|
17
|
+
side: 'side',
|
|
18
|
+
top: 'top',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const SWIMLANE_LABEL_PLACEMENT_VALUES = new Set(Object.values(SWIMLANE_LABEL_PLACEMENTS));
|
|
22
|
+
|
|
23
|
+
const FLOW_NODE_KINDS = new Set(['event', 'boundary', 'task', 'container', 'gateway']);
|
|
24
|
+
|
|
25
|
+
function kindOf(node) {
|
|
26
|
+
return NODE_DEFINITIONS[node?.type]?.kind;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeSwimlaneLabelPlacement(value) {
|
|
30
|
+
return SWIMLANE_LABEL_PLACEMENT_VALUES.has(value) ? value : SWIMLANE_LABEL_PLACEMENTS.side;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function insetsForSwimlaneLabelPlacement(placement) {
|
|
34
|
+
const size = CONTAINMENT_LIMITS.swimlaneTitleSize;
|
|
35
|
+
return normalizeSwimlaneLabelPlacement(placement) === SWIMLANE_LABEL_PLACEMENTS.top
|
|
36
|
+
? { top: size, right: 0, bottom: 0, left: 0 }
|
|
37
|
+
: { top: 0, right: 0, bottom: 0, left: size };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resolveSwimlaneLabelPlacement(model, node) {
|
|
41
|
+
if (!node || !['participant', 'lane'].includes(kindOf(node))) return SWIMLANE_LABEL_PLACEMENTS.side;
|
|
42
|
+
let source = node;
|
|
43
|
+
if (kindOf(node) === 'lane' && node.containerId) {
|
|
44
|
+
const participant = model?.nodes?.find((candidate) => candidate.id === node.containerId && kindOf(candidate) === 'participant');
|
|
45
|
+
if (participant) source = participant;
|
|
46
|
+
}
|
|
47
|
+
return normalizeSwimlaneLabelPlacement(source.properties?.swimlaneLabelPlacement);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getSwimlaneContentInsets(model, node) {
|
|
51
|
+
return insetsForSwimlaneLabelPlacement(resolveSwimlaneLabelPlacement(model, node));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getSwimlaneContentBounds(model, node) {
|
|
55
|
+
const insets = getSwimlaneContentInsets(model, node);
|
|
56
|
+
return {
|
|
57
|
+
x: node.x + insets.left,
|
|
58
|
+
y: node.y + insets.top,
|
|
59
|
+
width: Math.max(0, node.width - insets.left - insets.right),
|
|
60
|
+
height: Math.max(0, node.height - insets.top - insets.bottom),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isFlowNode(node) {
|
|
65
|
+
return FLOW_NODE_KINDS.has(kindOf(node));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function center(node) {
|
|
69
|
+
return { x: node.x + node.width / 2, y: node.y + node.height / 2 };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function containsPoint(node, point, inset = 0) {
|
|
73
|
+
return point.x >= node.x + inset
|
|
74
|
+
&& point.x <= node.x + node.width - inset
|
|
75
|
+
&& point.y >= node.y + inset
|
|
76
|
+
&& point.y <= node.y + node.height - inset;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function laneReferences(lane) {
|
|
80
|
+
return Array.isArray(lane?.properties?.flowNodeRefs) ? lane.properties.flowNodeRefs : [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function setLaneReferences(lane, refs) {
|
|
84
|
+
lane.properties = { ...(lane.properties || {}), flowNodeRefs: [...new Set(refs)] };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isCurrentProcessParticipant(model, node) {
|
|
88
|
+
return kindOf(node) === 'participant'
|
|
89
|
+
&& (node.scopeId || model.id) === model.id
|
|
90
|
+
&& node.properties?.processRef === model.id;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function participantLanes(model, participantId) {
|
|
94
|
+
return model.nodes
|
|
95
|
+
.filter((node) => kindOf(node) === 'lane' && node.containerId === participantId)
|
|
96
|
+
.sort((a, b) => a.y - b.y || a.x - b.x || a.id.localeCompare(b.id));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function memberIdsForLane(model, lane) {
|
|
100
|
+
const ids = new Set(laneReferences(lane));
|
|
101
|
+
for (const node of model.nodes) {
|
|
102
|
+
if (kindOf(node) !== 'boundary') continue;
|
|
103
|
+
if (ids.has(node.properties?.attachedToRef)) ids.add(node.id);
|
|
104
|
+
}
|
|
105
|
+
return ids;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function minimumLaneHeight(model, lane) {
|
|
109
|
+
const insets = getSwimlaneContentInsets(model, lane);
|
|
110
|
+
return CONTAINMENT_LIMITS.laneMinHeight + insets.top + insets.bottom;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function minimumParticipantHeight(model, participant, lanes = participantLanes(model, participant.id)) {
|
|
114
|
+
const insets = getSwimlaneContentInsets(model, participant);
|
|
115
|
+
const lanesHeight = lanes.reduce((sum, lane) => sum + minimumLaneHeight(model, lane), 0);
|
|
116
|
+
return Math.max(
|
|
117
|
+
CONTAINMENT_LIMITS.participantMinHeight,
|
|
118
|
+
insets.top + insets.bottom + lanesHeight,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function contentOriginForPlacement(node, placement) {
|
|
123
|
+
const insets = insetsForSwimlaneLabelPlacement(placement);
|
|
124
|
+
return { x: node.x + insets.left, y: node.y + insets.top };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function translateLaneMembersForPlacementChange(model, lane, previousPlacement) {
|
|
128
|
+
const nextPlacement = resolveSwimlaneLabelPlacement(model, lane);
|
|
129
|
+
if (nextPlacement === normalizeSwimlaneLabelPlacement(previousPlacement)) return;
|
|
130
|
+
const previousInsets = insetsForSwimlaneLabelPlacement(previousPlacement);
|
|
131
|
+
const nextInsets = insetsForSwimlaneLabelPlacement(nextPlacement);
|
|
132
|
+
translateNodes(
|
|
133
|
+
model,
|
|
134
|
+
memberIdsForLane(model, lane),
|
|
135
|
+
nextInsets.left - previousInsets.left,
|
|
136
|
+
nextInsets.top - previousInsets.top,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function translateNodes(model, ids, dx, dy) {
|
|
141
|
+
if (!dx && !dy) return;
|
|
142
|
+
for (const node of model.nodes) {
|
|
143
|
+
if (!ids.has(node.id)) continue;
|
|
144
|
+
node.x += dx;
|
|
145
|
+
node.y += dy;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function layoutParticipantLanes(model, participant, lanes, options = {}) {
|
|
150
|
+
if (!lanes.length) return;
|
|
151
|
+
const insets = getSwimlaneContentInsets(model, participant);
|
|
152
|
+
const previous = new Map(lanes.map((lane) => [lane.id, { x: lane.x, y: lane.y }]));
|
|
153
|
+
const total = lanes.reduce((sum, lane) => sum + Math.max(minimumLaneHeight(model, lane), lane.height), 0);
|
|
154
|
+
const contentHeight = Math.max(0, participant.height - insets.top - insets.bottom);
|
|
155
|
+
if (total < contentHeight) {
|
|
156
|
+
lanes[lanes.length - 1].height = Math.max(
|
|
157
|
+
minimumLaneHeight(model, lanes[lanes.length - 1]),
|
|
158
|
+
lanes[lanes.length - 1].height + contentHeight - total,
|
|
159
|
+
);
|
|
160
|
+
} else if (total > contentHeight) {
|
|
161
|
+
participant.height = Math.max(
|
|
162
|
+
minimumParticipantHeight(model, participant, lanes),
|
|
163
|
+
insets.top + total + insets.bottom,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let y = participant.y + insets.top;
|
|
168
|
+
for (const lane of lanes) {
|
|
169
|
+
lane.height = Math.max(minimumLaneHeight(model, lane), lane.height);
|
|
170
|
+
lane.x = participant.x + insets.left;
|
|
171
|
+
lane.y = y;
|
|
172
|
+
lane.width = Math.max(1, participant.width - insets.left - insets.right);
|
|
173
|
+
y += lane.height;
|
|
174
|
+
}
|
|
175
|
+
participant.height = Math.max(
|
|
176
|
+
minimumParticipantHeight(model, participant, lanes),
|
|
177
|
+
y - participant.y + insets.bottom,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
if (options.moveMembers !== false) {
|
|
181
|
+
for (const lane of lanes) {
|
|
182
|
+
const before = previous.get(lane.id);
|
|
183
|
+
if (!before) continue;
|
|
184
|
+
translateNodes(model, memberIdsForLane(model, lane), lane.x - before.x, lane.y - before.y);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function eligibleParticipantAt(model, point) {
|
|
190
|
+
return model.nodes
|
|
191
|
+
.filter((node) => isCurrentProcessParticipant(model, node))
|
|
192
|
+
.filter((node) => containsPoint(node, point))
|
|
193
|
+
.sort((a, b) => a.width * a.height - b.width * b.height)[0] || null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function laneAt(model, point, excludedId = '', scopeId = null) {
|
|
197
|
+
return model.nodes
|
|
198
|
+
.filter((node) => kindOf(node) === 'lane' && node.id !== excludedId)
|
|
199
|
+
.filter((node) => scopeId === null || (node.scopeId || model.id) === scopeId)
|
|
200
|
+
.filter((node) => containsPoint(node, point))
|
|
201
|
+
.sort((a, b) => a.width * a.height - b.width * b.height)[0] || null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function removeNodeFromLanes(model, nodeId) {
|
|
205
|
+
let changed = false;
|
|
206
|
+
for (const lane of model.nodes.filter((node) => kindOf(node) === 'lane')) {
|
|
207
|
+
const refs = laneReferences(lane);
|
|
208
|
+
const next = refs.filter((id) => id !== nodeId);
|
|
209
|
+
if (next.length !== refs.length) {
|
|
210
|
+
setLaneReferences(lane, next);
|
|
211
|
+
changed = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return changed;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function assignNodeToLane(model, nodeId) {
|
|
218
|
+
const node = model.nodes.find((candidate) => candidate.id === nodeId);
|
|
219
|
+
if (!node || !isFlowNode(node)) return false;
|
|
220
|
+
const inheritedHostId = kindOf(node) === 'boundary' ? node.properties?.attachedToRef : '';
|
|
221
|
+
const lookupId = inheritedHostId || nodeId;
|
|
222
|
+
const lookupNode = model.nodes.find((candidate) => candidate.id === lookupId) || node;
|
|
223
|
+
const target = laneAt(model, center(lookupNode), '', lookupNode.scopeId || model.id);
|
|
224
|
+
let changed = removeNodeFromLanes(model, nodeId);
|
|
225
|
+
if (!target) return changed;
|
|
226
|
+
const refs = laneReferences(target);
|
|
227
|
+
if (!refs.includes(nodeId)) {
|
|
228
|
+
setLaneReferences(target, [...refs, nodeId]);
|
|
229
|
+
changed = true;
|
|
230
|
+
}
|
|
231
|
+
return changed;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function reconcile(model, options = {}) {
|
|
235
|
+
const nodeIds = new Set(model.nodes.map((node) => node.id));
|
|
236
|
+
const participants = new Map(
|
|
237
|
+
model.nodes.filter((node) => isCurrentProcessParticipant(model, node)).map((node) => [node.id, node]),
|
|
238
|
+
);
|
|
239
|
+
let changed = false;
|
|
240
|
+
|
|
241
|
+
for (const lane of model.nodes.filter((node) => kindOf(node) === 'lane')) {
|
|
242
|
+
if (lane.containerId && !participants.has(lane.containerId)) {
|
|
243
|
+
delete lane.containerId;
|
|
244
|
+
changed = true;
|
|
245
|
+
}
|
|
246
|
+
if (!lane.containerId && options.inferOwners) {
|
|
247
|
+
const owner = eligibleParticipantAt(model, center(lane));
|
|
248
|
+
if (owner) {
|
|
249
|
+
lane.containerId = owner.id;
|
|
250
|
+
changed = true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const refs = laneReferences(lane);
|
|
254
|
+
const laneScopeId = lane.scopeId || model.id;
|
|
255
|
+
const next = refs.filter((id) => {
|
|
256
|
+
const referenced = model.nodes.find((node) => node.id === id);
|
|
257
|
+
return nodeIds.has(id) && isFlowNode(referenced) && (referenced.scopeId || model.id) === laneScopeId;
|
|
258
|
+
});
|
|
259
|
+
if (next.length !== refs.length || new Set(next).size !== next.length) {
|
|
260
|
+
setLaneReferences(lane, next);
|
|
261
|
+
changed = true;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const claimed = new Set();
|
|
266
|
+
for (const lane of model.nodes.filter((node) => kindOf(node) === 'lane')) {
|
|
267
|
+
const next = laneReferences(lane).filter((id) => {
|
|
268
|
+
if (claimed.has(id)) return false;
|
|
269
|
+
claimed.add(id);
|
|
270
|
+
return true;
|
|
271
|
+
});
|
|
272
|
+
if (next.length !== laneReferences(lane).length) {
|
|
273
|
+
setLaneReferences(lane, next);
|
|
274
|
+
changed = true;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
for (const boundary of model.nodes.filter((node) => kindOf(node) === 'boundary')) {
|
|
279
|
+
const hostId = boundary.properties?.attachedToRef;
|
|
280
|
+
if (!hostId) continue;
|
|
281
|
+
const ownerLane = model.nodes.find((lane) => kindOf(lane) === 'lane' && laneReferences(lane).includes(hostId));
|
|
282
|
+
const currentLane = model.nodes.find((lane) => kindOf(lane) === 'lane' && laneReferences(lane).includes(boundary.id));
|
|
283
|
+
if (currentLane?.id !== ownerLane?.id) {
|
|
284
|
+
removeNodeFromLanes(model, boundary.id);
|
|
285
|
+
if (ownerLane) setLaneReferences(ownerLane, [...laneReferences(ownerLane), boundary.id]);
|
|
286
|
+
changed = true;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (options.normalize) {
|
|
291
|
+
for (const participant of participants.values()) {
|
|
292
|
+
const lanes = participantLanes(model, participant.id);
|
|
293
|
+
if (lanes.length) layoutParticipantLanes(model, participant, lanes, { moveMembers: false });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return changed;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function attachLane(model, lane, participant, index) {
|
|
300
|
+
if (!lane
|
|
301
|
+
|| kindOf(lane) !== 'lane'
|
|
302
|
+
|| (lane.scopeId || model.id) !== model.id
|
|
303
|
+
|| !isCurrentProcessParticipant(model, participant)) return false;
|
|
304
|
+
let previousPlacement = resolveSwimlaneLabelPlacement(model, lane);
|
|
305
|
+
if (lane.containerId === participant.id) {
|
|
306
|
+
const lanes = participantLanes(model, participant.id).filter((candidate) => candidate.id !== lane.id);
|
|
307
|
+
const nextIndex = Math.max(0, Math.min(index ?? lanes.length, lanes.length));
|
|
308
|
+
lanes.splice(nextIndex, 0, lane);
|
|
309
|
+
layoutParticipantLanes(model, participant, lanes);
|
|
310
|
+
translateLaneMembersForPlacementChange(model, lane, previousPlacement);
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (lane.containerId) {
|
|
315
|
+
detachLane(model, lane, { preservePosition: true });
|
|
316
|
+
previousPlacement = resolveSwimlaneLabelPlacement(model, lane);
|
|
317
|
+
}
|
|
318
|
+
const existing = participantLanes(model, participant.id);
|
|
319
|
+
lane.containerId = participant.id;
|
|
320
|
+
if (!existing.length) {
|
|
321
|
+
const content = getSwimlaneContentBounds(model, participant);
|
|
322
|
+
lane.height = Math.max(minimumLaneHeight(model, lane), content.height);
|
|
323
|
+
layoutParticipantLanes(model, participant, [lane]);
|
|
324
|
+
translateLaneMembersForPlacementChange(model, lane, previousPlacement);
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
lane.height = Math.max(minimumLaneHeight(model, lane), lane.height);
|
|
329
|
+
const nextIndex = Math.max(0, Math.min(index ?? existing.length, existing.length));
|
|
330
|
+
existing.splice(nextIndex, 0, lane);
|
|
331
|
+
participant.height += lane.height;
|
|
332
|
+
layoutParticipantLanes(model, participant, existing);
|
|
333
|
+
translateLaneMembersForPlacementChange(model, lane, previousPlacement);
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function detachLane(model, lane, options = {}) {
|
|
338
|
+
if (!lane?.containerId) return false;
|
|
339
|
+
const previousPlacement = resolveSwimlaneLabelPlacement(model, lane);
|
|
340
|
+
const participant = model.nodes.find((node) => node.id === lane.containerId && kindOf(node) === 'participant');
|
|
341
|
+
const detachedHeight = lane.height;
|
|
342
|
+
delete lane.containerId;
|
|
343
|
+
if (participant) {
|
|
344
|
+
const remaining = participantLanes(model, participant.id);
|
|
345
|
+
if (remaining.length) {
|
|
346
|
+
participant.height = Math.max(minimumParticipantHeight(model, participant, remaining), participant.height - detachedHeight);
|
|
347
|
+
layoutParticipantLanes(model, participant, remaining);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
lane.width = Math.max(CONTAINMENT_LIMITS.laneMinWidth, lane.width);
|
|
351
|
+
lane.height = Math.max(minimumLaneHeight(model, lane), lane.height);
|
|
352
|
+
if (!options.preservePosition && Number.isFinite(options.x) && Number.isFinite(options.y)) {
|
|
353
|
+
const dx = options.x - lane.x;
|
|
354
|
+
const dy = options.y - lane.y;
|
|
355
|
+
lane.x = options.x;
|
|
356
|
+
lane.y = options.y;
|
|
357
|
+
translateNodes(model, memberIdsForLane(model, lane), dx, dy);
|
|
358
|
+
}
|
|
359
|
+
translateLaneMembersForPlacementChange(model, lane, previousPlacement);
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function moveContainer(model, node, dx, dy) {
|
|
364
|
+
if (!node || (!dx && !dy)) return false;
|
|
365
|
+
const ids = new Set([node.id]);
|
|
366
|
+
if (kindOf(node) === 'participant') {
|
|
367
|
+
const lanes = participantLanes(model, node.id);
|
|
368
|
+
for (const lane of lanes) {
|
|
369
|
+
ids.add(lane.id);
|
|
370
|
+
for (const memberId of memberIdsForLane(model, lane)) ids.add(memberId);
|
|
371
|
+
}
|
|
372
|
+
} else if (kindOf(node) === 'lane') {
|
|
373
|
+
for (const memberId of memberIdsForLane(model, node)) ids.add(memberId);
|
|
374
|
+
}
|
|
375
|
+
translateNodes(model, ids, dx, dy);
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function memberCenters(model, lanes) {
|
|
380
|
+
const ids = new Set(lanes.flatMap((lane) => [...memberIdsForLane(model, lane)]));
|
|
381
|
+
return model.nodes.filter((node) => ids.has(node.id)).map(center);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function resizeParticipant(model, participant, requested) {
|
|
385
|
+
const old = { x: participant.x, y: participant.y, width: participant.width, height: participant.height };
|
|
386
|
+
const lanes = participantLanes(model, participant.id);
|
|
387
|
+
const points = memberCenters(model, lanes);
|
|
388
|
+
const participantInsets = getSwimlaneContentInsets(model, participant);
|
|
389
|
+
const laneInsets = lanes[0] ? getSwimlaneContentInsets(model, lanes[0]) : { top: 0, right: 0, bottom: 0, left: 0 };
|
|
390
|
+
const padding = CONTAINMENT_LIMITS.participantContentPadding;
|
|
391
|
+
const minLaneHeight = lanes.reduce((sum, lane) => sum + minimumLaneHeight(model, lane), 0);
|
|
392
|
+
let left = requested.x;
|
|
393
|
+
let top = requested.y;
|
|
394
|
+
let right = requested.x + requested.width;
|
|
395
|
+
let bottom = requested.y + requested.height;
|
|
396
|
+
|
|
397
|
+
if (points.length) {
|
|
398
|
+
const minX = Math.min(...points.map((point) => point.x));
|
|
399
|
+
const maxX = Math.max(...points.map((point) => point.x));
|
|
400
|
+
const minY = Math.min(...points.map((point) => point.y));
|
|
401
|
+
const maxY = Math.max(...points.map((point) => point.y));
|
|
402
|
+
left = Math.min(left, minX - participantInsets.left - laneInsets.left - padding);
|
|
403
|
+
right = Math.max(right, maxX + participantInsets.right + laneInsets.right + padding);
|
|
404
|
+
top = Math.min(top, minY - participantInsets.top - laneInsets.top - padding);
|
|
405
|
+
bottom = Math.max(bottom, maxY + participantInsets.bottom + laneInsets.bottom + padding);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const minWidth = CONTAINMENT_LIMITS.participantMinWidth;
|
|
409
|
+
const minHeight = Math.max(
|
|
410
|
+
CONTAINMENT_LIMITS.participantMinHeight,
|
|
411
|
+
participantInsets.top + minLaneHeight + participantInsets.bottom,
|
|
412
|
+
);
|
|
413
|
+
if (right - left < minWidth) right = left + minWidth;
|
|
414
|
+
if (bottom - top < minHeight) bottom = top + minHeight;
|
|
415
|
+
participant.x = left;
|
|
416
|
+
participant.y = top;
|
|
417
|
+
participant.width = right - left;
|
|
418
|
+
participant.height = bottom - top;
|
|
419
|
+
|
|
420
|
+
if (lanes.length) {
|
|
421
|
+
const topDelta = top - old.y;
|
|
422
|
+
const bottomDelta = bottom - (old.y + old.height);
|
|
423
|
+
lanes[0].height = Math.max(minimumLaneHeight(model, lanes[0]), lanes[0].height - topDelta);
|
|
424
|
+
lanes[lanes.length - 1].height = Math.max(
|
|
425
|
+
minimumLaneHeight(model, lanes[lanes.length - 1]),
|
|
426
|
+
lanes[lanes.length - 1].height + bottomDelta,
|
|
427
|
+
);
|
|
428
|
+
const laneHeight = lanes.reduce((sum, lane) => sum + lane.height, 0);
|
|
429
|
+
const contentHeight = participant.height - participantInsets.top - participantInsets.bottom;
|
|
430
|
+
const resizingFromTop = Math.abs(bottom - (old.y + old.height)) < 0.5 && Math.abs(top - old.y) >= 0.5;
|
|
431
|
+
if (laneHeight > contentHeight && resizingFromTop) {
|
|
432
|
+
participant.height = laneHeight + participantInsets.top + participantInsets.bottom;
|
|
433
|
+
participant.y = bottom - participant.height;
|
|
434
|
+
}
|
|
435
|
+
layoutParticipantLanes(model, participant, lanes, { moveMembers: false });
|
|
436
|
+
}
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function resizeNode(model, node, requested) {
|
|
441
|
+
if (!node || !requested) return false;
|
|
442
|
+
const kind = kindOf(node);
|
|
443
|
+
if (kind === 'participant') return resizeParticipant(model, node, requested);
|
|
444
|
+
if (kind === 'lane' && node.containerId) return false;
|
|
445
|
+
const minimum = kind === 'group'
|
|
446
|
+
? { width: CONTAINMENT_LIMITS.groupMinWidth, height: CONTAINMENT_LIMITS.groupMinHeight }
|
|
447
|
+
: kind === 'lane'
|
|
448
|
+
? { width: CONTAINMENT_LIMITS.laneMinWidth, height: minimumLaneHeight(model, node) }
|
|
449
|
+
: null;
|
|
450
|
+
if (!minimum) return false;
|
|
451
|
+
node.x = requested.x;
|
|
452
|
+
node.y = requested.y;
|
|
453
|
+
node.width = Math.max(minimum.width, requested.width);
|
|
454
|
+
node.height = Math.max(minimum.height, requested.height);
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function resizeDivider(model, lane, delta) {
|
|
459
|
+
if (!lane?.containerId || !delta) return false;
|
|
460
|
+
const participant = model.nodes.find((node) => node.id === lane.containerId);
|
|
461
|
+
const lanes = participant ? participantLanes(model, participant.id) : [];
|
|
462
|
+
const index = lanes.findIndex((candidate) => candidate.id === lane.id);
|
|
463
|
+
if (index < 0 || index === lanes.length - 1) return false;
|
|
464
|
+
const upper = lanes[index];
|
|
465
|
+
const lower = lanes[index + 1];
|
|
466
|
+
const upperMinimum = minimumLaneHeight(model, upper);
|
|
467
|
+
const lowerMinimum = minimumLaneHeight(model, lower);
|
|
468
|
+
const clamped = Math.max(
|
|
469
|
+
upperMinimum - upper.height,
|
|
470
|
+
Math.min(delta, lower.height - lowerMinimum),
|
|
471
|
+
);
|
|
472
|
+
if (!clamped) return false;
|
|
473
|
+
upper.height += clamped;
|
|
474
|
+
lower.height -= clamped;
|
|
475
|
+
layoutParticipantLanes(model, participant, lanes);
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function updateLabelPlacement(model, node, previousPlacement) {
|
|
480
|
+
const kind = kindOf(node);
|
|
481
|
+
const previous = normalizeSwimlaneLabelPlacement(previousPlacement);
|
|
482
|
+
const next = resolveSwimlaneLabelPlacement(model, node);
|
|
483
|
+
if (!['participant', 'lane'].includes(kind) || previous === next) return false;
|
|
484
|
+
if (kind === 'lane' && node.containerId) return false;
|
|
485
|
+
|
|
486
|
+
if (kind === 'lane') {
|
|
487
|
+
const previousOrigin = contentOriginForPlacement(node, previous);
|
|
488
|
+
const previousInsets = insetsForSwimlaneLabelPlacement(previous);
|
|
489
|
+
const nextInsets = insetsForSwimlaneLabelPlacement(next);
|
|
490
|
+
node.height = Math.max(
|
|
491
|
+
minimumLaneHeight(model, node),
|
|
492
|
+
node.height + nextInsets.top + nextInsets.bottom - previousInsets.top - previousInsets.bottom,
|
|
493
|
+
);
|
|
494
|
+
const nextOrigin = contentOriginForPlacement(node, next);
|
|
495
|
+
translateNodes(model, memberIdsForLane(model, node), nextOrigin.x - previousOrigin.x, nextOrigin.y - previousOrigin.y);
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const lanes = participantLanes(model, node.id);
|
|
500
|
+
const previousOrigins = new Map(lanes.map((lane) => [lane.id, contentOriginForPlacement(lane, previous)]));
|
|
501
|
+
const previousInsets = insetsForSwimlaneLabelPlacement(previous);
|
|
502
|
+
const nextInsets = insetsForSwimlaneLabelPlacement(next);
|
|
503
|
+
const laneHeightDelta = nextInsets.top + nextInsets.bottom - previousInsets.top - previousInsets.bottom;
|
|
504
|
+
let totalLaneHeightDelta = 0;
|
|
505
|
+
for (const lane of lanes) {
|
|
506
|
+
const before = lane.height;
|
|
507
|
+
lane.height = Math.max(minimumLaneHeight(model, lane), lane.height + laneHeightDelta);
|
|
508
|
+
totalLaneHeightDelta += lane.height - before;
|
|
509
|
+
}
|
|
510
|
+
node.height = Math.max(
|
|
511
|
+
minimumParticipantHeight(model, node, lanes),
|
|
512
|
+
node.height + nextInsets.top + nextInsets.bottom - previousInsets.top - previousInsets.bottom + totalLaneHeightDelta,
|
|
513
|
+
);
|
|
514
|
+
if (!lanes.length) return true;
|
|
515
|
+
|
|
516
|
+
layoutParticipantLanes(model, node, lanes, { moveMembers: false });
|
|
517
|
+
for (const lane of lanes) {
|
|
518
|
+
const before = previousOrigins.get(lane.id);
|
|
519
|
+
const after = contentOriginForPlacement(lane, next);
|
|
520
|
+
translateNodes(model, memberIdsForLane(model, lane), after.x - before.x, after.y - before.y);
|
|
521
|
+
}
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export function resolveContainment(model) {
|
|
526
|
+
const nodesById = new Map((model?.nodes || []).map((node) => [node.id, node]));
|
|
527
|
+
const lanesByParticipant = new Map();
|
|
528
|
+
const laneByNode = new Map();
|
|
529
|
+
for (const node of model?.nodes || []) {
|
|
530
|
+
if (kindOf(node) !== 'lane') continue;
|
|
531
|
+
if (node.containerId) {
|
|
532
|
+
const lanes = lanesByParticipant.get(node.containerId) || [];
|
|
533
|
+
lanes.push(node);
|
|
534
|
+
lanesByParticipant.set(node.containerId, lanes);
|
|
535
|
+
}
|
|
536
|
+
for (const ref of laneReferences(node)) if (!laneByNode.has(ref)) laneByNode.set(ref, node);
|
|
537
|
+
}
|
|
538
|
+
for (const lanes of lanesByParticipant.values()) lanes.sort((a, b) => a.y - b.y || a.id.localeCompare(b.id));
|
|
539
|
+
return {
|
|
540
|
+
nodesById,
|
|
541
|
+
lanesByParticipant,
|
|
542
|
+
laneByNode,
|
|
543
|
+
getParticipantLanes: (participantId) => lanesByParticipant.get(participantId) || [],
|
|
544
|
+
getLaneForNode: (nodeId) => laneByNode.get(nodeId) || null,
|
|
545
|
+
getLaneAt: (point, excludedId) => laneAt(model, point, excludedId),
|
|
546
|
+
getParticipantAt: (point) => eligibleParticipantAt(model, point),
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export function applyContainmentOperation(model, operation = {}) {
|
|
551
|
+
if (!model?.nodes || !operation.type) return { changed: false };
|
|
552
|
+
const node = model.nodes.find((candidate) => candidate.id === (operation.nodeId || operation.laneId));
|
|
553
|
+
let changed = false;
|
|
554
|
+
if (operation.type === 'reconcile') {
|
|
555
|
+
changed = reconcile(model, operation);
|
|
556
|
+
} else if (operation.type === 'attach-lane') {
|
|
557
|
+
const participant = model.nodes.find((candidate) => candidate.id === operation.participantId);
|
|
558
|
+
changed = attachLane(model, node, participant, operation.index);
|
|
559
|
+
if (changed) {
|
|
560
|
+
for (const candidate of model.nodes) if (isFlowNode(candidate)) assignNodeToLane(model, candidate.id);
|
|
561
|
+
reconcile(model);
|
|
562
|
+
}
|
|
563
|
+
} else if (operation.type === 'detach-lane') {
|
|
564
|
+
changed = detachLane(model, node, operation);
|
|
565
|
+
} else if (operation.type === 'reorder-lane') {
|
|
566
|
+
const participant = model.nodes.find((candidate) => candidate.id === node?.containerId);
|
|
567
|
+
changed = participant ? attachLane(model, node, participant, operation.index) : false;
|
|
568
|
+
} else if (operation.type === 'assign-node') {
|
|
569
|
+
changed = assignNodeToLane(model, operation.nodeId);
|
|
570
|
+
} else if (operation.type === 'assign-all') {
|
|
571
|
+
for (const candidate of model.nodes) if (isFlowNode(candidate)) changed = assignNodeToLane(model, candidate.id) || changed;
|
|
572
|
+
} else if (operation.type === 'move-container') {
|
|
573
|
+
changed = moveContainer(model, node, operation.dx || 0, operation.dy || 0);
|
|
574
|
+
} else if (operation.type === 'resize-container') {
|
|
575
|
+
changed = resizeNode(model, node, operation.bounds);
|
|
576
|
+
} else if (operation.type === 'resize-divider') {
|
|
577
|
+
changed = resizeDivider(model, node, operation.delta || 0);
|
|
578
|
+
} else if (operation.type === 'update-label-placement') {
|
|
579
|
+
changed = updateLabelPlacement(model, node, operation.previousPlacement);
|
|
580
|
+
} else if (operation.type === 'prepare-remove') {
|
|
581
|
+
if (kindOf(node) === 'participant') {
|
|
582
|
+
for (const lane of participantLanes(model, node.id)) delete lane.containerId;
|
|
583
|
+
changed = true;
|
|
584
|
+
} else if (kindOf(node) === 'lane') {
|
|
585
|
+
changed = detachLane(model, node, { preservePosition: true }) || changed;
|
|
586
|
+
}
|
|
587
|
+
changed = removeNodeFromLanes(model, operation.nodeId) || changed;
|
|
588
|
+
}
|
|
589
|
+
return { changed };
|
|
590
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const sequenceFlowsFor = (model, predicate) => (model?.edges || [])
|
|
2
|
+
.filter((edge) => (edge.type || 'sequenceFlow') === 'sequenceFlow')
|
|
3
|
+
.filter(predicate);
|
|
4
|
+
|
|
5
|
+
const preset = (id, label, direction, iconId) => Object.freeze({
|
|
6
|
+
id,
|
|
7
|
+
label,
|
|
8
|
+
iconId,
|
|
9
|
+
nodeType: 'parallelGateway',
|
|
10
|
+
preset: Object.freeze({
|
|
11
|
+
name: label,
|
|
12
|
+
properties: Object.freeze({ gatewayDirection: direction }),
|
|
13
|
+
}),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const PARALLEL_GATEWAY_PRESETS = Object.freeze({
|
|
17
|
+
diverging: preset('parallel-diverging', '并行分支', 'Diverging', 'ui.parallelDiverging'),
|
|
18
|
+
converging: preset('parallel-converging', '并行汇聚', 'Converging', 'ui.parallelConverging'),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function inferGatewayDirection(incoming, outgoing) {
|
|
22
|
+
if (incoming.length > 1 && outgoing.length > 1) return 'Mixed';
|
|
23
|
+
if (incoming.length > 1 && outgoing.length <= 1) return 'Converging';
|
|
24
|
+
if (outgoing.length > 1 && incoming.length <= 1) return 'Diverging';
|
|
25
|
+
return 'Unspecified';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function roleIssues(effective, incoming, outgoing) {
|
|
29
|
+
const issues = [];
|
|
30
|
+
if (effective === 'Converging') {
|
|
31
|
+
if (incoming.length < 2) issues.push('并行汇聚至少需要两条入口线路。');
|
|
32
|
+
if (outgoing.length !== 1) issues.push('并行汇聚通常应只有一条出口线路。');
|
|
33
|
+
} else if (effective === 'Diverging') {
|
|
34
|
+
if (incoming.length !== 1) issues.push('并行分支通常应只有一条入口线路。');
|
|
35
|
+
if (outgoing.length < 2) issues.push('并行分支至少需要两条出口线路。');
|
|
36
|
+
} else if (effective === 'Mixed') {
|
|
37
|
+
issues.push('并行网关同时存在多个入口和多个出口,建议拆分为汇聚与分支两个网关。');
|
|
38
|
+
} else {
|
|
39
|
+
issues.push('当前线路不足,暂时无法判断并行分支或并行汇聚。');
|
|
40
|
+
}
|
|
41
|
+
if (outgoing.some((edge) => String(edge.condition || '').trim())) issues.push('并行网关出口不应设置条件表达式。');
|
|
42
|
+
if (outgoing.some((edge) => edge.isDefault)) issues.push('并行网关不支持默认出口。');
|
|
43
|
+
return issues;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveGatewayRole(model, node) {
|
|
47
|
+
const incoming = sequenceFlowsFor(model, (edge) => edge.target === node?.id);
|
|
48
|
+
const outgoing = sequenceFlowsFor(model, (edge) => edge.source === node?.id);
|
|
49
|
+
const declared = node?.properties?.gatewayDirection || 'Unspecified';
|
|
50
|
+
const inferred = inferGatewayDirection(incoming, outgoing);
|
|
51
|
+
const effective = declared === 'Unspecified' ? inferred : declared;
|
|
52
|
+
const topologyComplete = effective === 'Diverging'
|
|
53
|
+
? incoming.length === 1 && outgoing.length >= 2
|
|
54
|
+
: effective === 'Converging'
|
|
55
|
+
? incoming.length >= 2 && outgoing.length === 1
|
|
56
|
+
: false;
|
|
57
|
+
const contradictoryTopology = (effective === 'Diverging' && incoming.length > 1)
|
|
58
|
+
|| (effective === 'Converging' && outgoing.length > 1);
|
|
59
|
+
const invalidOutgoing = outgoing.some((edge) => String(edge.condition || '').trim() || edge.isDefault);
|
|
60
|
+
const status = ['Unspecified', 'Mixed'].includes(effective)
|
|
61
|
+
? 'ambiguous'
|
|
62
|
+
: contradictoryTopology || invalidOutgoing
|
|
63
|
+
? 'conflict'
|
|
64
|
+
: topologyComplete ? 'valid' : 'incomplete';
|
|
65
|
+
const issues = roleIssues(effective, incoming, outgoing);
|
|
66
|
+
return { declared, inferred, effective, incoming, outgoing, topologyComplete, status, issues };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function validateParallelGateway(model, node) {
|
|
70
|
+
if (node?.type !== 'parallelGateway') return [];
|
|
71
|
+
return [...resolveGatewayRole(model, node).issues];
|
|
72
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { cloneModel } from './model.js';
|
|
2
|
+
|
|
3
|
+
export class HistoryStack {
|
|
4
|
+
constructor(limit = 80) {
|
|
5
|
+
this.limit = limit;
|
|
6
|
+
this.undoStack = [];
|
|
7
|
+
this.redoStack = [];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
capture(model) {
|
|
11
|
+
this.undoStack.push(cloneModel(model));
|
|
12
|
+
if (this.undoStack.length > this.limit) this.undoStack.shift();
|
|
13
|
+
this.redoStack.length = 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
undo(current) {
|
|
17
|
+
if (!this.undoStack.length) return null;
|
|
18
|
+
const previous = this.undoStack.pop();
|
|
19
|
+
this.redoStack.push(cloneModel(current));
|
|
20
|
+
return previous;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
redo(current) {
|
|
24
|
+
if (!this.redoStack.length) return null;
|
|
25
|
+
const next = this.redoStack.pop();
|
|
26
|
+
this.undoStack.push(cloneModel(current));
|
|
27
|
+
return next;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get canUndo() { return this.undoStack.length > 0; }
|
|
31
|
+
get canRedo() { return this.redoStack.length > 0; }
|
|
32
|
+
}
|