@bpmnkit/ascii 0.0.12 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/edges.d.ts CHANGED
@@ -1,4 +1,22 @@
1
1
  import type { AsciiGrid } from "./grid.js";
2
+ /**
3
+ * Draw an edge using explicit source/target port sides.
4
+ *
5
+ * srcPort — which side the edge exits the source element:
6
+ * "right" → standard horizontal exit (srcCol = first col after right border, srcRow = midRow)
7
+ * "top" → exits the top of source (srcCol = midCol, srcRow = midRow)
8
+ * "bottom" → exits the bottom of source (srcCol = midCol, srcRow = midRow)
9
+ *
10
+ * dstPort — which side the edge enters the target element:
11
+ * "left" → standard horizontal entry (dstCol = left border col, dstRow = midRow)
12
+ * "top" → enters the top of target (dstCol = midCol, dstRow = midRow)
13
+ * "bottom" → enters the bottom of target (dstCol = midCol, dstRow = midRow)
14
+ *
15
+ * For top/bottom ports the element's middle row (midRow) is passed as srcRow/dstRow;
16
+ * this function offsets by ±2 to reach the actual visible exit/entry row (one row
17
+ * outside the 3-row element box).
18
+ */
19
+ export declare function drawPortedEdge(grid: AsciiGrid, srcCol: number, srcRow: number, srcPort: "right" | "top" | "bottom", dstCol: number, dstRow: number, dstPort: "left" | "top" | "bottom", label?: string): void;
2
20
  /**
3
21
  * Draw an orthogonal sequence-flow edge from a source exit point to a target
4
22
  * entry point, with an optional short label floated above the edge mid-point.
package/dist/edges.js CHANGED
@@ -1,4 +1,131 @@
1
1
  import { truncate } from "./util.js";
2
+ /**
3
+ * Draw an edge using explicit source/target port sides.
4
+ *
5
+ * srcPort — which side the edge exits the source element:
6
+ * "right" → standard horizontal exit (srcCol = first col after right border, srcRow = midRow)
7
+ * "top" → exits the top of source (srcCol = midCol, srcRow = midRow)
8
+ * "bottom" → exits the bottom of source (srcCol = midCol, srcRow = midRow)
9
+ *
10
+ * dstPort — which side the edge enters the target element:
11
+ * "left" → standard horizontal entry (dstCol = left border col, dstRow = midRow)
12
+ * "top" → enters the top of target (dstCol = midCol, dstRow = midRow)
13
+ * "bottom" → enters the bottom of target (dstCol = midCol, dstRow = midRow)
14
+ *
15
+ * For top/bottom ports the element's middle row (midRow) is passed as srcRow/dstRow;
16
+ * this function offsets by ±2 to reach the actual visible exit/entry row (one row
17
+ * outside the 3-row element box).
18
+ */
19
+ export function drawPortedEdge(grid, srcCol, srcRow, srcPort, dstCol, dstRow, dstPort, label) {
20
+ if (srcPort === "right" && dstPort === "left") {
21
+ drawEdge(grid, srcCol, srcRow, dstCol, dstRow, label);
22
+ return;
23
+ }
24
+ // top exit → left entry
25
+ if (srcPort === "top" && dstPort === "left") {
26
+ if (srcRow > dstRow) {
27
+ // Target is ABOVE: go up from gateway top to target row, corner, then right
28
+ grid.setLine(srcCol, dstRow, "┌");
29
+ for (let r = dstRow + 1; r < srcRow - 1; r++)
30
+ grid.setLine(srcCol, r, "│");
31
+ drawHorizontalSegment(grid, srcCol + 1, dstCol - 2, dstRow);
32
+ grid.set(dstCol - 1, dstRow, "►");
33
+ if (label) {
34
+ const mid = Math.floor((srcCol + dstCol) / 2) - Math.floor(label.length / 2);
35
+ grid.write(mid, dstRow - 1, truncate(label, dstCol - srcCol));
36
+ }
37
+ }
38
+ else {
39
+ // Target is at same row or below: route ABOVE the diagram via routeRow=1,
40
+ // then descend to target row. Path: up↑ at srcCol → right→ at routeRow → down↓ at dstCol-1 → ►
41
+ const routeRow = 1;
42
+ grid.setLine(srcCol, routeRow, "└"); // ↑ meets →
43
+ for (let r = routeRow + 1; r < srcRow - 1; r++)
44
+ grid.setLine(srcCol, r, "│");
45
+ drawHorizontalSegment(grid, srcCol + 1, dstCol - 2, routeRow);
46
+ grid.setLine(dstCol - 1, routeRow, "┐"); // ← meets ↓
47
+ for (let r = routeRow + 1; r < dstRow; r++)
48
+ grid.setLine(dstCol - 1, r, "│");
49
+ grid.set(dstCol - 1, dstRow, "►");
50
+ if (label) {
51
+ const mid = Math.floor((srcCol + dstCol) / 2) - Math.floor(label.length / 2);
52
+ grid.write(mid, routeRow - 1, truncate(label, dstCol - srcCol));
53
+ }
54
+ }
55
+ return;
56
+ }
57
+ // bottom exit → left entry
58
+ if (srcPort === "bottom" && dstPort === "left") {
59
+ if (srcRow < dstRow) {
60
+ // Target is BELOW: go down from gateway bottom to target row, corner, then right
61
+ grid.setLine(srcCol, dstRow, "└");
62
+ for (let r = srcRow + 2; r < dstRow; r++)
63
+ grid.setLine(srcCol, r, "│");
64
+ drawHorizontalSegment(grid, srcCol + 1, dstCol - 2, dstRow);
65
+ grid.set(dstCol - 1, dstRow, "►");
66
+ if (label) {
67
+ const mid = Math.floor((srcCol + dstCol) / 2) - Math.floor(label.length / 2);
68
+ grid.write(mid, dstRow - 1, truncate(label, dstCol - srcCol));
69
+ }
70
+ }
71
+ else {
72
+ // Target is at same row or above: route BELOW the diagram, then ascend to target row.
73
+ // Path: down↓ at srcCol → right→ at routeRow → up↑ at dstCol-1 → ►
74
+ const routeRow = grid.rows - 2;
75
+ grid.setLine(srcCol, routeRow, "┌"); // ↓ meets →
76
+ for (let r = srcRow + 2; r < routeRow; r++)
77
+ grid.setLine(srcCol, r, "│");
78
+ drawHorizontalSegment(grid, srcCol + 1, dstCol - 2, routeRow);
79
+ grid.setLine(dstCol - 1, routeRow, "┘"); // ← meets ↑
80
+ for (let r = dstRow + 1; r < routeRow; r++)
81
+ grid.setLine(dstCol - 1, r, "│");
82
+ grid.set(dstCol - 1, dstRow, "►");
83
+ if (label) {
84
+ const mid = Math.floor((srcCol + dstCol) / 2) - Math.floor(label.length / 2);
85
+ grid.write(mid, routeRow + 1, truncate(label, dstCol - srcCol));
86
+ }
87
+ }
88
+ return;
89
+ }
90
+ // right exit → top entry: horizontal right to target center-col, then vertical DOWN to target top
91
+ if (srcPort === "right" && dstPort === "top") {
92
+ if (srcRow >= dstRow) {
93
+ drawEdge(grid, srcCol, srcRow, dstCol, dstRow, label);
94
+ return;
95
+ }
96
+ // Corner at (dstCol, srcRow): connecting left (← from src) and down (↓ to dst top)
97
+ drawHorizontalSegment(grid, srcCol, dstCol - 1, srcRow);
98
+ grid.setLine(dstCol, srcRow, "┐");
99
+ for (let r = srcRow + 1; r < dstRow - 2; r++)
100
+ grid.setLine(dstCol, r, "│");
101
+ grid.set(dstCol, dstRow - 2, "▼");
102
+ if (label) {
103
+ const mid = Math.floor((srcCol + dstCol) / 2) - Math.floor(label.length / 2);
104
+ grid.write(mid, srcRow - 1, truncate(label, dstCol - srcCol));
105
+ }
106
+ return;
107
+ }
108
+ // right exit → bottom entry: horizontal right to target center-col, then vertical UP to target bottom
109
+ if (srcPort === "right" && dstPort === "bottom") {
110
+ if (srcRow <= dstRow) {
111
+ drawEdge(grid, srcCol, srcRow, dstCol, dstRow, label);
112
+ return;
113
+ }
114
+ // Corner at (dstCol, srcRow): connecting left (← from src) and up (↑ to dst bottom)
115
+ drawHorizontalSegment(grid, srcCol, dstCol - 1, srcRow);
116
+ grid.setLine(dstCol, srcRow, "┘");
117
+ for (let r = dstRow + 3; r < srcRow; r++)
118
+ grid.setLine(dstCol, r, "│");
119
+ grid.set(dstCol, dstRow + 2, "▲");
120
+ if (label) {
121
+ const mid = Math.floor((srcCol + dstCol) / 2) - Math.floor(label.length / 2);
122
+ grid.write(mid, srcRow - 1, truncate(label, dstCol - srcCol));
123
+ }
124
+ return;
125
+ }
126
+ // Fallback for any other combination
127
+ drawEdge(grid, srcCol, srcRow, dstCol, dstRow, label);
128
+ }
2
129
  /**
3
130
  * Draw an orthogonal sequence-flow edge from a source exit point to a target
4
131
  * entry point, with an optional short label floated above the edge mid-point.
package/dist/grid.js CHANGED
@@ -75,9 +75,9 @@ export function mergeBoxChars(existing, next) {
75
75
  if (existing === " " || existing === next)
76
76
  return next;
77
77
  // Arrows always win
78
- if (next === "►" || next === "▼")
78
+ if (next === "►" || next === "▼" || next === "▲")
79
79
  return next;
80
- if (existing === "►" || existing === "▼")
80
+ if (existing === "►" || existing === "▼" || existing === "▲")
81
81
  return existing;
82
82
  const eDirs = CHAR_DIRS[existing];
83
83
  const nDirs = CHAR_DIRS[next];
package/dist/render.js CHANGED
@@ -1,7 +1,30 @@
1
- import { Bpmn, layoutFlowNodes } from "@bpmnkit/core";
2
- import { drawEdge } from "./edges.js";
1
+ import { Bpmn, GRID_CELL_HEIGHT, layoutFlowNodes } from "@bpmnkit/core";
2
+ import { drawPortedEdge } from "./edges.js";
3
3
  import { AsciiGrid } from "./grid.js";
4
- import { CELL_H, CELL_W, drawElement, entryCol, exitCol, midRow } from "./shapes.js";
4
+ import { CELL_H, CELL_W, drawElement, entryCol, exitCol, midCol } from "./shapes.js";
5
+ /**
6
+ * Convert a node's pixel bounds to its ASCII mid-row (the middle of its 3-row box).
7
+ *
8
+ * The layout engine centers each element vertically within a GRID_CELL_HEIGHT-pixel
9
+ * tall cell. By recovering the cell's y-offset we get a stable ascii row index
10
+ * regardless of element type, and the centering applied by `centerLayersVertically`
11
+ * is preserved (e.g. a join node after 3 parallel branches appears in the middle).
12
+ */
13
+ function nodeMidRow(node) {
14
+ // Recover the cell's top y (element is centred inside its cell)
15
+ const cellY = node.bounds.y + node.bounds.height / 2 - GRID_CELL_HEIGHT / 2;
16
+ // Scale from pixel rows to ASCII rows (CELL_H ascii rows per GRID_CELL_HEIGHT pixels)
17
+ const asciiCellRow = Math.round((cellY / GRID_CELL_HEIGHT) * CELL_H);
18
+ // midRow = cell top + vertical padding + 1 (same formula as the old elemRow+1)
19
+ return asciiCellRow + Math.floor((CELL_H - 3) / 2) + 1;
20
+ }
21
+ const GATEWAY_TYPES = new Set([
22
+ "exclusiveGateway",
23
+ "parallelGateway",
24
+ "inclusiveGateway",
25
+ "eventBasedGateway",
26
+ "complexGateway",
27
+ ]);
5
28
  /**
6
29
  * Render a BPMN XML string as a Unicode box-drawing ASCII diagram.
7
30
  *
@@ -15,33 +38,36 @@ export function renderBpmnAscii(xml, options) {
15
38
  if (!process)
16
39
  return "(empty)";
17
40
  // Use layoutFlowNodes directly to skip the overlap assertion — the ASCII renderer
18
- // uses layer/position indices (not pixel coords) so pixel-level overlaps are harmless.
41
+ // re-scales pixel coords to character rows so pixel-level overlaps are harmless.
19
42
  const layout = layoutFlowNodes(process.flowElements, process.sequenceFlows);
20
43
  const { nodes, edges } = layout;
21
44
  if (nodes.length === 0)
22
45
  return "(empty)";
23
- // Compute grid dimensions from the layout's layer/position extents
46
+ // Compute grid dimensions from the layout's layer extents and pixel bounds
24
47
  const maxLayer = Math.max(...nodes.map((n) => n.layer));
25
- const maxPos = Math.max(...nodes.map((n) => n.position));
26
- // +2 padding on each axis, +1 because layers/positions are 0-indexed
48
+ const maxMidRow = Math.max(...nodes.map(nodeMidRow));
27
49
  const gridCols = (maxLayer + 1) * CELL_W + 4;
28
- const gridRows = (maxPos + 1) * CELL_H + 4;
50
+ // Element bottom is at maxMidRow+1; leave CELL_H rows of padding below.
51
+ const gridRows = maxMidRow + CELL_H + 1;
29
52
  const grid = new AsciiGrid(gridCols, gridRows);
30
53
  // Build an id → node map for O(1) edge-endpoint look-up
31
54
  const nodeById = new Map();
32
55
  for (const node of nodes)
33
56
  nodeById.set(node.id, node);
57
+ // Pre-compute port assignments for all edges
58
+ const ports = computeEdgePorts(edges, nodeById);
34
59
  // Draw edges first so that shapes render on top of any edge overlap
35
60
  for (const edge of edges) {
36
61
  const src = nodeById.get(edge.sourceRef);
37
62
  const dst = nodeById.get(edge.targetRef);
38
63
  if (!src || !dst)
39
64
  continue;
40
- drawEdge(grid, exitCol(src.type, src.layer), midRow(src.position), entryCol(dst.type, dst.layer), midRow(dst.position));
65
+ const { srcPort, dstPort } = ports.get(edge.id) ?? { srcPort: "right", dstPort: "left" };
66
+ drawPortedEdge(grid, srcPort === "right" ? exitCol(src.type, src.layer) : midCol(src.type, src.layer), nodeMidRow(src), srcPort, dstPort === "left" ? entryCol(dst.type, dst.layer) : midCol(dst.type, dst.layer), nodeMidRow(dst), dstPort, edge.label);
41
67
  }
42
68
  // Draw element boxes on top
43
69
  for (const node of nodes) {
44
- drawElement(grid, node.type, node.layer, node.position, node.label);
70
+ drawElement(grid, node.type, node.layer, nodeMidRow(node) - 1, node.label);
45
71
  }
46
72
  const diagram = grid.toString();
47
73
  // Optional title header
@@ -59,4 +85,90 @@ function resolveTitle(options, processName) {
59
85
  return options.title;
60
86
  return processName ?? undefined;
61
87
  }
88
+ /**
89
+ * Assign source and target ports for every edge.
90
+ *
91
+ * Source ports for gateway outgoing edges:
92
+ * N=1 → right
93
+ * N=2 → top, bottom
94
+ * N=3 → top, right (middle by target row), bottom
95
+ * N=4 → top, top, bottom, bottom
96
+ * N=5 → top, top, right, bottom, bottom …and so on.
97
+ *
98
+ * Target ports are derived from the source port and relative positions:
99
+ * srcPort=top/bottom → dstPort=left (edge routes vertical then horizontal)
100
+ * srcPort=right, dst is gateway:
101
+ * source above dst → dstPort=top
102
+ * source below dst → dstPort=bottom
103
+ * same row → dstPort=left
104
+ * all other targets → dstPort=left
105
+ */
106
+ function computeEdgePorts(edges, nodeById) {
107
+ // ── Step 1: assign source ports ──────────────────────────────────────────
108
+ const srcPorts = new Map();
109
+ // Collect forward gateway edges grouped by source
110
+ const bySource = new Map();
111
+ for (const edge of edges) {
112
+ const src = nodeById.get(edge.sourceRef);
113
+ const dst = nodeById.get(edge.targetRef);
114
+ // Back-edges (dst layer ≤ src layer) and non-gateway sources always exit right
115
+ if (!src || !dst || !GATEWAY_TYPES.has(src.type) || dst.layer <= src.layer) {
116
+ srcPorts.set(edge.id, "right");
117
+ continue;
118
+ }
119
+ const arr = bySource.get(edge.sourceRef) ?? [];
120
+ arr.push(edge);
121
+ bySource.set(edge.sourceRef, arr);
122
+ }
123
+ for (const [, outgoing] of bySource) {
124
+ const n = outgoing.length;
125
+ if (n === 1) {
126
+ // biome-ignore lint/style/noNonNullAssertion: length=1 guarantees element
127
+ srcPorts.set(outgoing[0].id, "right");
128
+ continue;
129
+ }
130
+ // Sort outgoing edges by their target's row position (ascending = topmost first)
131
+ const sorted = [...outgoing].sort((a, b) => {
132
+ const pa = nodeById.get(a.targetRef)?.position ?? 0;
133
+ const pb = nodeById.get(b.targetRef)?.position ?? 0;
134
+ return pa - pb;
135
+ });
136
+ for (let i = 0; i < sorted.length; i++) {
137
+ // biome-ignore lint/style/noNonNullAssertion: loop bounds guarantee element
138
+ const edge = sorted[i];
139
+ let port;
140
+ if (n % 2 === 1) {
141
+ // Odd: middle index → right, above → top, below → bottom
142
+ const mid = Math.floor(n / 2);
143
+ port = i < mid ? "top" : i === mid ? "right" : "bottom";
144
+ }
145
+ else {
146
+ // Even: upper half → top, lower half → bottom (no right port)
147
+ port = i < n / 2 ? "top" : "bottom";
148
+ }
149
+ srcPorts.set(edge.id, port);
150
+ }
151
+ }
152
+ // ── Step 2: assign target ports ──────────────────────────────────────────
153
+ const result = new Map();
154
+ for (const edge of edges) {
155
+ const srcPort = srcPorts.get(edge.id) ?? "right";
156
+ let dstPort = "left";
157
+ // Top/bottom source ports route vertical-then-horizontal → always left entry.
158
+ // Right source port into a gateway: entry side depends on relative row.
159
+ if (srcPort === "right") {
160
+ const src = nodeById.get(edge.sourceRef);
161
+ const dst = nodeById.get(edge.targetRef);
162
+ if (src && dst && GATEWAY_TYPES.has(dst.type)) {
163
+ if (src.position < dst.position)
164
+ dstPort = "top";
165
+ else if (src.position > dst.position)
166
+ dstPort = "bottom";
167
+ // else same row → left (default)
168
+ }
169
+ }
170
+ result.set(edge.id, { srcPort, dstPort });
171
+ }
172
+ return result;
173
+ }
62
174
  //# sourceMappingURL=render.js.map
package/dist/shapes.d.ts CHANGED
@@ -20,6 +20,8 @@ export declare const CELL_H = 8;
20
20
  export declare function elemCol(type: string, layer: number): number;
21
21
  /** Top-left row of a task or event element within its cell (3 rows tall). */
22
22
  export declare function elemRow(position: number): number;
23
+ /** Column of the horizontal center of an element (for top/bottom connections). */
24
+ export declare function midCol(type: string, layer: number): number;
23
25
  /**
24
26
  * Column of the right exit connection point (first column AFTER the element's
25
27
  * right border — where an outgoing edge begins).
@@ -32,6 +34,10 @@ export declare function exitCol(type: string, layer: number): number;
32
34
  export declare function entryCol(type: string, layer: number): number;
33
35
  /** Row of the horizontal mid-point (used as the connection row for edges). */
34
36
  export declare function midRow(position: number): number;
35
- /** Draw any BPMN element onto the grid at its logical (layer, position). */
36
- export declare function drawElement(grid: AsciiGrid, type: string, layer: number, position: number, label: string | undefined): void;
37
+ /**
38
+ * Draw any BPMN element onto the grid.
39
+ *
40
+ * @param row - The top row of the 3-row element box (pre-computed by the caller).
41
+ */
42
+ export declare function drawElement(grid: AsciiGrid, type: string, layer: number, row: number, label: string | undefined): void;
37
43
  //# sourceMappingURL=shapes.d.ts.map
package/dist/shapes.js CHANGED
@@ -118,6 +118,10 @@ export function elemRow(position) {
118
118
  // Centre the 3-row element vertically within the cell
119
119
  return position * CELL_H + Math.floor((CELL_H - 3) / 2);
120
120
  }
121
+ /** Column of the horizontal center of an element (for top/bottom connections). */
122
+ export function midCol(type, layer) {
123
+ return elemCol(type, layer) + Math.floor(elemW(type) / 2);
124
+ }
121
125
  /**
122
126
  * Column of the right exit connection point (first column AFTER the element's
123
127
  * right border — where an outgoing edge begins).
@@ -137,18 +141,22 @@ export function midRow(position) {
137
141
  return elemRow(position) + 1; // middle of the 3-row element
138
142
  }
139
143
  // ── Drawing ─────────────────────────────────────────────────────────────────
140
- /** Draw any BPMN element onto the grid at its logical (layer, position). */
141
- export function drawElement(grid, type, layer, position, label) {
144
+ /**
145
+ * Draw any BPMN element onto the grid.
146
+ *
147
+ * @param row - The top row of the 3-row element box (pre-computed by the caller).
148
+ */
149
+ export function drawElement(grid, type, layer, row, label) {
142
150
  const col = elemCol(type, layer);
143
151
  const name = label ?? "";
144
152
  if (isTaskLike(type)) {
145
- drawTaskBox(grid, col, elemRow(position), name, type);
153
+ drawTaskBox(grid, col, row, name, type);
146
154
  }
147
155
  else if (isGateway(type)) {
148
- drawGatewayBox(grid, col, elemRow(position), name, elementMarker(type));
156
+ drawGatewayBox(grid, col, row, name, elementMarker(type));
149
157
  }
150
158
  else {
151
- drawCompactBox(grid, col, elemRow(position), name, elementMarker(type));
159
+ drawCompactBox(grid, col, row, name, elementMarker(type));
152
160
  }
153
161
  }
154
162
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bpmnkit/ascii",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -17,7 +17,7 @@
17
17
  "dist/**/*.d.ts"
18
18
  ],
19
19
  "dependencies": {
20
- "@bpmnkit/core": "0.0.12"
20
+ "@bpmnkit/core": "0.0.13"
21
21
  },
22
22
  "description": "Render BPMN diagrams as Unicode box-drawing ASCII art — perfect for terminals and docs",
23
23
  "keywords": [