@elicdavis/node-flow 0.1.19 → 0.2.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/index.d.mts +386 -227
- package/dist/index.d.ts +386 -227
- package/dist/index.js +1884 -1076
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1867 -1075
- package/dist/index.mjs.map +1 -1
- package/index.html +1134 -329
- package/package.json +1 -1
- package/src/connection.ts +30 -5
- package/src/contextMenu.ts +66 -34
- package/src/graph.ts +12 -10
- package/src/index.ts +26 -12
- package/src/markdown/entry.ts +11 -15
- package/src/markdown/syntaxParser.ts +2 -1
- package/src/node.ts +162 -111
- package/src/nodes/subsystem.ts +123 -29
- package/src/notes/note.ts +9 -1
- package/src/organize.ts +540 -111
- package/src/popup.ts +4 -2
- package/src/port.ts +83 -6
- package/src/styles/bevel.ts +31 -0
- package/src/styles/cursor.ts +1 -0
- package/src/styles/text.ts +4 -4
- package/src/theme.ts +179 -92
- package/src/types/text.ts +5 -1
- package/src/utils/color.ts +1 -1
- package/src/widgets/button.ts +52 -107
- package/src/widgets/color.ts +91 -150
- package/src/widgets/factory.ts +89 -46
- package/src/widgets/field.ts +99 -0
- package/src/widgets/image.ts +107 -165
- package/src/widgets/inplace.ts +84 -0
- package/src/widgets/number.ts +122 -154
- package/src/widgets/row.ts +138 -0
- package/src/widgets/slider.ts +101 -216
- package/src/widgets/string.ts +39 -130
- package/src/widgets/text.ts +81 -92
- package/src/widgets/toggle.ts +62 -207
- package/src/widgets/value.ts +127 -0
- package/src/widgets/widget.ts +180 -34
- package/test/context_menu.test.ts +82 -0
- package/test/cursor.test.ts +17 -0
- package/test/hidden_port.test.ts +125 -0
- package/test/hover_after_removal.test.ts +72 -0
- package/test/markdown.test.ts +2 -2
- package/test/markdown_code_block.test.ts +58 -0
- package/test/multi_input.test.ts +124 -0
- package/test/organize.test.ts +117 -1
- package/test/property_binding.test.ts +49 -0
- package/test/support.ts +175 -0
- package/test/theme.test.ts +86 -0
- package/test/widget_click.test.ts +111 -97
- package/test/widget_construction.test.ts +5 -4
- package/test/widgets.test.ts +333 -0
- package/test/wire_follows_camera.test.ts +57 -0
package/src/organize.ts
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
import { Camera } from "./camera";
|
|
2
2
|
import { FlowNode } from "./node";
|
|
3
3
|
import { NodeSubsystem } from "./nodes/subsystem";
|
|
4
|
-
import { Box } from "./types/box";
|
|
5
4
|
import { Vector2 } from "./types/vector2";
|
|
6
5
|
|
|
7
6
|
const widthSpacing = 100;
|
|
8
7
|
const heightSpacing = 50;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* How many times to sweep the graph re-ordering each layer by where its
|
|
12
|
-
* neighbours sit. Crossings drop quickly and then stop improving, so there
|
|
13
|
-
* is no reason to keep going.
|
|
14
|
-
*/
|
|
8
|
+
const laneSpacing = 25;
|
|
9
|
+
const pieceSpacing = 150;
|
|
15
10
|
const orderingSweeps = 8;
|
|
11
|
+
const placementSweeps = 8;
|
|
16
12
|
|
|
17
13
|
export interface Layout {
|
|
18
14
|
/** Indices into the node array, one list per layer, left to right. */
|
|
@@ -21,17 +17,12 @@ export interface Layout {
|
|
|
21
17
|
layerOf: Map<number, number>;
|
|
22
18
|
}
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
* closing a cycle - are the ones left out, which is what keeps a cyclic
|
|
28
|
-
* graph from hanging or stacking into one column.
|
|
29
|
-
*/
|
|
30
|
-
export function topologicalOrder(sorting: Array<number>, upstream: Map<number, Array<number>>): Array<number> {
|
|
20
|
+
type Edges = Map<number, Array<number>>;
|
|
21
|
+
|
|
22
|
+
export function topologicalOrder(sorting: Array<number>, upstream: Edges): Array<number> {
|
|
31
23
|
const visited = new Set<number>();
|
|
32
24
|
const order = new Array<number>();
|
|
33
25
|
|
|
34
|
-
// Iterative: a deep graph would blow the stack recursing.
|
|
35
26
|
for (const root of sorting) {
|
|
36
27
|
if (visited.has(root)) {
|
|
37
28
|
continue;
|
|
@@ -54,11 +45,7 @@ export function topologicalOrder(sorting: Array<number>, upstream: Map<number, A
|
|
|
54
45
|
}
|
|
55
46
|
visited.add(top.node);
|
|
56
47
|
|
|
57
|
-
const
|
|
58
|
-
if (feeders === undefined) {
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
for (const feeder of feeders) {
|
|
48
|
+
for (const feeder of upstream.get(top.node) ?? []) {
|
|
62
49
|
if (!visited.has(feeder)) {
|
|
63
50
|
stack.push({ node: feeder, expanded: false });
|
|
64
51
|
}
|
|
@@ -69,73 +56,129 @@ export function topologicalOrder(sorting: Array<number>, upstream: Map<number, A
|
|
|
69
56
|
return order;
|
|
70
57
|
}
|
|
71
58
|
|
|
59
|
+
function consumersOf(sorting: Array<number>, upstream: Edges): Edges {
|
|
60
|
+
const downstream: Edges = new Map();
|
|
61
|
+
for (const node of sorting) {
|
|
62
|
+
downstream.set(node, []);
|
|
63
|
+
}
|
|
64
|
+
for (const node of sorting) {
|
|
65
|
+
for (const feeder of upstream.get(node) ?? []) {
|
|
66
|
+
const consumers = downstream.get(feeder);
|
|
67
|
+
if (consumers !== undefined && !consumers.includes(node)) {
|
|
68
|
+
consumers.push(node);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return downstream;
|
|
73
|
+
}
|
|
74
|
+
|
|
72
75
|
/**
|
|
73
|
-
* Puts every node
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* from landing on top of each other.
|
|
76
|
+
* Puts every node in the layer just left of its leftmost consumer, so an
|
|
77
|
+
* input sits beside what it feeds rather than at the far left. Nodes nothing
|
|
78
|
+
* consumes sit just right of their feeders.
|
|
77
79
|
*/
|
|
78
|
-
export function assignLayers(sorting: Array<number>, upstream:
|
|
79
|
-
const
|
|
80
|
+
export function assignLayers(sorting: Array<number>, upstream: Edges): Layout {
|
|
81
|
+
const downstream = consumersOf(sorting, upstream);
|
|
82
|
+
const order = topologicalOrder(sorting, downstream);
|
|
80
83
|
const placed = new Set<number>();
|
|
81
|
-
const
|
|
84
|
+
const rank = new Map<number, number>();
|
|
82
85
|
|
|
83
86
|
for (const node of order) {
|
|
84
|
-
let
|
|
85
|
-
for (const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (placed.has(feeder)) {
|
|
89
|
-
layer = Math.max(layer, (layerOf.get(feeder) as number) + 1);
|
|
87
|
+
let r = 0;
|
|
88
|
+
for (const consumer of downstream.get(node) ?? []) {
|
|
89
|
+
if (placed.has(consumer)) {
|
|
90
|
+
r = Math.max(r, (rank.get(consumer) as number) + 1);
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
|
-
|
|
93
|
+
rank.set(node, r);
|
|
93
94
|
placed.add(node);
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
let deepest = 0;
|
|
97
|
-
for (const
|
|
98
|
-
deepest = Math.max(deepest,
|
|
98
|
+
for (const r of rank.values()) {
|
|
99
|
+
deepest = Math.max(deepest, r);
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
const
|
|
102
|
-
for (
|
|
103
|
-
|
|
102
|
+
const layerOf = new Map<number, number>();
|
|
103
|
+
for (const node of sorting) {
|
|
104
|
+
layerOf.set(node, deepest - (rank.get(node) as number));
|
|
104
105
|
}
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
|
|
107
|
+
for (const node of sorting) {
|
|
108
|
+
const layer = layerOf.get(node) as number;
|
|
109
|
+
if ((downstream.get(node) ?? []).some((c) => (layerOf.get(c) as number) > layer)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
let pulled = -1;
|
|
113
|
+
for (const feeder of upstream.get(node) ?? []) {
|
|
114
|
+
const feederLayer = layerOf.get(feeder);
|
|
115
|
+
if (feederLayer !== undefined && feederLayer < layer) {
|
|
116
|
+
pulled = Math.max(pulled, feederLayer + 1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (pulled >= 0) {
|
|
120
|
+
layerOf.set(node, pulled);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const used = [...new Set(layerOf.values())].sort((a, b) => a - b);
|
|
125
|
+
const compact = new Map<number, number>();
|
|
126
|
+
used.forEach((layer, i) => compact.set(layer, i));
|
|
127
|
+
|
|
128
|
+
const layers = used.map(() => new Array<number>());
|
|
129
|
+
for (const node of sorting) {
|
|
130
|
+
const layer = compact.get(layerOf.get(node) as number) as number;
|
|
131
|
+
layerOf.set(node, layer);
|
|
132
|
+
layers[layer].push(node);
|
|
107
133
|
}
|
|
108
134
|
|
|
109
135
|
return { layers, layerOf };
|
|
110
136
|
}
|
|
111
137
|
|
|
138
|
+
function crossingsOf(layout: Layout, downstream: Edges): number {
|
|
139
|
+
const slotOf = new Map<number, number>();
|
|
140
|
+
for (const layer of layout.layers) {
|
|
141
|
+
layer.forEach((node, slot) => slotOf.set(node, slot));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let total = 0;
|
|
145
|
+
for (let i = 0; i + 1 < layout.layers.length; i++) {
|
|
146
|
+
const spans = new Array<[number, number]>();
|
|
147
|
+
for (const node of layout.layers[i]) {
|
|
148
|
+
for (const consumer of downstream.get(node) ?? []) {
|
|
149
|
+
if (layout.layerOf.get(consumer) === i + 1) {
|
|
150
|
+
spans.push([slotOf.get(node) as number, slotOf.get(consumer) as number]);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for (let a = 0; a < spans.length; a++) {
|
|
155
|
+
for (let b = a + 1; b < spans.length; b++) {
|
|
156
|
+
if ((spans[a][0] - spans[b][0]) * (spans[a][1] - spans[b][1]) < 0) {
|
|
157
|
+
total++;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return total;
|
|
163
|
+
}
|
|
164
|
+
|
|
112
165
|
/**
|
|
113
166
|
* Reorders each layer so nodes sit across from the ones they connect to,
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* edges crossing over each other.
|
|
167
|
+
* sweeping back and forth and keeping whichever order crossed the fewest
|
|
168
|
+
* wires.
|
|
117
169
|
*/
|
|
118
|
-
export function orderWithinLayers(
|
|
119
|
-
layout: Layout,
|
|
120
|
-
upstream: Map<number, Array<number>>,
|
|
121
|
-
downstream: Map<number, Array<number>>,
|
|
122
|
-
): void {
|
|
170
|
+
export function orderWithinLayers(layout: Layout, upstream: Edges, downstream: Edges): void {
|
|
123
171
|
const slotOf = new Map<number, number>();
|
|
124
|
-
const
|
|
125
|
-
for (
|
|
126
|
-
|
|
127
|
-
slotOf.set(layer[i], i);
|
|
128
|
-
}
|
|
172
|
+
for (const layer of layout.layers) {
|
|
173
|
+
for (let i = 0; i < layer.length; i++) {
|
|
174
|
+
slotOf.set(layer[i], i);
|
|
129
175
|
}
|
|
130
|
-
}
|
|
131
|
-
recordSlots();
|
|
176
|
+
}
|
|
132
177
|
|
|
133
|
-
const meanSlot = (node: number, neighbours:
|
|
178
|
+
const meanSlot = (node: number, neighbours: Edges, layer: number): number => {
|
|
134
179
|
let total = 0;
|
|
135
180
|
let count = 0;
|
|
136
181
|
for (const neighbour of neighbours.get(node) ?? []) {
|
|
137
|
-
// Only neighbours in the adjacent layer say anything about where
|
|
138
|
-
// this node should sit; one several layers away does not.
|
|
139
182
|
if (layout.layerOf.get(neighbour) !== layer) {
|
|
140
183
|
continue;
|
|
141
184
|
}
|
|
@@ -145,7 +188,7 @@ export function orderWithinLayers(
|
|
|
145
188
|
return count === 0 ? -1 : total / count;
|
|
146
189
|
};
|
|
147
190
|
|
|
148
|
-
const sweep = (neighbours:
|
|
191
|
+
const sweep = (neighbours: Edges, towards: number): void => {
|
|
149
192
|
for (let i = 0; i < layout.layers.length; i++) {
|
|
150
193
|
const index = towards > 0 ? i : layout.layers.length - 1 - i;
|
|
151
194
|
const adjacent = index + towards;
|
|
@@ -157,8 +200,6 @@ export function orderWithinLayers(
|
|
|
157
200
|
const keys = new Map<number, number>();
|
|
158
201
|
for (const node of layer) {
|
|
159
202
|
const mean = meanSlot(node, neighbours, adjacent);
|
|
160
|
-
// A node with nothing in the adjacent layer keeps its place
|
|
161
|
-
// rather than being dragged to the top.
|
|
162
203
|
keys.set(node, mean === -1 ? (slotOf.get(node) as number) : mean);
|
|
163
204
|
}
|
|
164
205
|
|
|
@@ -169,10 +210,424 @@ export function orderWithinLayers(
|
|
|
169
210
|
}
|
|
170
211
|
};
|
|
171
212
|
|
|
172
|
-
|
|
213
|
+
let best = layout.layers.map((layer) => layer.slice());
|
|
214
|
+
let fewest = crossingsOf(layout, downstream);
|
|
215
|
+
for (let i = 0; i < orderingSweeps && fewest > 0; i++) {
|
|
173
216
|
sweep(upstream, -1);
|
|
174
217
|
sweep(downstream, 1);
|
|
218
|
+
const crossings = crossingsOf(layout, downstream);
|
|
219
|
+
if (crossings < fewest) {
|
|
220
|
+
fewest = crossings;
|
|
221
|
+
best = layout.layers.map((layer) => layer.slice());
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
for (let i = 0; i < best.length; i++) {
|
|
226
|
+
layout.layers[i] = best[i];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function findPieces(sorting: Array<number>, upstream: Edges, downstream: Edges): Array<Array<number>> {
|
|
231
|
+
const rankOf = new Map<number, number>();
|
|
232
|
+
sorting.forEach((node, i) => rankOf.set(node, i));
|
|
233
|
+
|
|
234
|
+
const seen = new Set<number>();
|
|
235
|
+
const pieces = new Array<Array<number>>();
|
|
236
|
+
for (const start of sorting) {
|
|
237
|
+
if (seen.has(start)) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
seen.add(start);
|
|
241
|
+
const piece = new Array<number>();
|
|
242
|
+
const stack = [start];
|
|
243
|
+
while (stack.length > 0) {
|
|
244
|
+
const node = stack.pop() as number;
|
|
245
|
+
piece.push(node);
|
|
246
|
+
for (const neighbour of [...(upstream.get(node) ?? []), ...(downstream.get(node) ?? [])]) {
|
|
247
|
+
if (rankOf.has(neighbour) && !seen.has(neighbour)) {
|
|
248
|
+
seen.add(neighbour);
|
|
249
|
+
stack.push(neighbour);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
piece.sort((a, b) => (rankOf.get(a) as number) - (rankOf.get(b) as number));
|
|
254
|
+
pieces.push(piece);
|
|
255
|
+
}
|
|
256
|
+
return pieces;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function treeRoot(piece: Array<number>, downstream: Edges): number | undefined {
|
|
260
|
+
let root: number | undefined;
|
|
261
|
+
for (const node of piece) {
|
|
262
|
+
const consumers = downstream.get(node) ?? [];
|
|
263
|
+
if (consumers.length > 1) {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
if (consumers.length === 0) {
|
|
267
|
+
if (root !== undefined) {
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
root = node;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return root;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function columnXs(widths: Array<number>): Array<number> {
|
|
277
|
+
const xs = new Array<number>();
|
|
278
|
+
let x = 0;
|
|
279
|
+
for (const width of widths) {
|
|
280
|
+
xs.push(x);
|
|
281
|
+
x += width + widthSpacing;
|
|
282
|
+
}
|
|
283
|
+
return xs;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function layoutTree(root: number, upstream: Edges, sizes: Map<number, Vector2>): Map<number, Vector2> {
|
|
287
|
+
const children = (node: number): Array<number> => [...new Set(upstream.get(node) ?? [])];
|
|
288
|
+
|
|
289
|
+
const depthOf = new Map<number, number>([[root, 0]]);
|
|
290
|
+
const preorder = new Array<number>();
|
|
291
|
+
const stack = [root];
|
|
292
|
+
let deepest = 0;
|
|
293
|
+
while (stack.length > 0) {
|
|
294
|
+
const node = stack.pop() as number;
|
|
295
|
+
preorder.push(node);
|
|
296
|
+
const kids = children(node);
|
|
297
|
+
for (let i = kids.length - 1; i >= 0; i--) {
|
|
298
|
+
const depth = (depthOf.get(node) as number) + 1;
|
|
299
|
+
depthOf.set(kids[i], depth);
|
|
300
|
+
deepest = Math.max(deepest, depth);
|
|
301
|
+
stack.push(kids[i]);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const widths = new Array<number>(deepest + 1).fill(0);
|
|
306
|
+
for (const [node, depth] of depthOf) {
|
|
307
|
+
widths[deepest - depth] = Math.max(widths[deepest - depth], (sizes.get(node) as Vector2).x);
|
|
308
|
+
}
|
|
309
|
+
const xs = columnXs(widths);
|
|
310
|
+
|
|
311
|
+
const band = new Map<number, number>();
|
|
312
|
+
const topInBand = new Map<number, number>();
|
|
313
|
+
const childOffset = new Map<number, number>();
|
|
314
|
+
for (let i = preorder.length - 1; i >= 0; i--) {
|
|
315
|
+
const node = preorder[i];
|
|
316
|
+
const height = (sizes.get(node) as Vector2).y;
|
|
317
|
+
const kids = children(node);
|
|
318
|
+
if (kids.length === 0) {
|
|
319
|
+
band.set(node, height);
|
|
320
|
+
topInBand.set(node, 0);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
let offset = 0;
|
|
325
|
+
for (const kid of kids) {
|
|
326
|
+
childOffset.set(kid, offset);
|
|
327
|
+
offset += (band.get(kid) as number) + heightSpacing;
|
|
328
|
+
}
|
|
329
|
+
const centre = (kid: number) =>
|
|
330
|
+
(childOffset.get(kid) as number) + (topInBand.get(kid) as number) + (sizes.get(kid) as Vector2).y / 2;
|
|
331
|
+
let top = (centre(kids[0]) + centre(kids[kids.length - 1])) / 2 - height / 2;
|
|
332
|
+
let shift = 0;
|
|
333
|
+
if (top < 0) {
|
|
334
|
+
shift = -top;
|
|
335
|
+
top = 0;
|
|
336
|
+
for (const kid of kids) {
|
|
337
|
+
childOffset.set(kid, (childOffset.get(kid) as number) + shift);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
band.set(node, Math.max(offset - heightSpacing + shift, top + height));
|
|
341
|
+
topInBand.set(node, top);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const positions = new Map<number, Vector2>();
|
|
345
|
+
const bandTop = new Map<number, number>([[root, 0]]);
|
|
346
|
+
for (const node of preorder) {
|
|
347
|
+
const column = deepest - (depthOf.get(node) as number);
|
|
348
|
+
const size = sizes.get(node) as Vector2;
|
|
349
|
+
positions.set(node, {
|
|
350
|
+
x: xs[column] + (widths[column] - size.x) / 2,
|
|
351
|
+
y: (bandTop.get(node) as number) + (topInBand.get(node) as number),
|
|
352
|
+
});
|
|
353
|
+
for (const kid of children(node)) {
|
|
354
|
+
bandTop.set(kid, (bandTop.get(node) as number) + (childOffset.get(kid) as number));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return positions;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function isotonic(targets: Array<number>): Array<number> {
|
|
361
|
+
const blocks = new Array<{ sum: number, count: number }>();
|
|
362
|
+
for (const target of targets) {
|
|
363
|
+
blocks.push({ sum: target, count: 1 });
|
|
364
|
+
while (blocks.length > 1) {
|
|
365
|
+
const last = blocks[blocks.length - 1];
|
|
366
|
+
const before = blocks[blocks.length - 2];
|
|
367
|
+
if (before.sum / before.count <= last.sum / last.count) {
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
before.sum += last.sum;
|
|
371
|
+
before.count += last.count;
|
|
372
|
+
blocks.pop();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const result = new Array<number>();
|
|
376
|
+
for (const block of blocks) {
|
|
377
|
+
for (let i = 0; i < block.count; i++) {
|
|
378
|
+
result.push(block.sum / block.count);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function median(values: Array<number>): number {
|
|
385
|
+
const sorted = values.slice().sort((a, b) => a - b);
|
|
386
|
+
const middle = Math.floor(sorted.length / 2);
|
|
387
|
+
return sorted.length % 2 === 1 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function layoutLayered(piece: Array<number>, upstream: Edges, downstream: Edges, sizes: Map<number, Vector2>): Map<number, Vector2> {
|
|
391
|
+
const layout = assignLayers(piece, upstream);
|
|
392
|
+
|
|
393
|
+
const up: Edges = new Map();
|
|
394
|
+
const down: Edges = new Map();
|
|
395
|
+
const heightOf = new Map<number, number>();
|
|
396
|
+
for (const node of piece) {
|
|
397
|
+
up.set(node, [...new Set(upstream.get(node) ?? [])]);
|
|
398
|
+
down.set(node, [...new Set(downstream.get(node) ?? [])]);
|
|
399
|
+
heightOf.set(node, (sizes.get(node) as Vector2).y);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
let nextLane = -1;
|
|
403
|
+
for (const from of piece) {
|
|
404
|
+
const fromLayer = layout.layerOf.get(from) as number;
|
|
405
|
+
const consumers = down.get(from) as Array<number>;
|
|
406
|
+
for (let c = 0; c < consumers.length; c++) {
|
|
407
|
+
const to = consumers[c];
|
|
408
|
+
const toLayer = layout.layerOf.get(to) as number;
|
|
409
|
+
if (toLayer - fromLayer < 2) {
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
let previous = from;
|
|
414
|
+
for (let layer = fromLayer + 1; layer < toLayer; layer++) {
|
|
415
|
+
const lane = nextLane--;
|
|
416
|
+
layout.layerOf.set(lane, layer);
|
|
417
|
+
layout.layers[layer].push(lane);
|
|
418
|
+
heightOf.set(lane, 0);
|
|
419
|
+
up.set(lane, [previous]);
|
|
420
|
+
down.set(lane, []);
|
|
421
|
+
if (previous === from) {
|
|
422
|
+
consumers[c] = lane;
|
|
423
|
+
} else {
|
|
424
|
+
(down.get(previous) as Array<number>).push(lane);
|
|
425
|
+
}
|
|
426
|
+
previous = lane;
|
|
427
|
+
}
|
|
428
|
+
(down.get(previous) as Array<number>).push(to);
|
|
429
|
+
const feeders = up.get(to) as Array<number>;
|
|
430
|
+
feeders[feeders.indexOf(from)] = previous;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const sequence = new Map<number, number>();
|
|
435
|
+
for (const sink of piece) {
|
|
436
|
+
if ((down.get(sink) as Array<number>).length > 0) {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const stack = [sink];
|
|
440
|
+
while (stack.length > 0) {
|
|
441
|
+
const node = stack.pop() as number;
|
|
442
|
+
if (sequence.has(node)) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
sequence.set(node, sequence.size);
|
|
446
|
+
const feeders = up.get(node) ?? [];
|
|
447
|
+
for (let i = feeders.length - 1; i >= 0; i--) {
|
|
448
|
+
stack.push(feeders[i]);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
for (const layer of layout.layers) {
|
|
453
|
+
layer.sort((a, b) => (sequence.get(a) ?? Infinity) - (sequence.get(b) ?? Infinity));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
orderWithinLayers(layout, up, down);
|
|
457
|
+
|
|
458
|
+
const gapAfter = (above: number, below: number): number =>
|
|
459
|
+
above < 0 || below < 0 ? laneSpacing : heightSpacing;
|
|
460
|
+
|
|
461
|
+
const topOf = new Map<number, number>();
|
|
462
|
+
for (const layer of layout.layers) {
|
|
463
|
+
let y = 0;
|
|
464
|
+
for (let i = 0; i < layer.length; i++) {
|
|
465
|
+
topOf.set(layer[i], y);
|
|
466
|
+
y += (heightOf.get(layer[i]) as number) + (i + 1 < layer.length ? gapAfter(layer[i], layer[i + 1]) : 0);
|
|
467
|
+
}
|
|
468
|
+
for (const node of layer) {
|
|
469
|
+
topOf.set(node, (topOf.get(node) as number) - y / 2);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const centreOf = (node: number) => (topOf.get(node) as number) + (heightOf.get(node) as number) / 2;
|
|
474
|
+
|
|
475
|
+
const place = (layer: Array<number>, neighbours: Edges, adjacent: number): void => {
|
|
476
|
+
const offsets = new Array<number>();
|
|
477
|
+
const targets = new Array<number>();
|
|
478
|
+
let offset = 0;
|
|
479
|
+
for (let i = 0; i < layer.length; i++) {
|
|
480
|
+
const node = layer[i];
|
|
481
|
+
const centres = (neighbours.get(node) ?? [])
|
|
482
|
+
.filter((n) => layout.layerOf.get(n) === adjacent)
|
|
483
|
+
.map(centreOf);
|
|
484
|
+
const desired = centres.length > 0
|
|
485
|
+
? median(centres) - (heightOf.get(node) as number) / 2
|
|
486
|
+
: topOf.get(node) as number;
|
|
487
|
+
offsets.push(offset);
|
|
488
|
+
targets.push(desired - offset);
|
|
489
|
+
if (i + 1 < layer.length) {
|
|
490
|
+
offset += (heightOf.get(node) as number) + gapAfter(node, layer[i + 1]);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const solved = isotonic(targets);
|
|
494
|
+
for (let i = 0; i < layer.length; i++) {
|
|
495
|
+
topOf.set(layer[i], solved[i] + offsets[i]);
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
for (let s = 0; s < placementSweeps; s++) {
|
|
500
|
+
if (s % 2 === 0) {
|
|
501
|
+
for (let i = 1; i < layout.layers.length; i++) {
|
|
502
|
+
place(layout.layers[i], up, i - 1);
|
|
503
|
+
}
|
|
504
|
+
} else {
|
|
505
|
+
for (let i = layout.layers.length - 2; i >= 0; i--) {
|
|
506
|
+
place(layout.layers[i], down, i + 1);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const widths = layout.layers.map((layer) =>
|
|
512
|
+
layer.reduce((widest, node) => node < 0 ? widest : Math.max(widest, (sizes.get(node) as Vector2).x), 0));
|
|
513
|
+
const xs = columnXs(widths);
|
|
514
|
+
|
|
515
|
+
const positions = new Map<number, Vector2>();
|
|
516
|
+
for (const node of piece) {
|
|
517
|
+
const layer = layout.layerOf.get(node) as number;
|
|
518
|
+
positions.set(node, {
|
|
519
|
+
x: xs[layer] + (widths[layer] - (sizes.get(node) as Vector2).x) / 2,
|
|
520
|
+
y: topOf.get(node) as number,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
return positions;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function layoutLoose(loose: Array<number>, sizes: Map<number, Vector2>): Map<number, Vector2> {
|
|
527
|
+
const columns = Math.ceil(Math.sqrt(loose.length));
|
|
528
|
+
const widths = new Array<number>(columns).fill(0);
|
|
529
|
+
const heights = new Array<number>(Math.ceil(loose.length / columns)).fill(0);
|
|
530
|
+
loose.forEach((node, i) => {
|
|
531
|
+
const size = sizes.get(node) as Vector2;
|
|
532
|
+
widths[i % columns] = Math.max(widths[i % columns], size.x);
|
|
533
|
+
heights[Math.floor(i / columns)] = Math.max(heights[Math.floor(i / columns)], size.y);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
const xs = new Array<number>();
|
|
537
|
+
let x = 0;
|
|
538
|
+
for (const width of widths) {
|
|
539
|
+
xs.push(x);
|
|
540
|
+
x += width + heightSpacing;
|
|
541
|
+
}
|
|
542
|
+
const ys = new Array<number>();
|
|
543
|
+
let y = 0;
|
|
544
|
+
for (const height of heights) {
|
|
545
|
+
ys.push(y);
|
|
546
|
+
y += height + heightSpacing;
|
|
175
547
|
}
|
|
548
|
+
|
|
549
|
+
const positions = new Map<number, Vector2>();
|
|
550
|
+
loose.forEach((node, i) => positions.set(node, { x: xs[i % columns], y: ys[Math.floor(i / columns)] }));
|
|
551
|
+
return positions;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
interface Block {
|
|
555
|
+
positions: Map<number, Vector2>;
|
|
556
|
+
size: Vector2;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function toBlock(positions: Map<number, Vector2>, sizes: Map<number, Vector2>): Block {
|
|
560
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
561
|
+
for (const [node, position] of positions) {
|
|
562
|
+
const size = sizes.get(node) as Vector2;
|
|
563
|
+
minX = Math.min(minX, position.x);
|
|
564
|
+
minY = Math.min(minY, position.y);
|
|
565
|
+
maxX = Math.max(maxX, position.x + size.x);
|
|
566
|
+
maxY = Math.max(maxY, position.y + size.y);
|
|
567
|
+
}
|
|
568
|
+
const shifted = new Map<number, Vector2>();
|
|
569
|
+
for (const [node, position] of positions) {
|
|
570
|
+
shifted.set(node, { x: position.x - minX, y: position.y - minY });
|
|
571
|
+
}
|
|
572
|
+
return { positions: shifted, size: { x: maxX - minX, y: maxY - minY } };
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Lays out each connected piece on its own and packs the pieces into rows,
|
|
577
|
+
* tallest first, with every unconnected node gathered into one grid at the
|
|
578
|
+
* end. Positions are top-left corners, with the whole layout starting at the
|
|
579
|
+
* origin.
|
|
580
|
+
*/
|
|
581
|
+
export function layoutGraph(
|
|
582
|
+
sorting: Array<number>,
|
|
583
|
+
upstream: Edges,
|
|
584
|
+
downstream: Edges,
|
|
585
|
+
sizes: Map<number, Vector2>,
|
|
586
|
+
): Map<number, Vector2> {
|
|
587
|
+
const blocks = new Array<Block>();
|
|
588
|
+
const loose = new Array<number>();
|
|
589
|
+
for (const piece of findPieces(sorting, upstream, downstream)) {
|
|
590
|
+
if (piece.length === 1) {
|
|
591
|
+
loose.push(piece[0]);
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
const root = treeRoot(piece, downstream);
|
|
595
|
+
const positions = root !== undefined
|
|
596
|
+
? layoutTree(root, upstream, sizes)
|
|
597
|
+
: layoutLayered(piece, upstream, downstream, sizes);
|
|
598
|
+
blocks.push(toBlock(positions, sizes));
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
blocks.sort((a, b) => b.size.y - a.size.y || b.size.x - a.size.x);
|
|
602
|
+
if (loose.length > 0) {
|
|
603
|
+
blocks.push(toBlock(layoutLoose(loose, sizes), sizes));
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
let area = 0;
|
|
607
|
+
let widest = 0;
|
|
608
|
+
for (const block of blocks) {
|
|
609
|
+
area += (block.size.x + pieceSpacing) * (block.size.y + pieceSpacing);
|
|
610
|
+
widest = Math.max(widest, block.size.x);
|
|
611
|
+
}
|
|
612
|
+
const rowWidth = Math.max(widest, Math.sqrt(area));
|
|
613
|
+
|
|
614
|
+
const positions = new Map<number, Vector2>();
|
|
615
|
+
let x = 0;
|
|
616
|
+
let y = 0;
|
|
617
|
+
let rowHeight = 0;
|
|
618
|
+
for (const block of blocks) {
|
|
619
|
+
if (x > 0 && x + block.size.x > rowWidth) {
|
|
620
|
+
x = 0;
|
|
621
|
+
y += rowHeight + pieceSpacing;
|
|
622
|
+
rowHeight = 0;
|
|
623
|
+
}
|
|
624
|
+
for (const [node, position] of block.positions) {
|
|
625
|
+
positions.set(node, { x: position.x + x, y: position.y + y });
|
|
626
|
+
}
|
|
627
|
+
x += block.size.x + pieceSpacing;
|
|
628
|
+
rowHeight = Math.max(rowHeight, block.size.y);
|
|
629
|
+
}
|
|
630
|
+
return positions;
|
|
176
631
|
}
|
|
177
632
|
|
|
178
633
|
export function Organize(ctx: CanvasRenderingContext2D, graph: NodeSubsystem, nodesToSort?: Array<number>): void {
|
|
@@ -183,9 +638,6 @@ export function Organize(ctx: CanvasRenderingContext2D, graph: NodeSubsystem, no
|
|
|
183
638
|
|
|
184
639
|
const sorting = new Array<number>();
|
|
185
640
|
if (nodesToSort) {
|
|
186
|
-
if (nodesToSort.length < 2) {
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
641
|
for (const index of nodesToSort) {
|
|
190
642
|
if (index >= 0 && index < nodes.length) {
|
|
191
643
|
sorting.push(index);
|
|
@@ -207,41 +659,31 @@ export function Organize(ctx: CanvasRenderingContext2D, graph: NodeSubsystem, no
|
|
|
207
659
|
}
|
|
208
660
|
|
|
209
661
|
const camera = new Camera();
|
|
210
|
-
const
|
|
662
|
+
const sizes = new Map<number, Vector2>();
|
|
211
663
|
for (const index of sorting) {
|
|
212
|
-
|
|
664
|
+
sizes.set(index, nodes[index].calculateBounds(ctx, camera).Size);
|
|
213
665
|
}
|
|
214
666
|
|
|
215
|
-
|
|
216
|
-
// must not be steered by nodes that are staying where they are.
|
|
217
|
-
const upstream = new Map<number, Array<number>>();
|
|
218
|
-
const downstream = new Map<number, Array<number>>();
|
|
667
|
+
const upstream: Edges = new Map();
|
|
219
668
|
for (const index of sorting) {
|
|
669
|
+
const node = nodes[index];
|
|
220
670
|
const feeders = new Array<number>();
|
|
221
|
-
for (
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
671
|
+
for (let i = 0; i < node.inputs(); i++) {
|
|
672
|
+
for (const connection of node.inputPort(i).connections()) {
|
|
673
|
+
const feeder = connection.outNode();
|
|
674
|
+
const feederIndex = feeder === null ? undefined : nodeLUT.get(feeder);
|
|
675
|
+
if (feederIndex !== undefined && inLayout.has(feederIndex) && feederIndex !== index && !feeders.includes(feederIndex)) {
|
|
676
|
+
feeders.push(feederIndex);
|
|
677
|
+
}
|
|
225
678
|
}
|
|
226
679
|
}
|
|
227
680
|
upstream.set(index, feeders);
|
|
228
|
-
|
|
229
|
-
const consumers = new Array<number>();
|
|
230
|
-
for (const consumer of graph.connectedOutputsNodeReferences(index)) {
|
|
231
|
-
const consumerIndex = nodeLUT.get(consumer);
|
|
232
|
-
if (consumerIndex !== undefined && inLayout.has(consumerIndex) && consumerIndex !== index) {
|
|
233
|
-
consumers.push(consumerIndex);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
downstream.set(index, consumers);
|
|
237
681
|
}
|
|
682
|
+
const downstream = consumersOf(sorting, upstream);
|
|
238
683
|
|
|
239
|
-
const
|
|
240
|
-
orderWithinLayers(layout, upstream, downstream);
|
|
684
|
+
const positions = layoutGraph(sorting, upstream, downstream, sizes);
|
|
241
685
|
|
|
242
|
-
|
|
243
|
-
// selection doesn't fling it off to wherever the origin happens to be.
|
|
244
|
-
let anchor: Vector2 = { x: 0, y: 0 };
|
|
686
|
+
const anchor: Vector2 = { x: 0, y: 0 };
|
|
245
687
|
for (const index of sorting) {
|
|
246
688
|
const position = nodes[index].getPosition();
|
|
247
689
|
anchor.x += position.x;
|
|
@@ -250,27 +692,14 @@ export function Organize(ctx: CanvasRenderingContext2D, graph: NodeSubsystem, no
|
|
|
250
692
|
anchor.x /= sorting.length;
|
|
251
693
|
anchor.y /= sorting.length;
|
|
252
694
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const tallest = layerHeights.reduce((tallest, height) => Math.max(tallest, height), 0);
|
|
260
|
-
|
|
261
|
-
// Inputs on the left, consumers to their right, every layer centered
|
|
262
|
-
// against the tallest one so the graph reads as a band rather than
|
|
263
|
-
// hanging off a single corner.
|
|
264
|
-
let x = anchor.x - (totalWidth / 2);
|
|
265
|
-
for (let i = 0; i < layout.layers.length; i++) {
|
|
266
|
-
let y = anchor.y - (tallest / 2) + ((tallest - layerHeights[i]) / 2);
|
|
267
|
-
|
|
268
|
-
for (const node of layout.layers[i]) {
|
|
269
|
-
const size = (bounds.get(node) as Box).Size;
|
|
270
|
-
nodes[node].setPosition({ x: x + ((layerWidths[i] - size.x) / 2), y: y });
|
|
271
|
-
y += size.y + heightSpacing;
|
|
272
|
-
}
|
|
695
|
+
let right = 0;
|
|
696
|
+
let bottom = 0;
|
|
697
|
+
for (const [index, position] of positions) {
|
|
698
|
+
right = Math.max(right, position.x + (sizes.get(index) as Vector2).x);
|
|
699
|
+
bottom = Math.max(bottom, position.y + (sizes.get(index) as Vector2).y);
|
|
700
|
+
}
|
|
273
701
|
|
|
274
|
-
|
|
702
|
+
for (const [index, position] of positions) {
|
|
703
|
+
nodes[index].setPosition({ x: anchor.x - right / 2 + position.x, y: anchor.y - bottom / 2 + position.y });
|
|
275
704
|
}
|
|
276
705
|
}
|