bpmn-elk-layout 1.2.0 → 1.3.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.
@@ -1251,8 +1251,6 @@ var init_bpmn_constants = __esm({
1251
1251
  "dataObject",
1252
1252
  "dataObjectReference",
1253
1253
  "dataStoreReference",
1254
- "dataInput",
1255
- "dataOutput",
1256
1254
  "textAnnotation"
1257
1255
  ]);
1258
1256
  }
@@ -2022,10 +2020,13 @@ var init_lane_arranger = __esm({
2022
2020
  console.log(`[BPMN] targetPos=${JSON.stringify(targetPos)}`);
2023
2021
  }
2024
2022
  if (sourcePos && targetPos) {
2025
- const startX = sourcePos.x + sourcePos.width;
2026
- const startY = sourcePos.y + sourcePos.height / 2;
2027
- const endX = targetPos.x;
2028
- const endY = targetPos.y + targetPos.height / 2;
2023
+ const { startX, startY, endX, endY } = this.calculateConnectionPoints(
2024
+ sourcePos,
2025
+ targetPos,
2026
+ sourceId,
2027
+ targetId,
2028
+ nodePositions
2029
+ );
2029
2030
  if (isDebugEnabled()) {
2030
2031
  console.log(`[BPMN] startX=${startX}, startY=${startY}, endX=${endX}, endY=${endY}`);
2031
2032
  }
@@ -2040,33 +2041,10 @@ var init_lane_arranger = __esm({
2040
2041
  targetId,
2041
2042
  nodePositions
2042
2043
  );
2043
- if (Math.abs(startY - endY) > 10 || obstaclesInPath.length > 0) {
2044
- if (obstaclesInPath.length > 0 && Math.abs(startY - endY) <= 10) {
2045
- const routePoints = this.routeAroundObstacles(
2046
- startX,
2047
- startY,
2048
- endX,
2049
- endY,
2050
- obstaclesInPath,
2051
- nodePositions
2052
- );
2053
- for (const pt of routePoints) {
2054
- waypoints.push(pt);
2055
- }
2056
- } else {
2057
- const midX = this.findClearMidX(
2058
- startX,
2059
- endX,
2060
- startY,
2061
- endY,
2062
- sourceId,
2063
- targetId,
2064
- nodePositions
2065
- );
2066
- if (midX !== null) {
2067
- waypoints.push({ x: midX, y: startY });
2068
- waypoints.push({ x: midX, y: endY });
2069
- } else {
2044
+ const isHorizontalFlow = this.isHorizontalConnection(startX, startY, endX, endY, sourcePos, targetPos);
2045
+ if (isHorizontalFlow && Math.abs(startY - endY) > 10 || !isHorizontalFlow && Math.abs(startX - endX) > 10 || obstaclesInPath.length > 0) {
2046
+ if (isHorizontalFlow) {
2047
+ if (obstaclesInPath.length > 0 && Math.abs(startY - endY) <= 10) {
2070
2048
  const routePoints = this.routeAroundObstacles(
2071
2049
  startX,
2072
2050
  startY,
@@ -2078,6 +2056,50 @@ var init_lane_arranger = __esm({
2078
2056
  for (const pt of routePoints) {
2079
2057
  waypoints.push(pt);
2080
2058
  }
2059
+ } else {
2060
+ const midX = this.findClearMidX(
2061
+ startX,
2062
+ endX,
2063
+ startY,
2064
+ endY,
2065
+ sourceId,
2066
+ targetId,
2067
+ nodePositions
2068
+ );
2069
+ if (midX !== null) {
2070
+ waypoints.push({ x: midX, y: startY });
2071
+ waypoints.push({ x: midX, y: endY });
2072
+ } else {
2073
+ const routePoints = this.routeAroundObstacles(
2074
+ startX,
2075
+ startY,
2076
+ endX,
2077
+ endY,
2078
+ obstaclesInPath,
2079
+ nodePositions
2080
+ );
2081
+ for (const pt of routePoints) {
2082
+ waypoints.push(pt);
2083
+ }
2084
+ }
2085
+ }
2086
+ } else {
2087
+ const midY = this.findClearMidY(
2088
+ startX,
2089
+ endX,
2090
+ startY,
2091
+ endY,
2092
+ sourceId,
2093
+ targetId,
2094
+ nodePositions
2095
+ );
2096
+ if (midY !== null) {
2097
+ waypoints.push({ x: startX, y: midY });
2098
+ waypoints.push({ x: endX, y: midY });
2099
+ } else {
2100
+ const simpleMidY = (startY + endY) / 2;
2101
+ waypoints.push({ x: startX, y: simpleMidY });
2102
+ waypoints.push({ x: endX, y: simpleMidY });
2081
2103
  }
2082
2104
  }
2083
2105
  }
@@ -2095,6 +2117,265 @@ var init_lane_arranger = __esm({
2095
2117
  }
2096
2118
  }
2097
2119
  }
2120
+ /**
2121
+ * Calculate optimal connection points based on relative node positions.
2122
+ * Chooses the best sides (left/right/top/bottom) to connect nodes to minimize edge crossings.
2123
+ *
2124
+ * For BPMN diagrams, we prefer horizontal connections (left-to-right flow) even for cross-lane edges,
2125
+ * because BPMN processes typically flow horizontally.
2126
+ */
2127
+ calculateConnectionPoints(sourcePos, targetPos, sourceId, targetId, nodePositions) {
2128
+ const sourceCenterX = sourcePos.x + sourcePos.width / 2;
2129
+ const sourceCenterY = sourcePos.y + sourcePos.height / 2;
2130
+ const targetCenterX = targetPos.x + targetPos.width / 2;
2131
+ const targetCenterY = targetPos.y + targetPos.height / 2;
2132
+ const dx = targetCenterX - sourceCenterX;
2133
+ const dy = targetCenterY - sourceCenterY;
2134
+ let startX, startY, endX, endY;
2135
+ const horizontalDistanceSmall = Math.abs(dx) < 30;
2136
+ const verticalDistanceSignificant = Math.abs(dy) > 50;
2137
+ const isVerticalPrimary = horizontalDistanceSmall && verticalDistanceSignificant;
2138
+ if (!isVerticalPrimary) {
2139
+ startY = sourceCenterY;
2140
+ endY = targetCenterY;
2141
+ if (dx >= 0) {
2142
+ startX = sourcePos.x + sourcePos.width;
2143
+ endX = targetPos.x;
2144
+ } else {
2145
+ startX = sourcePos.x;
2146
+ endX = targetPos.x + targetPos.width;
2147
+ }
2148
+ } else {
2149
+ startX = sourceCenterX;
2150
+ endX = targetCenterX;
2151
+ if (dy >= 0) {
2152
+ startY = sourcePos.y + sourcePos.height;
2153
+ endY = targetPos.y;
2154
+ } else {
2155
+ startY = sourcePos.y;
2156
+ endY = targetPos.y + targetPos.height;
2157
+ }
2158
+ }
2159
+ const directObstacles = this.getObstaclesInPath(
2160
+ startX,
2161
+ startY,
2162
+ endX,
2163
+ endY,
2164
+ sourceId,
2165
+ targetId,
2166
+ nodePositions
2167
+ );
2168
+ if (directObstacles.length > 0) {
2169
+ const alternatives = this.findAlternativeConnectionPoints(
2170
+ sourcePos,
2171
+ targetPos,
2172
+ dx,
2173
+ dy,
2174
+ !isVerticalPrimary,
2175
+ sourceId,
2176
+ targetId,
2177
+ nodePositions
2178
+ );
2179
+ if (alternatives) {
2180
+ return alternatives;
2181
+ }
2182
+ }
2183
+ return { startX, startY, endX, endY };
2184
+ }
2185
+ /**
2186
+ * Find alternative connection points that may have fewer obstacles.
2187
+ */
2188
+ findAlternativeConnectionPoints(sourcePos, targetPos, dx, dy, isHorizontalPrimary, sourceId, targetId, nodePositions) {
2189
+ const sourceCenterX = sourcePos.x + sourcePos.width / 2;
2190
+ const sourceCenterY = sourcePos.y + sourcePos.height / 2;
2191
+ const targetCenterX = targetPos.x + targetPos.width / 2;
2192
+ const targetCenterY = targetPos.y + targetPos.height / 2;
2193
+ const candidates = [];
2194
+ if (dx >= 0) {
2195
+ candidates.push({
2196
+ startX: sourcePos.x + sourcePos.width,
2197
+ startY: sourceCenterY,
2198
+ endX: targetPos.x,
2199
+ endY: targetCenterY,
2200
+ score: 0
2201
+ });
2202
+ } else {
2203
+ candidates.push({
2204
+ startX: sourcePos.x,
2205
+ startY: sourceCenterY,
2206
+ endX: targetPos.x + targetPos.width,
2207
+ endY: targetCenterY,
2208
+ score: 0
2209
+ });
2210
+ }
2211
+ if (dy >= 0) {
2212
+ candidates.push({
2213
+ startX: sourceCenterX,
2214
+ startY: sourcePos.y + sourcePos.height,
2215
+ endX: targetCenterX,
2216
+ endY: targetPos.y,
2217
+ score: 0
2218
+ });
2219
+ } else {
2220
+ candidates.push({
2221
+ startX: sourceCenterX,
2222
+ startY: sourcePos.y,
2223
+ endX: targetCenterX,
2224
+ endY: targetPos.y + targetPos.height,
2225
+ score: 0
2226
+ });
2227
+ }
2228
+ if (dx >= 0 && dy >= 0) {
2229
+ candidates.push({
2230
+ startX: sourcePos.x + sourcePos.width,
2231
+ startY: sourceCenterY,
2232
+ endX: targetCenterX,
2233
+ endY: targetPos.y,
2234
+ score: 0
2235
+ });
2236
+ } else if (dx >= 0 && dy < 0) {
2237
+ candidates.push({
2238
+ startX: sourcePos.x + sourcePos.width,
2239
+ startY: sourceCenterY,
2240
+ endX: targetCenterX,
2241
+ endY: targetPos.y + targetPos.height,
2242
+ score: 0
2243
+ });
2244
+ } else if (dx < 0 && dy >= 0) {
2245
+ candidates.push({
2246
+ startX: sourcePos.x,
2247
+ startY: sourceCenterY,
2248
+ endX: targetCenterX,
2249
+ endY: targetPos.y,
2250
+ score: 0
2251
+ });
2252
+ } else {
2253
+ candidates.push({
2254
+ startX: sourcePos.x,
2255
+ startY: sourceCenterY,
2256
+ endX: targetCenterX,
2257
+ endY: targetPos.y + targetPos.height,
2258
+ score: 0
2259
+ });
2260
+ }
2261
+ for (const candidate of candidates) {
2262
+ const obstacles = this.getObstaclesInPath(
2263
+ candidate.startX,
2264
+ candidate.startY,
2265
+ candidate.endX,
2266
+ candidate.endY,
2267
+ sourceId,
2268
+ targetId,
2269
+ nodePositions
2270
+ );
2271
+ candidate.score = obstacles.length;
2272
+ }
2273
+ candidates.sort((a, b) => a.score - b.score);
2274
+ const best = candidates[0];
2275
+ if (best && best.score === 0) {
2276
+ return { startX: best.startX, startY: best.startY, endX: best.endX, endY: best.endY };
2277
+ }
2278
+ return null;
2279
+ }
2280
+ /**
2281
+ * Determine if the connection is primarily horizontal or vertical.
2282
+ */
2283
+ isHorizontalConnection(startX, startY, endX, endY, sourcePos, targetPos) {
2284
+ const isStartOnHorizontalEdge = Math.abs(startX - sourcePos.x) < 1 || Math.abs(startX - (sourcePos.x + sourcePos.width)) < 1;
2285
+ return isStartOnHorizontalEdge;
2286
+ }
2287
+ /**
2288
+ * Find a clear Y position for horizontal edge segment that avoids obstacles.
2289
+ * Similar to findClearMidX but for vertical flow routing.
2290
+ */
2291
+ findClearMidY(startX, endX, startY, endY, sourceId, targetId, nodePositions) {
2292
+ const margin = 15;
2293
+ const minX = Math.min(startX, endX);
2294
+ const maxX = Math.max(startX, endX);
2295
+ const rangeMinY = Math.min(startY, endY);
2296
+ const rangeMaxY = Math.max(startY, endY);
2297
+ const flowNodePatterns = [
2298
+ /^task_/,
2299
+ /^gateway_/,
2300
+ /^start_/,
2301
+ /^end_/,
2302
+ /^subprocess_/,
2303
+ /^call_/,
2304
+ /^intermediate_/,
2305
+ /^event_/,
2306
+ /^catch_/
2307
+ ];
2308
+ const allObstacles = [];
2309
+ for (const [nodeId, pos] of nodePositions) {
2310
+ if (nodeId === sourceId || nodeId === targetId) continue;
2311
+ if (nodeId.startsWith("lane_")) continue;
2312
+ const isFlowNode = flowNodePatterns.some((pattern) => pattern.test(nodeId));
2313
+ if (!isFlowNode) continue;
2314
+ const nodeLeft = pos.x;
2315
+ const nodeRight = pos.x + pos.width;
2316
+ const nodeTop = pos.y;
2317
+ const nodeBottom = pos.y + pos.height;
2318
+ const yOverlap = nodeBottom > rangeMinY && nodeTop < rangeMaxY;
2319
+ const xOverlapHorizontal = nodeRight > minX && nodeLeft < maxX;
2320
+ const xContainsStartX = nodeLeft <= startX && nodeRight >= startX;
2321
+ const xContainsEndX = nodeLeft <= endX && nodeRight >= endX;
2322
+ if (yOverlap && (xOverlapHorizontal || xContainsStartX || xContainsEndX)) {
2323
+ allObstacles.push({
2324
+ x: nodeLeft,
2325
+ y: nodeTop,
2326
+ width: pos.width,
2327
+ height: pos.height,
2328
+ right: nodeRight,
2329
+ bottom: nodeBottom
2330
+ });
2331
+ }
2332
+ }
2333
+ if (allObstacles.length === 0) {
2334
+ return (startY + endY) / 2;
2335
+ }
2336
+ const isValidMidY = (midY) => {
2337
+ for (const obs of allObstacles) {
2338
+ const seg1MinY = Math.min(startY, midY);
2339
+ const seg1MaxY = Math.max(startY, midY);
2340
+ if (obs.x <= startX && obs.right >= startX && obs.bottom > seg1MinY && obs.y < seg1MaxY) {
2341
+ return false;
2342
+ }
2343
+ if (obs.y <= midY && obs.bottom >= midY && obs.right > minX && obs.x < maxX) {
2344
+ return false;
2345
+ }
2346
+ const seg2MinY = Math.min(midY, endY);
2347
+ const seg2MaxY = Math.max(midY, endY);
2348
+ if (obs.x <= endX && obs.right >= endX && obs.bottom > seg2MinY && obs.y < seg2MaxY) {
2349
+ return false;
2350
+ }
2351
+ }
2352
+ return true;
2353
+ };
2354
+ const candidates = [];
2355
+ candidates.push((startY + endY) / 2);
2356
+ candidates.push(startY + margin);
2357
+ candidates.push(endY - margin);
2358
+ for (const obs of allObstacles) {
2359
+ candidates.push(obs.y - margin);
2360
+ candidates.push(obs.bottom + margin);
2361
+ }
2362
+ const simpleMidY = (startY + endY) / 2;
2363
+ const validCandidates = candidates.filter((y) => y >= rangeMinY && y <= rangeMaxY).sort((a, b) => Math.abs(a - simpleMidY) - Math.abs(b - simpleMidY));
2364
+ for (const candidate of validCandidates) {
2365
+ if (isValidMidY(candidate)) {
2366
+ return candidate;
2367
+ }
2368
+ }
2369
+ const topMost = Math.min(...allObstacles.map((o) => o.y)) - margin;
2370
+ const bottomMost = Math.max(...allObstacles.map((o) => o.bottom)) + margin;
2371
+ if (topMost >= rangeMinY && isValidMidY(topMost)) {
2372
+ return topMost;
2373
+ }
2374
+ if (bottomMost <= rangeMaxY && isValidMidY(bottomMost)) {
2375
+ return bottomMost;
2376
+ }
2377
+ return null;
2378
+ }
2098
2379
  /**
2099
2380
  * Get obstacles in the direct path from source to target.
2100
2381
  * This handles the case where source and target are at similar Y positions
@@ -3980,6 +4261,23 @@ var init_elk_graph_preparer = __esm({
3980
4261
  "elk.layered.layering.layerConstraint": "LAST"
3981
4262
  };
3982
4263
  }
4264
+ const ioSpec = node.bpmn?.ioSpecification;
4265
+ let ioSpecExtraHeight = 0;
4266
+ if (ioSpec) {
4267
+ const dataInputs = ioSpec.dataInputs ?? [];
4268
+ const dataOutputs = ioSpec.dataOutputs ?? [];
4269
+ const maxCount = Math.max(dataInputs.length, dataOutputs.length);
4270
+ if (maxCount > 0) {
4271
+ const dataHeight = 50;
4272
+ const gapBelow = 20;
4273
+ const verticalSpacing = 24;
4274
+ const labelHeight = 14;
4275
+ ioSpecExtraHeight = gapBelow + maxCount * (dataHeight + verticalSpacing) + labelHeight;
4276
+ if (isDebugEnabled()) {
4277
+ console.log(`[BPMN] Adding extra height ${ioSpecExtraHeight} for node ${node.id} with ioSpecification`);
4278
+ }
4279
+ }
4280
+ }
3983
4281
  if (mainFlowNodes.has(node.id) && !boundaryEventTargetIds.has(node.id)) {
3984
4282
  layoutOptions = {
3985
4283
  ...layoutOptions,
@@ -4059,12 +4357,15 @@ var init_elk_graph_preparer = __esm({
4059
4357
  "elk.padding": "[top=12,left=12,bottom=12,right=12]"
4060
4358
  };
4061
4359
  }
4360
+ const originalHeight = node.height ?? 80;
4361
+ const elkHeight = originalHeight + ioSpecExtraHeight;
4362
+ const bpmnWithVisualHeight = ioSpecExtraHeight > 0 ? { ...node.bpmn, _visualHeight: originalHeight } : node.bpmn;
4062
4363
  const elkNode = {
4063
4364
  id: node.id,
4064
4365
  width: node.width,
4065
- height: node.height,
4366
+ height: elkHeight,
4066
4367
  layoutOptions,
4067
- bpmn: node.bpmn
4368
+ bpmn: bpmnWithVisualHeight
4068
4369
  };
4069
4370
  if (node.children && node.children.length > 0) {
4070
4371
  const childNodes = [];
@@ -4231,12 +4532,16 @@ var init_result_merger = __esm({
4231
4532
  * Merge a single node's layout results with original BPMN data
4232
4533
  */
4233
4534
  mergeNodeResults(original, layouted) {
4535
+ const layoutedBpmn = layouted.bpmn;
4536
+ const visualHeight = layoutedBpmn?._visualHeight;
4537
+ const mergedBpmn = visualHeight !== void 0 ? { ...original.bpmn, _visualHeight: visualHeight } : original.bpmn;
4234
4538
  const result = {
4235
4539
  ...original,
4236
4540
  x: layouted.x ?? 0,
4237
4541
  y: layouted.y ?? 0,
4238
4542
  width: layouted.width ?? original.width,
4239
- height: layouted.height ?? original.height
4543
+ height: layouted.height ?? original.height,
4544
+ bpmn: mergedBpmn
4240
4545
  };
4241
4546
  if (original.children && layouted.children) {
4242
4547
  const layoutedChildMap = new Map(layouted.children.map((c) => [c.id, c]));
@@ -4898,23 +5203,36 @@ var init_diagram_builder = __esm({
4898
5203
  const absoluteX = offsetX + node.x;
4899
5204
  const absoluteY = offsetY + node.y;
4900
5205
  const nodeWidth = node.width ?? 100;
4901
- const nodeHeight = node.height ?? 80;
5206
+ const bpmnAny = node.bpmn;
5207
+ const nodeHeight = bpmnAny?._visualHeight ?? node.height ?? 80;
4902
5208
  let effectiveHeight = nodeHeight;
4903
5209
  if (this.isEventType(node.bpmn?.type) && node.labels && node.labels.length > 0) {
4904
5210
  const labelHeight = node.labels[0]?.height ?? 14;
4905
5211
  effectiveHeight = nodeHeight + 4 + labelHeight;
4906
5212
  }
4907
- this.nodePositions.set(node.id, {
5213
+ const nodePosition = {
4908
5214
  x: absoluteX,
4909
5215
  y: absoluteY,
4910
5216
  width: nodeWidth,
4911
5217
  height: effectiveHeight
4912
- });
5218
+ };
5219
+ if (bpmnAny?._visualHeight !== void 0) {
5220
+ nodePosition.visualHeight = bpmnAny._visualHeight;
5221
+ }
5222
+ this.nodePositions.set(node.id, nodePosition);
4913
5223
  if (node.bpmn) {
4914
5224
  this.storeNodeBpmn(node.id, { type: node.bpmn.type });
4915
5225
  }
4916
5226
  this.nodeOffsets.set(node.id, { x: offsetX, y: offsetY });
4917
5227
  shapes.push(this.buildShape(node, offsetX, offsetY));
5228
+ const nodeType = node.bpmn?.type;
5229
+ const isTaskOrActivity = nodeType && (nodeType.includes("Task") || nodeType === "task" || nodeType === "callActivity" || nodeType === "subProcess" || nodeType === "transaction" || nodeType === "adHocSubProcess");
5230
+ if (isTaskOrActivity) {
5231
+ const ioSpec = node.bpmn?.ioSpecification;
5232
+ if (ioSpec) {
5233
+ this.buildIoSpecificationShapes(node, ioSpec, shapes, edges, absoluteX, absoluteY, nodeWidth, nodeHeight);
5234
+ }
5235
+ }
4918
5236
  }
4919
5237
  const isExpandedSubprocess = node.bpmn?.isExpanded === true && (node.bpmn?.type === "subProcess" || node.bpmn?.type === "transaction" || node.bpmn?.type === "adHocSubProcess" || node.bpmn?.type === "eventSubProcess" || node.bpmn?.triggeredByEvent === true);
4920
5238
  const isPoolOrLane = node.bpmn?.type === "participant" || node.bpmn?.type === "lane";
@@ -4998,6 +5316,131 @@ var init_diagram_builder = __esm({
4998
5316
  storeNodeBpmn(nodeId, bpmn) {
4999
5317
  this.nodeBpmn.set(nodeId, bpmn);
5000
5318
  }
5319
+ /**
5320
+ * Build shapes for ioSpecification dataInputs and dataOutputs
5321
+ * Positions: dataInputs below-left of the task (stacked vertically),
5322
+ * dataOutputs below-right of the task (stacked vertically)
5323
+ * Only the topmost item in each stack has a dashed association edge to the task
5324
+ */
5325
+ buildIoSpecificationShapes(node, ioSpec, shapes, edges, taskX, taskY, taskWidth, taskHeight) {
5326
+ const dataWidth = 36;
5327
+ const dataHeight = 50;
5328
+ const gapBelow = 20;
5329
+ const verticalSpacing = 24;
5330
+ const labelHeight = 14;
5331
+ const dataInputs = ioSpec.dataInputs ?? [];
5332
+ const inputStartX = taskX;
5333
+ dataInputs.forEach((dataInput, index) => {
5334
+ const inputId = dataInput.id ?? `${node.id}_input_${index}`;
5335
+ const inputX = inputStartX;
5336
+ const inputY = taskY + taskHeight + gapBelow + index * (dataHeight + verticalSpacing);
5337
+ this.nodePositions.set(inputId, {
5338
+ x: inputX,
5339
+ y: inputY,
5340
+ width: dataWidth,
5341
+ height: dataHeight
5342
+ });
5343
+ const shape = {
5344
+ id: `${inputId}_di`,
5345
+ bpmnElement: inputId,
5346
+ bounds: {
5347
+ x: inputX,
5348
+ y: inputY,
5349
+ width: dataWidth,
5350
+ height: dataHeight
5351
+ }
5352
+ };
5353
+ if (dataInput.name) {
5354
+ const labelWidth = Math.max(dataWidth, this.estimateTextWidth(dataInput.name));
5355
+ shape.label = {
5356
+ bounds: {
5357
+ x: inputX + (dataWidth - labelWidth) / 2,
5358
+ y: inputY + dataHeight + 4,
5359
+ width: labelWidth,
5360
+ height: labelHeight
5361
+ }
5362
+ };
5363
+ }
5364
+ shapes.push(shape);
5365
+ if (index === 0) {
5366
+ const assocId = `${inputId}_assoc`;
5367
+ const inputCenterX = inputX + dataWidth / 2;
5368
+ const inputTopY = inputY;
5369
+ const taskBottomY = taskY + taskHeight;
5370
+ edges.push({
5371
+ id: `${assocId}_di`,
5372
+ bpmnElement: assocId,
5373
+ waypoints: [
5374
+ { x: inputCenterX, y: inputTopY },
5375
+ { x: inputCenterX, y: taskBottomY }
5376
+ ]
5377
+ });
5378
+ }
5379
+ });
5380
+ const dataOutputs = ioSpec.dataOutputs ?? [];
5381
+ const outputStartX = taskX + taskWidth - dataWidth;
5382
+ dataOutputs.forEach((dataOutput, index) => {
5383
+ const outputId = dataOutput.id ?? `${node.id}_output_${index}`;
5384
+ const outputX = outputStartX;
5385
+ const outputY = taskY + taskHeight + gapBelow + index * (dataHeight + verticalSpacing);
5386
+ this.nodePositions.set(outputId, {
5387
+ x: outputX,
5388
+ y: outputY,
5389
+ width: dataWidth,
5390
+ height: dataHeight
5391
+ });
5392
+ const shape = {
5393
+ id: `${outputId}_di`,
5394
+ bpmnElement: outputId,
5395
+ bounds: {
5396
+ x: outputX,
5397
+ y: outputY,
5398
+ width: dataWidth,
5399
+ height: dataHeight
5400
+ }
5401
+ };
5402
+ if (dataOutput.name) {
5403
+ const labelWidth = Math.max(dataWidth, this.estimateTextWidth(dataOutput.name));
5404
+ shape.label = {
5405
+ bounds: {
5406
+ x: outputX + (dataWidth - labelWidth) / 2,
5407
+ y: outputY + dataHeight + 4,
5408
+ width: labelWidth,
5409
+ height: labelHeight
5410
+ }
5411
+ };
5412
+ }
5413
+ shapes.push(shape);
5414
+ if (index === 0) {
5415
+ const assocId = `${outputId}_assoc`;
5416
+ const outputCenterX = outputX + dataWidth / 2;
5417
+ const outputTopY = outputY;
5418
+ const taskBottomY = taskY + taskHeight;
5419
+ edges.push({
5420
+ id: `${assocId}_di`,
5421
+ bpmnElement: assocId,
5422
+ waypoints: [
5423
+ { x: outputCenterX, y: taskBottomY },
5424
+ { x: outputCenterX, y: outputTopY }
5425
+ ]
5426
+ });
5427
+ }
5428
+ });
5429
+ }
5430
+ /**
5431
+ * Estimate text width for label sizing (simplified)
5432
+ */
5433
+ estimateTextWidth(text) {
5434
+ let width = 0;
5435
+ for (const char of text) {
5436
+ if (char.charCodeAt(0) > 255) {
5437
+ width += 14;
5438
+ } else {
5439
+ width += 7;
5440
+ }
5441
+ }
5442
+ return Math.max(36, Math.min(width, 150));
5443
+ }
5001
5444
  /**
5002
5445
  * Find node BPMN metadata by id
5003
5446
  */
@@ -5029,6 +5472,8 @@ var init_diagram_builder = __esm({
5029
5472
  buildShape(node, offsetX = 0, offsetY = 0) {
5030
5473
  const absoluteX = offsetX + (node.x ?? 0);
5031
5474
  const absoluteY = offsetY + (node.y ?? 0);
5475
+ const bpmnAny = node.bpmn;
5476
+ const visualHeight = bpmnAny?._visualHeight ?? node.height ?? 80;
5032
5477
  const shape = {
5033
5478
  id: `${node.id}_di`,
5034
5479
  bpmnElement: node.id,
@@ -5036,7 +5481,7 @@ var init_diagram_builder = __esm({
5036
5481
  x: absoluteX,
5037
5482
  y: absoluteY,
5038
5483
  width: node.width ?? 100,
5039
- height: node.height ?? 80
5484
+ height: visualHeight
5040
5485
  }
5041
5486
  };
5042
5487
  if (node.bpmn?.isExpanded !== void 0) {
@@ -5046,7 +5491,7 @@ var init_diagram_builder = __esm({
5046
5491
  shape.isHorizontal = true;
5047
5492
  }
5048
5493
  const nodeWidth = node.width ?? 36;
5049
- const nodeHeight = node.height ?? 36;
5494
+ const nodeHeight = bpmnAny?._visualHeight ?? node.height ?? 36;
5050
5495
  const label = node.labels?.[0];
5051
5496
  const labelText = node.bpmn?.name ?? label?.text ?? "";
5052
5497
  if (this.isEventType(node.bpmn?.type) && labelText) {
@@ -5174,6 +5619,7 @@ var init_diagram_builder = __esm({
5174
5619
  }
5175
5620
  }
5176
5621
  }
5622
+ this.adjustEndpointsForVisualHeight(waypoints, sourceId, targetId);
5177
5623
  }
5178
5624
  this.ensureOrthogonalWaypoints(waypoints);
5179
5625
  this.ensurePerpendicularEndpoints(
@@ -5216,6 +5662,66 @@ var init_diagram_builder = __esm({
5216
5662
  }
5217
5663
  return edgeModel;
5218
5664
  }
5665
+ /**
5666
+ * Adjust edge endpoints for nodes with ioSpecification (visualHeight)
5667
+ *
5668
+ * When a node has ioSpecification, ELK uses an enlarged height for layout (to make space for data objects).
5669
+ * However, the edge endpoints should connect to the visual node border, not based on the layout height.
5670
+ *
5671
+ * This method adjusts endpoint Y coordinates and also adjusts adjacent waypoints if they were
5672
+ * on the same horizontal line, to maintain horizontal segments without introducing extra bends.
5673
+ */
5674
+ adjustEndpointsForVisualHeight(waypoints, sourceId, targetId) {
5675
+ if (waypoints.length < 2) return;
5676
+ const tolerance = 5;
5677
+ if (sourceId) {
5678
+ const sourcePos = this.nodePositions.get(sourceId);
5679
+ if (sourcePos?.visualHeight !== void 0) {
5680
+ const firstWp = waypoints[0];
5681
+ const secondWp = waypoints[1];
5682
+ if (firstWp && secondWp) {
5683
+ const nodeRight = sourcePos.x + sourcePos.width;
5684
+ const nodeLeft = sourcePos.x;
5685
+ const visualBottom = sourcePos.y + sourcePos.visualHeight;
5686
+ const visualCenterY = sourcePos.y + sourcePos.visualHeight / 2;
5687
+ if (Math.abs(firstWp.x - nodeRight) < tolerance || Math.abs(firstWp.x - nodeLeft) < tolerance) {
5688
+ const oldY = firstWp.y;
5689
+ const newY = visualCenterY;
5690
+ firstWp.y = newY;
5691
+ if (Math.abs(secondWp.y - oldY) < tolerance && waypoints.length > 2) {
5692
+ secondWp.y = newY;
5693
+ }
5694
+ } else if (firstWp.y > visualBottom) {
5695
+ firstWp.y = visualBottom;
5696
+ }
5697
+ }
5698
+ }
5699
+ }
5700
+ if (targetId) {
5701
+ const targetPos = this.nodePositions.get(targetId);
5702
+ if (targetPos?.visualHeight !== void 0) {
5703
+ const lastIdx = waypoints.length - 1;
5704
+ const lastWp = waypoints[lastIdx];
5705
+ const prevWp = waypoints[lastIdx - 1];
5706
+ if (lastWp && prevWp) {
5707
+ const nodeLeft = targetPos.x;
5708
+ const nodeRight = targetPos.x + targetPos.width;
5709
+ const visualBottom = targetPos.y + targetPos.visualHeight;
5710
+ const visualCenterY = targetPos.y + targetPos.visualHeight / 2;
5711
+ if (Math.abs(lastWp.x - nodeLeft) < tolerance || Math.abs(lastWp.x - nodeRight) < tolerance) {
5712
+ const oldY = lastWp.y;
5713
+ const newY = visualCenterY;
5714
+ lastWp.y = newY;
5715
+ if (Math.abs(prevWp.y - oldY) < tolerance && waypoints.length > 2) {
5716
+ prevWp.y = newY;
5717
+ }
5718
+ } else if (lastWp.y > visualBottom) {
5719
+ lastWp.y = visualBottom;
5720
+ }
5721
+ }
5722
+ }
5723
+ }
5724
+ }
5219
5725
  /**
5220
5726
  * Calculate smart label position on the edge
5221
5727
  * Strategy:
@@ -5696,7 +6202,7 @@ var init_model_builder = __esm({
5696
6202
  buildFlowElement(node) {
5697
6203
  const incoming = this.refResolver.getIncomingSequenceFlows(node.id);
5698
6204
  const outgoing = this.refResolver.getOutgoingSequenceFlows(node.id);
5699
- return {
6205
+ const flowElement = {
5700
6206
  type: node.bpmn?.type ?? "task",
5701
6207
  id: node.id,
5702
6208
  name: node.bpmn?.name,
@@ -5704,6 +6210,76 @@ var init_model_builder = __esm({
5704
6210
  outgoing,
5705
6211
  properties: this.extractProperties(node.bpmn ?? {})
5706
6212
  };
6213
+ const ioSpec = node.bpmn?.ioSpecification;
6214
+ if (ioSpec) {
6215
+ flowElement.ioSpecification = this.buildIoSpecification(ioSpec, node.id);
6216
+ const dataInputs = ioSpec.dataInputs ?? [];
6217
+ if (dataInputs.length > 0) {
6218
+ const firstInput = dataInputs[0];
6219
+ flowElement.dataInputAssociations = [{
6220
+ id: `${firstInput.id ?? `${node.id}_input_0`}_assoc`,
6221
+ sourceRef: firstInput.id ?? `${node.id}_input_0`,
6222
+ targetRef: node.id
6223
+ }];
6224
+ }
6225
+ const dataOutputs = ioSpec.dataOutputs ?? [];
6226
+ if (dataOutputs.length > 0) {
6227
+ const firstOutput = dataOutputs[0];
6228
+ flowElement.dataOutputAssociations = [{
6229
+ id: `${firstOutput.id ?? `${node.id}_output_0`}_assoc`,
6230
+ sourceRef: node.id,
6231
+ targetRef: firstOutput.id ?? `${node.id}_output_0`
6232
+ }];
6233
+ }
6234
+ }
6235
+ return flowElement;
6236
+ }
6237
+ /**
6238
+ * Build ioSpecification model from input
6239
+ */
6240
+ buildIoSpecification(ioSpec, taskId) {
6241
+ const dataInputs = (ioSpec.dataInputs ?? []).map((di, index) => ({
6242
+ id: di.id ?? `${taskId}_input_${index}`,
6243
+ name: di.name,
6244
+ itemSubjectRef: di.itemSubjectRef,
6245
+ isCollection: di.isCollection
6246
+ }));
6247
+ const dataOutputs = (ioSpec.dataOutputs ?? []).map((dout, index) => ({
6248
+ id: dout.id ?? `${taskId}_output_${index}`,
6249
+ name: dout.name,
6250
+ itemSubjectRef: dout.itemSubjectRef,
6251
+ isCollection: dout.isCollection
6252
+ }));
6253
+ const inputSets = (ioSpec.inputSets ?? []).map((is, index) => ({
6254
+ id: is.id ?? `${taskId}_inputSet_${index}`,
6255
+ name: is.name,
6256
+ dataInputRefs: is.dataInputRefs ?? []
6257
+ }));
6258
+ if (inputSets.length === 0 && dataInputs.length > 0) {
6259
+ inputSets.push({
6260
+ id: `${taskId}_inputSet_0`,
6261
+ name: void 0,
6262
+ dataInputRefs: dataInputs.map((di) => di.id)
6263
+ });
6264
+ }
6265
+ const outputSets = (ioSpec.outputSets ?? []).map((os, index) => ({
6266
+ id: os.id ?? `${taskId}_outputSet_${index}`,
6267
+ name: os.name,
6268
+ dataOutputRefs: os.dataOutputRefs ?? []
6269
+ }));
6270
+ if (outputSets.length === 0 && dataOutputs.length > 0) {
6271
+ outputSets.push({
6272
+ id: `${taskId}_outputSet_0`,
6273
+ name: void 0,
6274
+ dataOutputRefs: dataOutputs.map((dout) => dout.id)
6275
+ });
6276
+ }
6277
+ return {
6278
+ dataInputs,
6279
+ dataOutputs,
6280
+ inputSets,
6281
+ outputSets
6282
+ };
5707
6283
  }
5708
6284
  /**
5709
6285
  * Build a boundary event model
@@ -6037,6 +6613,9 @@ var init_bpmn_xml_generator = __esm({
6037
6613
  }
6038
6614
  if (element.type.includes("Task") || element.type === "task") {
6039
6615
  this.applyTaskProperties(bpmnElement, props);
6616
+ if (element.ioSpecification) {
6617
+ bpmnElement.ioSpecification = this.buildIoSpecification(element.ioSpecification);
6618
+ }
6040
6619
  }
6041
6620
  if (element.type.includes("Gateway")) {
6042
6621
  this.applyGatewayProperties(bpmnElement, props);
@@ -6229,6 +6808,55 @@ var init_bpmn_xml_generator = __esm({
6229
6808
  });
6230
6809
  }
6231
6810
  }
6811
+ /**
6812
+ * Build ioSpecification element
6813
+ */
6814
+ buildIoSpecification(ioSpec) {
6815
+ const ioSpecElement = this.moddle.create("bpmn:InputOutputSpecification", {});
6816
+ if (ioSpec.dataInputs.length > 0) {
6817
+ ioSpecElement.dataInputs = ioSpec.dataInputs.map((di) => {
6818
+ return this.moddle.create("bpmn:DataInput", {
6819
+ id: di.id,
6820
+ name: di.name,
6821
+ isCollection: di.isCollection
6822
+ });
6823
+ });
6824
+ }
6825
+ if (ioSpec.dataOutputs.length > 0) {
6826
+ ioSpecElement.dataOutputs = ioSpec.dataOutputs.map((dout) => {
6827
+ return this.moddle.create("bpmn:DataOutput", {
6828
+ id: dout.id,
6829
+ name: dout.name,
6830
+ isCollection: dout.isCollection
6831
+ });
6832
+ });
6833
+ }
6834
+ if (ioSpec.inputSets.length > 0) {
6835
+ ioSpecElement.inputSets = ioSpec.inputSets.map((is) => {
6836
+ const inputSet = this.moddle.create("bpmn:InputSet", {
6837
+ id: is.id,
6838
+ name: is.name
6839
+ });
6840
+ if (is.dataInputRefs.length > 0) {
6841
+ inputSet.dataInputRefs = is.dataInputRefs.map((ref) => ({ id: ref }));
6842
+ }
6843
+ return inputSet;
6844
+ });
6845
+ }
6846
+ if (ioSpec.outputSets.length > 0) {
6847
+ ioSpecElement.outputSets = ioSpec.outputSets.map((os) => {
6848
+ const outputSet = this.moddle.create("bpmn:OutputSet", {
6849
+ id: os.id,
6850
+ name: os.name
6851
+ });
6852
+ if (os.dataOutputRefs.length > 0) {
6853
+ outputSet.dataOutputRefs = os.dataOutputRefs.map((ref) => ({ id: ref }));
6854
+ }
6855
+ return outputSet;
6856
+ });
6857
+ }
6858
+ return ioSpecElement;
6859
+ }
6232
6860
  /**
6233
6861
  * Apply data associations (BPMN 2.0 spec: dataInputAssociation/dataOutputAssociation are child elements of Activity)
6234
6862
  */