@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.
Files changed (55) hide show
  1. package/dist/index.d.mts +386 -227
  2. package/dist/index.d.ts +386 -227
  3. package/dist/index.js +1884 -1076
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +1867 -1075
  6. package/dist/index.mjs.map +1 -1
  7. package/index.html +1134 -329
  8. package/package.json +1 -1
  9. package/src/connection.ts +30 -5
  10. package/src/contextMenu.ts +66 -34
  11. package/src/graph.ts +12 -10
  12. package/src/index.ts +26 -12
  13. package/src/markdown/entry.ts +11 -15
  14. package/src/markdown/syntaxParser.ts +2 -1
  15. package/src/node.ts +162 -111
  16. package/src/nodes/subsystem.ts +123 -29
  17. package/src/notes/note.ts +9 -1
  18. package/src/organize.ts +540 -111
  19. package/src/popup.ts +4 -2
  20. package/src/port.ts +83 -6
  21. package/src/styles/bevel.ts +31 -0
  22. package/src/styles/cursor.ts +1 -0
  23. package/src/styles/text.ts +4 -4
  24. package/src/theme.ts +179 -92
  25. package/src/types/text.ts +5 -1
  26. package/src/utils/color.ts +1 -1
  27. package/src/widgets/button.ts +52 -107
  28. package/src/widgets/color.ts +91 -150
  29. package/src/widgets/factory.ts +89 -46
  30. package/src/widgets/field.ts +99 -0
  31. package/src/widgets/image.ts +107 -165
  32. package/src/widgets/inplace.ts +84 -0
  33. package/src/widgets/number.ts +122 -154
  34. package/src/widgets/row.ts +138 -0
  35. package/src/widgets/slider.ts +101 -216
  36. package/src/widgets/string.ts +39 -130
  37. package/src/widgets/text.ts +81 -92
  38. package/src/widgets/toggle.ts +62 -207
  39. package/src/widgets/value.ts +127 -0
  40. package/src/widgets/widget.ts +180 -34
  41. package/test/context_menu.test.ts +82 -0
  42. package/test/cursor.test.ts +17 -0
  43. package/test/hidden_port.test.ts +125 -0
  44. package/test/hover_after_removal.test.ts +72 -0
  45. package/test/markdown.test.ts +2 -2
  46. package/test/markdown_code_block.test.ts +58 -0
  47. package/test/multi_input.test.ts +124 -0
  48. package/test/organize.test.ts +117 -1
  49. package/test/property_binding.test.ts +49 -0
  50. package/test/support.ts +175 -0
  51. package/test/theme.test.ts +86 -0
  52. package/test/widget_click.test.ts +111 -97
  53. package/test/widget_construction.test.ts +5 -4
  54. package/test/widgets.test.ts +333 -0
  55. 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
- * Orders the nodes so every edge points from an earlier node to a later one,
26
- * as far as that is possible. Edges that would point backwards - the ones
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 feeders = upstream.get(top.node);
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 one layer to the right of the furthest node feeding it, so
74
- * a node never sits left of something it consumes. Taking the furthest and
75
- * not the first is what keeps a long chain and a short chain that rejoin
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: Map<number, Array<number>>): Layout {
79
- const order = topologicalOrder(sorting, upstream);
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 layerOf = new Map<number, number>();
84
+ const rank = new Map<number, number>();
82
85
 
83
86
  for (const node of order) {
84
- let layer = 0;
85
- for (const feeder of upstream.get(node) ?? []) {
86
- // Only feeders already placed: anything else is a back edge, and
87
- // following it would mean waiting on a node that waits on this one.
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
- layerOf.set(node, layer);
93
+ rank.set(node, r);
93
94
  placed.add(node);
94
95
  }
95
96
 
96
97
  let deepest = 0;
97
- for (const layer of layerOf.values()) {
98
- deepest = Math.max(deepest, layer);
98
+ for (const r of rank.values()) {
99
+ deepest = Math.max(deepest, r);
99
100
  }
100
101
 
101
- const layers = new Array<Array<number>>(deepest + 1);
102
- for (let i = 0; i < layers.length; i++) {
103
- layers[i] = new Array<number>();
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
- for (const node of order) {
106
- layers[layerOf.get(node) as number].push(node);
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
- * by repeatedly moving every node to the average position of its neighbours
115
- * in the layer before it, then the layer after it. This is what stops the
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 recordSlots = (): void => {
125
- for (const layer of layout.layers) {
126
- for (let i = 0; i < layer.length; i++) {
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: Map<number, Array<number>>, layer: number): number => {
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: Map<number, Array<number>>, towards: number): void => {
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
- for (let i = 0; i < orderingSweeps; i++) {
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 bounds = new Map<number, Box>();
662
+ const sizes = new Map<number, Vector2>();
211
663
  for (const index of sorting) {
212
- bounds.set(index, nodes[index].calculateBounds(ctx, camera));
664
+ sizes.set(index, nodes[index].calculateBounds(ctx, camera).Size);
213
665
  }
214
666
 
215
- // Edges, restricted to what is being laid out. Organizing a selection
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 (const feeder of graph.connectedInputsNodeReferencesByIndex(index)) {
222
- const feederIndex = nodeLUT.get(feeder);
223
- if (feederIndex !== undefined && inLayout.has(feederIndex) && feederIndex !== index) {
224
- feeders.push(feederIndex);
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 layout = assignLayers(sorting, upstream);
240
- orderWithinLayers(layout, upstream, downstream);
684
+ const positions = layoutGraph(sorting, upstream, downstream, sizes);
241
685
 
242
- // Keep the result where the nodes already were, so organizing a
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
- const layerWidths = layout.layers.map(layer =>
254
- layer.reduce((widest, node) => Math.max(widest, (bounds.get(node) as Box).Size.x), 0));
255
- const layerHeights = layout.layers.map(layer =>
256
- layer.reduce((total, node) => total + (bounds.get(node) as Box).Size.y + heightSpacing, -heightSpacing));
257
-
258
- const totalWidth = layerWidths.reduce((total, width) => total + width + widthSpacing, -widthSpacing);
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
- x += layerWidths[i] + widthSpacing;
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
  }