@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 +24 -24
- package/dist/order.js +2 -3
- package/dist/route.d.ts +1 -4
- package/dist/route.js +5 -7
- package/package.json +5 -5
- package/src/across.ts +52 -0
- package/src/layout.ts +183 -25
- package/src/order.ts +49 -3
- package/src/protocol.ts +14 -0
- package/src/reading-order.ts +5 -0
- package/src/route.ts +53 -8
- package/src/shapes.ts +8 -0
- package/src/svg.ts +7 -0
- package/src/text.ts +19 -0
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
|
|
142
|
-
return {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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 { ...
|
|
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
|
-
|
|
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
|
|
637
|
-
(size
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
|
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.
|
|
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.
|
|
44
|
-
"@types/node": "^24.
|
|
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.
|
|
49
|
-
"zod": "^4.6.
|
|
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
|
}
|