@adecore/diagram 0.39.0 → 0.40.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.
package/dist/layout.js CHANGED
@@ -135,11 +135,14 @@ export function layersOf(nodes, edges) {
135
135
  return layers;
136
136
  }
137
137
  /* The layout works in the frame of a diagram that runs to the right; one that runs down is that frame mirrored across the diagonal. */
138
- function flip(box) {
139
- return { ...box, x: box.y, y: box.x, w: box.h, h: box.w };
138
+ function flip(box, down) {
139
+ return down ? { ...box, x: box.y, y: box.x, w: box.h, h: box.w } : box;
140
140
  }
141
- function flipPoint(point) {
142
- return { x: point.y, y: point.x };
141
+ function flipSize(size, down) {
142
+ return down ? { w: size.h, h: size.w } : { w: size.w, h: size.h };
143
+ }
144
+ function flipPoint(point, down) {
145
+ return down ? { x: point.y, y: point.x } : point;
143
146
  }
144
147
  /* Rounded, because a diagram lays its boxes and its lines out on a whole-pixel grid. */
145
148
  function center(box) {
@@ -263,8 +266,7 @@ function unitsOf(nodes, layers, sizes, down) {
263
266
  layer: layers.get(node.id) ?? 0,
264
267
  node: index,
265
268
  band: -1,
266
- w: down ? size.h : size.w,
267
- h: down ? size.w : size.h,
269
+ ...flipSize(size, down),
268
270
  margin: NODE_MARGIN,
269
271
  line: -1,
270
272
  up: [],
@@ -472,13 +474,15 @@ function placeAlong(layering, edges, ports, labelSizes, layerCount, down) {
472
474
  const edge = labelHop.get(hop);
473
475
  const size = edge === undefined ? null : labelSizes[edge];
474
476
  if (edge !== undefined && size) {
475
- channelLabels.push({ edge, hop, y: ports.hopStart[hop], along: down ? size.h : size.w, across: down ? size.w : size.h });
477
+ const frame = flipSize(size, down);
478
+ channelLabels.push({ edge, hop, y: ports.hopStart[hop], along: frame.w, across: frame.h });
476
479
  }
477
480
  }
478
481
  loops.forEach((loop, index) => {
479
482
  const size = labelSizes[loop.edge];
480
483
  if (size && units[loop.unit].layer === layer) {
481
- channelLabels.push({ edge: loop.edge, hop: -1, y: ports.loopEnd[index], along: down ? size.h : size.w, across: down ? size.w : size.h });
484
+ const frame = flipSize(size, down);
485
+ channelLabels.push({ edge: loop.edge, hop: -1, y: ports.loopEnd[index], along: frame.w, across: frame.h });
482
486
  }
483
487
  });
484
488
  channelLabels.sort((left, right) => left.edge - right.edge);
@@ -562,13 +566,13 @@ export function layoutOf(document) {
562
566
  const box = frameBoxOf(unit);
563
567
  return box.x + insetOf(shapeOf(unit), box, y - middleOf(unit), down);
564
568
  };
565
- const toReal = (point) => (down ? flipPoint(point) : point);
569
+ const toReal = (point) => flipPoint(point, down);
566
570
  const labelOf = (edge, frame) => {
567
571
  const size = labelSizes[edge];
568
572
  if (!size || !frame) {
569
573
  return null;
570
574
  }
571
- return { ...(down ? flip(frame) : frame), lines: size.lines };
575
+ return { ...flip(frame, down), lines: size.lines };
572
576
  };
573
577
  const nodes = document.nodes.map((node, index) => {
574
578
  const size = sizes[index];
@@ -587,9 +591,7 @@ export function layoutOf(document) {
587
591
  sub: size.sub
588
592
  };
589
593
  }
590
- const frame = frameBoxOf(unit);
591
- const box = down ? flip(frame) : frame;
592
- return { id: node.id, ...box, layer: units[unit].layer, pinned: false, label: size.label, sub: size.sub };
594
+ return { id: node.id, ...flip(frameBoxOf(unit), down), layer: units[unit].layer, pinned: false, label: size.label, sub: size.sub };
593
595
  });
594
596
  const byId = new Map(nodes.map((box) => [box.id, box]));
595
597
  const routes = [];
@@ -633,15 +635,14 @@ export function layoutOf(document) {
633
635
  }
634
636
  const size = labelSizes[loop.edge];
635
637
  // In the last layer there is no channel to put a label in, so it sits beside the loop.
636
- const frame = frameLabels.get(loop.edge) ??
637
- (size
638
- ? {
639
- x: reach + 6,
640
- y: Math.round((startY + endY) / 2) - Math.floor((down ? size.w : size.h) / 2),
641
- w: down ? size.h : size.w,
642
- h: down ? size.w : size.h
643
- }
644
- : undefined);
638
+ const beside = () => {
639
+ if (!size) {
640
+ return undefined;
641
+ }
642
+ const turned = flipSize(size, down);
643
+ return { x: reach + 6, y: Math.round((startY + endY) / 2) - Math.floor(turned.h / 2), ...turned };
644
+ };
645
+ const frame = frameLabels.get(loop.edge) ?? beside();
645
646
  const edge = document.edges[loop.edge];
646
647
  routes[loop.edge] = { index: loop.edge, from: edge.from, to: edge.to, points: points.map(toReal), label: labelOf(loop.edge, frame) };
647
648
  });
@@ -671,8 +672,7 @@ export function layoutOf(document) {
671
672
  const members = band.members.flat().filter((unit) => units[unit].node >= 0);
672
673
  const alongStart = Math.min(...band.members[0].map((unit) => unitX[unit])) - alongBefore;
673
674
  const alongEnd = Math.max(...members.map((unit) => unitX[unit] + units[unit].w)) + GROUP_PADDING;
674
- const frame = { x: alongStart, y: band.top, w: alongEnd - alongStart, h: band.height };
675
- rects.push(down ? flip(frame) : frame);
675
+ rects.push(flip({ x: alongStart, y: band.top, w: alongEnd - alongStart, h: band.height }, down));
676
676
  }
677
677
  for (const id of group.wraps) {
678
678
  const box = byId.get(id);
package/dist/order.js CHANGED
@@ -187,9 +187,8 @@ export function orderLayers(layering) {
187
187
  });
188
188
  items.forEach((_, layer) => flatten(layer));
189
189
  // A band's first layer always holds a member, which is what made it the first.
190
- const firstSeen = bands.map((band, index) => [band.first, position[band.members[0][0]], index]);
191
- firstSeen
192
- .slice()
190
+ bands
191
+ .map((band, index) => [band.first, position[band.members[0][0]], index])
193
192
  .sort((left, right) => left[0] - right[0] || left[1] - right[1] || left[2] - right[2])
194
193
  .forEach(([, , band], rank) => {
195
194
  bands[band].rank = rank;
package/dist/route.d.ts CHANGED
@@ -1,6 +1,4 @@
1
- import type { Point, Rect } from './layout.ts';
2
- export declare const TRACK_GAP = 12;
3
- export declare const MIN_CHANNEL = 64;
1
+ import type { Rect } from './layout.ts';
4
2
  export interface ChannelHop {
5
3
  hop: number;
6
4
  ya: number;
@@ -29,5 +27,4 @@ export interface ChannelResult {
29
27
  trackX: Map<number, number>;
30
28
  labels: Map<number, Rect>;
31
29
  }
32
- export declare function conflictsOf(left: readonly Point[], right: readonly Point[]): number;
33
30
  export declare function routeChannel(input: ChannelInput): ChannelResult;
package/dist/route.js CHANGED
@@ -1,18 +1,16 @@
1
- export const TRACK_GAP = 12;
1
+ import { intersects } from '@adecore/drawing';
2
+ const TRACK_GAP = 12;
2
3
  /* Space between the lane of labels and the first track. */
3
4
  const LANE_GAP = 12;
4
5
  /* Space between two labels side by side in a lane. */
5
6
  const LABEL_GAP = 8;
6
7
  /* How far a label keeps from the line it belongs to. */
7
8
  const LABEL_OFFSET = 4;
8
- export const MIN_CHANNEL = 64;
9
+ const MIN_CHANNEL = 64;
9
10
  /* Two horizontal lines closer than this read as one. */
10
11
  const COLLINEAR = 4;
11
12
  /* Two vertical ranges closer than this need tracks of their own. */
12
13
  const RANGE_CLEARANCE = 8;
13
- function overlaps(left, right) {
14
- return left.x < right.x + right.w && right.x < left.x + left.w && left.y < right.y + right.h && right.y < left.y + left.h;
15
- }
16
14
  function inflate(rect, by) {
17
15
  return { x: rect.x - by, y: rect.y - by, w: rect.w + by * 2, h: rect.h + by * 2 };
18
16
  }
@@ -20,7 +18,7 @@ function inflate(rect, by) {
20
18
  * How badly two orthogonal polylines get in each other's way: a crossing counts once, a stretch where
21
19
  * they run on top of each other counts four times, since that is the one a reader cannot follow.
22
20
  */
23
- export function conflictsOf(left, right) {
21
+ function conflictsOf(left, right) {
24
22
  let total = 0;
25
23
  for (let i = 1; i < left.length; i++) {
26
24
  const a0 = left[i - 1];
@@ -124,7 +122,7 @@ export function routeChannel(input) {
124
122
  let slotsBefore = input.labels.length > 0 ? 1 : 0;
125
123
  let slotsAfter = 0;
126
124
  const touches = (x0, x1, y0, y1, rect) => x1 >= rect.x - 3 && x0 <= rect.x + rect.w + 3 && y1 >= rect.y - 3 && y0 <= rect.y + rect.h + 3;
127
- const blocked = (zone, rect) => placed.some((other) => other.zone === zone && overlaps(inflate(other.rect, 4), rect)) ||
125
+ const blocked = (zone, rect) => placed.some((other) => other.zone === zone && intersects(inflate(other.rect, 4), rect)) ||
128
126
  input.obstacles.some((obstacle) => obstacle.y < rect.y + rect.h && rect.y < obstacle.y + obstacle.h);
129
127
  const onLine = (zone, rect, label) => input.hops.some((hop) => {
130
128
  if (zone === 'before') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/diagram",
3
- "version": "0.39.0",
3
+ "version": "0.40.0",
4
4
  "description": "Directed graph schemas, layered layout, SVG export and reading order.",
5
5
  "license": "FSL-1.1-MIT",
6
6
  "author": "Bas Milius",
@@ -40,12 +40,12 @@
40
40
  "test": "bun test --conditions=source"
41
41
  },
42
42
  "devDependencies": {
43
- "@types/bun": "^1.4.2",
44
- "@types/node": "^24.13.6",
43
+ "@types/bun": "^1.4.4",
44
+ "@types/node": "^24.19.2",
45
45
  "typescript": "^7.0.2"
46
46
  },
47
47
  "dependencies": {
48
- "@adecore/drawing": "0.39.0",
49
- "zod": "^4.6.5"
48
+ "@adecore/drawing": "0.40.0",
49
+ "zod": "^4.6.7"
50
50
  }
51
51
  }
package/src/across.ts CHANGED
@@ -23,21 +23,29 @@ export function placeAcross(layering: Layering): number[] {
23
23
  const local = new Array<number>(unitCount).fill(0);
24
24
 
25
25
  const variableOf = (code: number): number => (code >= 0 ? code : unitCount + bandOfCode(code));
26
+
26
27
  const codeOf = (variable: number): number => (variable < unitCount ? variable : -(variable - unitCount + 1));
28
+
27
29
  const sizeOf = (code: number): number => (code >= 0 ? units[code]!.h : bands[bandOfCode(code)]!.height);
30
+
28
31
  const marginOf = (code: number): number => (code >= 0 ? units[code]!.margin : BAND_MARGIN);
32
+
29
33
  const topOf = (code: number): number => top[variableOf(code)]!;
34
+
30
35
  const absoluteTop = (unit: number): number => {
31
36
  const band = units[unit]!.band;
37
+
32
38
  return band >= 0 ? top[unitCount + band]! + local[unit]! : top[unit]!;
33
39
  };
34
40
 
35
41
  const successors: number[][] = Array.from({ length: variableCount }, () => []);
36
42
  const incoming = new Array<number>(variableCount).fill(0);
37
43
  const present = new Array<boolean>(variableCount).fill(false);
44
+
38
45
  for (const row of items) {
39
46
  row.forEach((code, index) => {
40
47
  present[variableOf(code)] = true;
48
+
41
49
  if (index > 0) {
42
50
  successors[variableOf(row[index - 1]!)]!.push(code);
43
51
  incoming[variableOf(code)]! += 1;
@@ -48,35 +56,44 @@ export function placeAcross(layering: Layering): number[] {
48
56
  // The band order is the same in every layer, so these constraints never form a cycle.
49
57
  const lowest = new Array<number>(variableCount).fill(0);
50
58
  const queue: number[] = [];
59
+
51
60
  for (let variable = 0; variable < variableCount; variable++) {
52
61
  if (present[variable] && incoming[variable] === 0) {
53
62
  queue.push(variable);
54
63
  }
55
64
  }
65
+
56
66
  for (let i = 0; i < queue.length; i++) {
57
67
  const variable = queue[i]!;
58
68
  const code = codeOf(variable);
69
+
59
70
  for (const next of successors[variable]!) {
60
71
  const target = variableOf(next);
61
72
  lowest[target] = Math.max(lowest[target]!, lowest[variable]! + sizeOf(code) + marginOf(code) + marginOf(next));
62
73
  incoming[target]! -= 1;
74
+
63
75
  if (incoming[target] === 0) {
64
76
  queue.push(target);
65
77
  }
66
78
  }
67
79
  }
80
+
68
81
  const tail = new Array<number>(variableCount).fill(0);
69
82
  let total = 0;
83
+
70
84
  for (let i = queue.length - 1; i >= 0; i--) {
71
85
  const variable = queue[i]!;
72
86
  const code = codeOf(variable);
73
87
  let length = sizeOf(code);
88
+
74
89
  for (const next of successors[variable]!) {
75
90
  length = Math.max(length, sizeOf(code) + marginOf(code) + marginOf(next) + tail[variableOf(next)]!);
76
91
  }
92
+
77
93
  tail[variable] = length;
78
94
  total = Math.max(total, lowest[variable]! + length);
79
95
  }
96
+
80
97
  for (const variable of queue) {
81
98
  top[variable] = Math.floor((lowest[variable]! + total - tail[variable]!) / 2);
82
99
  }
@@ -85,19 +102,25 @@ export function placeAcross(layering: Layering): number[] {
85
102
  for (const slot of band.members) {
86
103
  const low: number[] = [];
87
104
  let at = band.before;
105
+
88
106
  slot.forEach((unit, index) => {
89
107
  if (index > 0) {
90
108
  const previous = units[slot[index - 1]!]!;
91
109
  at += previous.h + previous.margin + units[unit]!.margin;
92
110
  }
111
+
93
112
  low.push(at);
94
113
  });
114
+
95
115
  let high = band.height - band.after;
116
+
96
117
  for (let index = slot.length - 1; index >= 0; index--) {
97
118
  const unit = units[slot[index]!]!;
119
+
98
120
  if (index < slot.length - 1) {
99
121
  high -= unit.margin + units[slot[index + 1]!]!.margin;
100
122
  }
123
+
101
124
  high -= unit.h;
102
125
  local[slot[index]!] = Math.floor((low[index]! + high) / 2);
103
126
  }
@@ -107,25 +130,34 @@ export function placeAcross(layering: Layering): number[] {
107
130
  const medianOf = (values: number[]): number => {
108
131
  values.sort((left, right) => left - right);
109
132
  const middle = values.length >> 1;
133
+
110
134
  return values.length % 2 === 1 ? values[middle]! : (values[middle - 1]! + values[middle]!) / 2;
111
135
  };
136
+
112
137
  const desiredTop = (unit: number): number | null => {
113
138
  const self = units[unit]!;
139
+
114
140
  if (self.line >= 0) {
115
141
  // The points of one long edge pull towards each other rather than their neighbors, so the edge runs straight
116
142
  // instead of stepping a little in every layer it passes.
117
143
  const line = lines[self.line]!;
118
144
  const ends = [units[line[0]!]!.up[0]!, units[line.at(-1)!]!.down[0]!].map((end) => absoluteTop(end) + units[end]!.h / 2);
145
+
119
146
  if (line.length === 1) {
120
147
  const here = absoluteTop(unit);
148
+
121
149
  return Math.round(Math.abs(ends[1]! - here) < Math.abs(ends[0]! - here) ? ends[1]! : ends[0]!);
122
150
  }
151
+
123
152
  return Math.round(medianOf([...line.filter((other) => other !== unit).map(absoluteTop), ...ends]));
124
153
  }
154
+
125
155
  const neighbors = [...self.up, ...self.down];
156
+
126
157
  if (neighbors.length === 0) {
127
158
  return null;
128
159
  }
160
+
129
161
  return Math.round(medianOf(neighbors.map((neighbor) => absoluteTop(neighbor) + units[neighbor]!.h / 2)) - self.h / 2);
130
162
  };
131
163
 
@@ -133,27 +165,35 @@ export function placeAcross(layering: Layering): number[] {
133
165
  for (let step = 0; step < items.length; step++) {
134
166
  const layer = round % 2 === 0 ? step : items.length - 1 - step;
135
167
  const row = items[layer]!;
168
+
136
169
  row.forEach((code, index) => {
137
170
  if (code >= 0) {
138
171
  const want = desiredTop(code);
172
+
139
173
  if (want === null) {
140
174
  return;
141
175
  }
176
+
142
177
  const previous = row[index - 1];
143
178
  const next = row[index + 1];
144
179
  const low = previous === undefined ? -Infinity : topOf(previous) + sizeOf(previous) + marginOf(previous) + marginOf(code);
145
180
  const high = next === undefined ? Infinity : topOf(next) - marginOf(next) - marginOf(code) - sizeOf(code);
146
181
  top[code] = clamp(want, low, high);
182
+
147
183
  return;
148
184
  }
185
+
149
186
  const bandIndex = bandOfCode(code);
150
187
  const band = bands[bandIndex]!;
151
188
  const slot = band.members[layer - band.first]!;
189
+
152
190
  slot.forEach((unit, position) => {
153
191
  const want = desiredTop(unit);
192
+
154
193
  if (want === null) {
155
194
  return;
156
195
  }
196
+
157
197
  const self = units[unit]!;
158
198
  const previous = slot[position - 1];
159
199
  const next = slot[position + 1];
@@ -166,48 +206,60 @@ export function placeAcross(layering: Layering): number[] {
166
206
 
167
207
  bands.forEach((band, bandIndex) => {
168
208
  const shifts: number[] = [];
209
+
169
210
  for (const unit of band.members.flat()) {
170
211
  const want = desiredTop(unit);
212
+
171
213
  if (want !== null) {
172
214
  shifts.push(want - absoluteTop(unit));
173
215
  }
174
216
  }
217
+
175
218
  if (shifts.length === 0) {
176
219
  return;
177
220
  }
221
+
178
222
  const code = bandCode(bandIndex);
179
223
  let low = -Infinity;
180
224
  let high = Infinity;
225
+
181
226
  for (let layer = band.first; layer <= band.last; layer++) {
182
227
  const row = items[layer]!;
183
228
  const index = row.indexOf(code);
184
229
  const previous = row[index - 1];
185
230
  const next = row[index + 1];
231
+
186
232
  if (previous !== undefined) {
187
233
  low = Math.max(low, topOf(previous) + sizeOf(previous) + marginOf(previous) + BAND_MARGIN);
188
234
  }
235
+
189
236
  if (next !== undefined) {
190
237
  high = Math.min(high, topOf(next) - marginOf(next) - BAND_MARGIN - band.height);
191
238
  }
192
239
  }
240
+
193
241
  const shift = Math.round(shifts.reduce((sum, value) => sum + value, 0) / shifts.length);
194
242
  top[unitCount + bandIndex] = clamp(top[unitCount + bandIndex]! + shift, low, high);
195
243
  });
196
244
  }
197
245
 
198
246
  let least = Infinity;
247
+
199
248
  for (const row of items) {
200
249
  for (const code of row) {
201
250
  least = Math.min(least, topOf(code));
202
251
  }
203
252
  }
253
+
204
254
  if (Number.isFinite(least)) {
205
255
  for (let variable = 0; variable < variableCount; variable++) {
206
256
  top[variable] = top[variable]! - least || 0;
207
257
  }
208
258
  }
259
+
209
260
  bands.forEach((band, index) => {
210
261
  band.top = top[unitCount + index]!;
211
262
  });
263
+
212
264
  return units.map((_, unit) => absoluteTop(unit));
213
265
  }