@davidsouther/agathon 0.0.0-stage → 0.0.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/package.json +19 -4
- package/src/assert.ts +67 -0
- package/src/geometry/box.ts +76 -0
- package/src/geometry/circle.ts +296 -0
- package/src/geometry/geometry.ts +318 -0
- package/src/geometry/path.ts +315 -0
- package/src/geometry/quadtree.ts +321 -0
- package/src/geometry/rectangle.ts +129 -0
- package/src/geometry/rngon.ts +132 -0
- package/src/geometry/types.ts +102 -0
- package/src/geometry.ts +61 -0
- package/src/highlight/highlight.ts +85 -0
- package/src/index.ts +5 -0
- package/src/layout/grid/manager.ts +55 -0
- package/src/layout/grid.ts +1 -0
- package/src/layout/hierarchic/README.md +21 -0
- package/src/layout/hierarchic/acyclic.ts +249 -0
- package/src/layout/hierarchic/children.ts +169 -0
- package/src/layout/hierarchic/debug.ts +186 -0
- package/src/layout/hierarchic/layout.ts +266 -0
- package/src/layout/hierarchic/manager.ts +65 -0
- package/src/layout/hierarchic/normalize.ts +177 -0
- package/src/layout/hierarchic/order.ts +321 -0
- package/src/layout/hierarchic/position.ts +514 -0
- package/src/layout/hierarchic/rank.ts +629 -0
- package/src/layout/hierarchic.ts +2 -0
- package/src/layout/hulls/arrange.ts +94 -0
- package/src/layout/hulls/hulls.ts +243 -0
- package/src/layout/hulls/segments.ts +174 -0
- package/src/layout/hulls.ts +3 -0
- package/src/layout/manager/child_manager.ts +42 -0
- package/src/layout/manager/drag_manager.ts +112 -0
- package/src/layout/manager/manager.ts +425 -0
- package/src/layout/manager/pan_manager.ts +146 -0
- package/src/layout/manager/visibility_manager.ts +84 -0
- package/src/layout/manager/zoom_manager.ts +263 -0
- package/src/layout/manager.ts +6 -0
- package/src/layout/properties/defaults.ts +99 -0
- package/src/layout/properties/symbol.ts +99 -0
- package/src/layout/properties/weak_map.ts +73 -0
- package/src/layout/properties.ts +111 -0
- package/src/layout/types.ts +470 -0
- package/src/layout/util.ts +444 -0
- package/src/layout.ts +6 -0
- package/src/samples/diff_graph.ts +15 -0
- package/src/samples/edge_cases.ts +168 -0
- package/src/samples/gansner_with_states.ts +67 -0
- package/src/samples/load_generator.ts +213 -0
- package/src/samples/search_graph.ts +35 -0
- package/src/tsabre/algo.ts +126 -0
- package/src/tsabre/builder.ts +97 -0
- package/src/tsabre/compress.ts +15 -0
- package/src/tsabre/dijkstra/dijkstra.ts +76 -0
- package/src/tsabre/dijkstra/heap.ts +83 -0
- package/src/tsabre/graph.ts +808 -0
- package/src/tsabre/parse.ts +634 -0
- package/src/tsabre/print.ts +188 -0
- package/src/tsabre/sourcebuilder/code_content_buffer.ts +275 -0
- package/src/tsabre/sourcebuilder/printable.ts +33 -0
- package/src/tsabre.ts +6 -0
- package/src/types.ts +424 -0
- package/README.md +0 -3
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
import { assert, assertExists } from "../../assert.ts";
|
|
2
|
+
import { RNGon } from "../../geometry.ts";
|
|
3
|
+
import { TsabreGraph } from "../../tsabre/graph.ts";
|
|
4
|
+
import type { FullGraph, NodeId } from "../../types.ts";
|
|
5
|
+
import * as util from "../util.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* An alignment marks a set of blocks (vertical chains of edges) and classes
|
|
9
|
+
* (horizontal groupings of blocks).
|
|
10
|
+
*/
|
|
11
|
+
export interface Alignment {
|
|
12
|
+
root: Record<NodeId, NodeId>;
|
|
13
|
+
align: Record<NodeId, NodeId>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A shorthand for tracking id to number. */
|
|
17
|
+
export type Coordinates = Record<NodeId, number>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The coordinates of each node, from each of the diaganols.
|
|
21
|
+
*/
|
|
22
|
+
export interface CoordinateBounds {
|
|
23
|
+
ul: Coordinates;
|
|
24
|
+
ur: Coordinates;
|
|
25
|
+
dl: Coordinates;
|
|
26
|
+
dr: Coordinates;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Given a graph with nodes' rank and order filled in, determine their relative
|
|
31
|
+
* position, obeying size & label constraints. Y positions are trivially easy-
|
|
32
|
+
* all nodes with the same rank have the same Y value, and the next Y value is
|
|
33
|
+
* the prior Y value plus half the max node height of the current row plus half
|
|
34
|
+
* the max node height of the next row plus the graph's rankSep.
|
|
35
|
+
*
|
|
36
|
+
* X coordinates are much more complex, and lean heavily on the "balance and
|
|
37
|
+
* symmetry" aesthetic. Constraints to consider are balancing whitespace, length
|
|
38
|
+
* and straightness of edges, and balancing edges incident on one node. Given
|
|
39
|
+
* that vertical placement is fixed by ranks, above, X positions indeed only
|
|
40
|
+
* lay out in the horizontal direction.
|
|
41
|
+
*
|
|
42
|
+
* See [Brandes and Koepf] in docs/tsabre for more details.
|
|
43
|
+
*/
|
|
44
|
+
export function position(g: FullGraph): void {
|
|
45
|
+
const layerMatrix = util.buildLayerMatrix(g);
|
|
46
|
+
|
|
47
|
+
const ys = positionY(g, layerMatrix);
|
|
48
|
+
const xs = positionX(g, layerMatrix);
|
|
49
|
+
|
|
50
|
+
for (const node of g.allNodes) {
|
|
51
|
+
const x = xs[node.id] ?? 0;
|
|
52
|
+
const y = ys[node.id] ?? 0;
|
|
53
|
+
util.getNodeProps(node).geometry.moveTo({ x, y });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Calculate aesthetically pleasing X coordinates for nodes, given a SmartGraph
|
|
59
|
+
* with nodes having `order` and `rank` on their layout, as well as having
|
|
60
|
+
* virtual nodes indicating segments of multi-rank edge spans. Node layout
|
|
61
|
+
* should keep conceptual "columns" grouped, that is, keeping edges as vertical
|
|
62
|
+
* and as short as possible.
|
|
63
|
+
*/
|
|
64
|
+
function positionX(g: FullGraph, layers: NodeId[][]): Coordinates {
|
|
65
|
+
const conflicts = findType1Conflicts(g, layers);
|
|
66
|
+
|
|
67
|
+
// Positioning layouts have strong directional affinities. To mitigate this,
|
|
68
|
+
// we calculate all 4 layouts, and then take the median positions for the
|
|
69
|
+
// final balanced position. See [Brandes and Kopf 4.3].
|
|
70
|
+
const direction = {
|
|
71
|
+
ul: [...layers],
|
|
72
|
+
ur: [...layers].map((l) => [...l].reverse()),
|
|
73
|
+
dl: [...layers].reverse(),
|
|
74
|
+
dr: [...layers].map((l) => [...l].reverse()).reverse(),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const xss: CoordinateBounds = {
|
|
78
|
+
ul: makeBounds(direction.ul, (id: NodeId) => g.predecessors(id), false),
|
|
79
|
+
ur: makeBounds(direction.ur, (id: NodeId) => g.predecessors(id), true),
|
|
80
|
+
dl: makeBounds(direction.dl, (id: NodeId) => g.successors(id), false),
|
|
81
|
+
dr: makeBounds(direction.dr, (id: NodeId) => g.successors(id), true),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const smallestWidth = findSmallestWidthAlignment(g, xss);
|
|
85
|
+
alignCoordinates(xss, smallestWidth);
|
|
86
|
+
return balance(xss);
|
|
87
|
+
|
|
88
|
+
function makeBounds(
|
|
89
|
+
layers: NodeId[][],
|
|
90
|
+
neighborFn: (id: NodeId) => Set<NodeId>,
|
|
91
|
+
correction: boolean,
|
|
92
|
+
) {
|
|
93
|
+
const align = verticalAlignment(layers, conflicts, neighborFn);
|
|
94
|
+
const xs = horizontalCompaction(g, layers, align);
|
|
95
|
+
|
|
96
|
+
if (correction) {
|
|
97
|
+
for (const [id, x] of Object.entries(xs)) {
|
|
98
|
+
xs[id] = -x;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return xs;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function positionY(g: FullGraph, layers: NodeId[][]): Coordinates {
|
|
106
|
+
const coordinates: Coordinates = {};
|
|
107
|
+
const { rankSep, staggerRows } = util.getGraphProps(g);
|
|
108
|
+
assert(staggerRows > 0);
|
|
109
|
+
// It's easiest to assign y position as the middle of the row + row sep + the
|
|
110
|
+
// middle of the next row. To normalize back to y=0 for the first row,
|
|
111
|
+
// subtract that first middle row height.
|
|
112
|
+
// All empty/undefined (virtual) nodes in layers are filtered out in
|
|
113
|
+
// buildLayerMatrix.
|
|
114
|
+
let prevY =
|
|
115
|
+
-0.5 * Math.max(...layers[0].map((id) => util.getNodeProps(g.node(id)!).geometry.height));
|
|
116
|
+
for (const layer of layers) {
|
|
117
|
+
const height = util.staggerRow(g, layer, coordinates, staggerRows);
|
|
118
|
+
for (const id of layer) {
|
|
119
|
+
coordinates[id] += prevY;
|
|
120
|
+
}
|
|
121
|
+
prevY += height + rankSep;
|
|
122
|
+
}
|
|
123
|
+
return coordinates;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Conflicts track which other nodes a node has conflicting edges with.
|
|
128
|
+
* Conflicts occur when two edge segments cross or share a vertex.
|
|
129
|
+
*
|
|
130
|
+
* a b
|
|
131
|
+
* X
|
|
132
|
+
* c d
|
|
133
|
+
*
|
|
134
|
+
* a conflicts with c and d, b conflicts with c and d,
|
|
135
|
+
*/
|
|
136
|
+
type Conflicts = Map<NodeId, Set<NodeId>>;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Type 1 conflicts are when a non-inner segment crosses an inner segment. An
|
|
140
|
+
* inner segment is one on which both incident nodes are marked `virtual`, and
|
|
141
|
+
* represents a portion of a longer edge which crosses multiple ranks. Inner
|
|
142
|
+
* segments should be kept vertical; otherwise, long edges will be "jagged".
|
|
143
|
+
*
|
|
144
|
+
* We scan layer by layer, starting at the second, looking for type-1 conflicts
|
|
145
|
+
* between the current and prior layer.
|
|
146
|
+
*/
|
|
147
|
+
function findType1Conflicts(g: FullGraph, layers: NodeId[][]): Conflicts {
|
|
148
|
+
const conflicts = new Map<NodeId, Set<NodeId>>();
|
|
149
|
+
|
|
150
|
+
for (let i = 1; i < layers.length; i++) {
|
|
151
|
+
const layer = layers[i];
|
|
152
|
+
const prev = layers[i - 1];
|
|
153
|
+
|
|
154
|
+
let k0 = 0;
|
|
155
|
+
let scanPos = 0;
|
|
156
|
+
const prevLength = prev.length;
|
|
157
|
+
|
|
158
|
+
for (let j = 0; j < layer.length; j++) {
|
|
159
|
+
const id = layer[j];
|
|
160
|
+
|
|
161
|
+
const target = findOtherInnerSegmentNode(g, id);
|
|
162
|
+
const k1 = target ? util.getNodeProps(g.node(target)!).order : prevLength;
|
|
163
|
+
|
|
164
|
+
if (target || id === layer[layer.length - 1]) {
|
|
165
|
+
for (const scanNodeId of layer.slice(scanPos, j + 1)) {
|
|
166
|
+
const scanNode = g.node(scanNodeId);
|
|
167
|
+
|
|
168
|
+
if (!scanNode) continue;
|
|
169
|
+
|
|
170
|
+
const vScanNode = util.getNodeProps(scanNode).virtual;
|
|
171
|
+
for (const pred of g.predecessors(scanNodeId)) {
|
|
172
|
+
const { order, virtual } = util.getNodeProps(g.node(pred)!);
|
|
173
|
+
|
|
174
|
+
const hasVScanNode = vScanNode !== false;
|
|
175
|
+
const hasVirtual = virtual !== false;
|
|
176
|
+
|
|
177
|
+
if ((order < k0 || k1 < order) && !(hasVirtual && hasVScanNode)) {
|
|
178
|
+
addConflict(conflicts, pred, scanNodeId);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
scanPos = j + 1;
|
|
183
|
+
k0 = k1;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return conflicts;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function findOtherInnerSegmentNode(g: FullGraph, id: NodeId): NodeId | undefined {
|
|
192
|
+
const node = g.node(id);
|
|
193
|
+
if (node && util.getNodeProps(node).virtual !== false) {
|
|
194
|
+
return Array.from(g.predecessors(id)).find(
|
|
195
|
+
(id) => util.getNodeProps(g.node(id)!).virtual !== false,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function sortIds(a: NodeId, b: NodeId): [NodeId, NodeId] {
|
|
202
|
+
// Normalize source and target alphabetically
|
|
203
|
+
if (a > b) {
|
|
204
|
+
[b, a] = [a, b];
|
|
205
|
+
}
|
|
206
|
+
return [a, b];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Mark a conflict between any source/target pair. */
|
|
210
|
+
function addConflict(conflicts: Conflicts, a: NodeId, b: NodeId): void {
|
|
211
|
+
const [source, target] = sortIds(a, b);
|
|
212
|
+
const conflictSet = conflicts.get(source) || new Set<NodeId>();
|
|
213
|
+
conflicts.set(source, conflictSet);
|
|
214
|
+
conflictSet.add(target);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Check if there is a conflict between any source/target pair. */
|
|
218
|
+
function hasConflict(conflicts: Conflicts, a: NodeId, b: NodeId): boolean {
|
|
219
|
+
const [source, target] = sortIds(a, b);
|
|
220
|
+
if (!conflicts.has(source)) return false;
|
|
221
|
+
return conflicts.get(source)!.has(target);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Determine vertical alignment among nodes by finding the median positions of a
|
|
226
|
+
*
|
|
227
|
+
* See [Brandes and Kopf 4.1].
|
|
228
|
+
*/
|
|
229
|
+
function verticalAlignment(
|
|
230
|
+
layers: NodeId[][],
|
|
231
|
+
conflicts: Conflicts,
|
|
232
|
+
neighbors: (id: NodeId) => Set<NodeId>,
|
|
233
|
+
): Alignment {
|
|
234
|
+
// The root mapping will point to the node's neighbor to the left.
|
|
235
|
+
const root: Record<NodeId, NodeId> = {};
|
|
236
|
+
// The align mapping will point to the node's neighbor to the right.
|
|
237
|
+
const align: Record<NodeId, NodeId> = {};
|
|
238
|
+
const pos: Coordinates = {};
|
|
239
|
+
|
|
240
|
+
for (const layer of layers) {
|
|
241
|
+
for (let i = 0; i < layer.length; i++) {
|
|
242
|
+
const id = layer[i];
|
|
243
|
+
root[id] = id;
|
|
244
|
+
align[id] = id;
|
|
245
|
+
pos[id] = i;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
for (const layer of layers) {
|
|
250
|
+
let prevIdx = -1;
|
|
251
|
+
for (const id of layer) {
|
|
252
|
+
const ns = neighbors(id);
|
|
253
|
+
if (ns.size > 0) {
|
|
254
|
+
const ids = Array.from(ns).sort((a, b) => pos[a] - pos[b]);
|
|
255
|
+
// Update the median point
|
|
256
|
+
const mp = (ids.length - 1) / 2;
|
|
257
|
+
for (let i = Math.floor(mp), j = Math.ceil(mp); i <= j; i++) {
|
|
258
|
+
const targetId = ids[i];
|
|
259
|
+
if (
|
|
260
|
+
align[id] === id &&
|
|
261
|
+
prevIdx < pos[targetId] &&
|
|
262
|
+
!hasConflict(conflicts, id, targetId)
|
|
263
|
+
) {
|
|
264
|
+
align[targetId] = id;
|
|
265
|
+
align[id] = root[id] = root[targetId];
|
|
266
|
+
prevIdx = pos[targetId];
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return { root, align };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Given a vertical alignment, shrink it as tight as possible. This shrinking
|
|
278
|
+
* is facilitated by a block graph, that is, a graph that points each node at
|
|
279
|
+
* its predecessor on a rank. See [Brandes and Kopf 4.2].
|
|
280
|
+
*/
|
|
281
|
+
function horizontalCompaction(
|
|
282
|
+
graph: FullGraph,
|
|
283
|
+
layers: NodeId[][],
|
|
284
|
+
alignment: Alignment,
|
|
285
|
+
): Coordinates {
|
|
286
|
+
const xs: Coordinates = {};
|
|
287
|
+
const { root, align } = alignment;
|
|
288
|
+
|
|
289
|
+
const blockG = buildBlockGraph();
|
|
290
|
+
|
|
291
|
+
iterate(pass1, (id) => blockG.predecessors(id));
|
|
292
|
+
iterate(pass2, (id) => blockG.successors(id));
|
|
293
|
+
|
|
294
|
+
for (const id of Object.keys(align)) {
|
|
295
|
+
xs[id] = xs[root[id]];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return xs;
|
|
299
|
+
|
|
300
|
+
/* Set x values to the maximum incoming edge weights, pushing the node to the
|
|
301
|
+
* far right of the vertical block. */
|
|
302
|
+
function pass1(id: NodeId) {
|
|
303
|
+
const edgeWeights = Array.from(blockG.inEdges(id)).map(
|
|
304
|
+
(e) => (xs[e.source] || 0) + blockG.edge(e)!.separation,
|
|
305
|
+
);
|
|
306
|
+
xs[id] = edgeWeights.reduce((max, e) => Math.max(max, e), 0);
|
|
307
|
+
if (isNaN(xs[id])) {
|
|
308
|
+
// This is a serious error that is unclear how best to handle. In lieu of
|
|
309
|
+
// a robust understanding of the problem, just shunt the node far-left.
|
|
310
|
+
// If this is happening, drop a debugger here and take a look.
|
|
311
|
+
xs[id] = -1;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* Set x values to the minimum outgoing edge weights, pulling the node to the
|
|
316
|
+
* far left of the vertical block. */
|
|
317
|
+
function pass2(id: NodeId) {
|
|
318
|
+
const MAX = Number.POSITIVE_INFINITY;
|
|
319
|
+
const edgeWeights = Array.from(blockG.outEdges(id)).map(
|
|
320
|
+
(e) => xs[e.target] - blockG.edge(e)!.separation,
|
|
321
|
+
);
|
|
322
|
+
const min = edgeWeights.reduce((min, e) => Math.min(min, e), MAX);
|
|
323
|
+
if (min !== MAX) {
|
|
324
|
+
xs[id] = Math.max(xs[id], min);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Utility to apply a function across the blockG nodes
|
|
329
|
+
function iterate(setXs: (id: NodeId) => void, nextNodes: (id: NodeId) => Set<NodeId>) {
|
|
330
|
+
const stack = Array.from(blockG.nodes);
|
|
331
|
+
const visited = new Set<NodeId>();
|
|
332
|
+
while (stack.length > 0) {
|
|
333
|
+
const id = stack.pop()!;
|
|
334
|
+
if (visited.has(id)) {
|
|
335
|
+
setXs(id);
|
|
336
|
+
} else {
|
|
337
|
+
visited.add(id);
|
|
338
|
+
stack.push(id, ...nextNodes(id));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function buildBlockGraph() {
|
|
344
|
+
const blockGraph = new TsabreGraph<
|
|
345
|
+
{},
|
|
346
|
+
{ source: NodeId; target: NodeId; separation: number }
|
|
347
|
+
>();
|
|
348
|
+
const { nodeSep, edgeSep } = util.getGraphProps(graph);
|
|
349
|
+
|
|
350
|
+
for (const layer of layers) {
|
|
351
|
+
let tId: NodeId | undefined;
|
|
352
|
+
for (const sId of layer) {
|
|
353
|
+
const sRoot = root[sId];
|
|
354
|
+
blockGraph.setNode(sRoot, {});
|
|
355
|
+
if (tId) {
|
|
356
|
+
const tRoot = root[tId];
|
|
357
|
+
const ts = {
|
|
358
|
+
source: tRoot,
|
|
359
|
+
target: sRoot,
|
|
360
|
+
separation: separation(tId, sId),
|
|
361
|
+
};
|
|
362
|
+
const tsEdge = blockGraph.edge(ts);
|
|
363
|
+
if (tsEdge) {
|
|
364
|
+
ts.separation = Math.max(ts.separation, tsEdge.separation);
|
|
365
|
+
}
|
|
366
|
+
blockGraph.setEdge(ts, ts);
|
|
367
|
+
}
|
|
368
|
+
tId = sId;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return blockGraph;
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* calculate the horizontal separation between two nodes in a graph.
|
|
376
|
+
* Horizontal separation is half of the width of each node, plus nodeSep,
|
|
377
|
+
* plus edgeSep for any virtual nodes created to break up long edges.
|
|
378
|
+
*/
|
|
379
|
+
function separation(sid: NodeId, tid: NodeId): number {
|
|
380
|
+
const sNode = graph.node(sid);
|
|
381
|
+
const tNode = graph.node(tid);
|
|
382
|
+
|
|
383
|
+
// Occasionally, there are virtual nodes in the layers, that are used to
|
|
384
|
+
// build node-connecting lines, but don't take any space (0 width &
|
|
385
|
+
// height).
|
|
386
|
+
const virtualNodePlaceholder = {
|
|
387
|
+
width: 0,
|
|
388
|
+
virtual: true,
|
|
389
|
+
geometry: RNGon.point(),
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const sLayout = sNode ? util.getNodeProps(sNode) : virtualNodePlaceholder;
|
|
393
|
+
const tLayout = tNode ? util.getNodeProps(tNode) : virtualNodePlaceholder;
|
|
394
|
+
|
|
395
|
+
let sum = 0;
|
|
396
|
+
sum += sLayout.geometry.width;
|
|
397
|
+
sum += sLayout.virtual ? edgeSep : nodeSep;
|
|
398
|
+
sum += tLayout.virtual ? edgeSep : nodeSep;
|
|
399
|
+
sum += tLayout.geometry.width;
|
|
400
|
+
|
|
401
|
+
return sum / 2;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Arrange the coordinates of each of the layout alignment bounds such that
|
|
408
|
+
* left-biased alignments have their minimum coordinate at the same point as
|
|
409
|
+
* the minimum coordinate of the smallest width alignment and right-biased
|
|
410
|
+
* alignments have their maximum coordinate at the same point as the maximum
|
|
411
|
+
* coordinate of the smallest width alignment.
|
|
412
|
+
*/
|
|
413
|
+
function alignCoordinates(xss: CoordinateBounds, alignTo: Coordinates): void {
|
|
414
|
+
const alignToVals = Object.values(alignTo);
|
|
415
|
+
const alignToMin = Math.min(...alignToVals);
|
|
416
|
+
const alignToMax = Math.max(...alignToVals);
|
|
417
|
+
|
|
418
|
+
function updateCoords(xs: Coordinates, left: boolean) {
|
|
419
|
+
if (xs === alignTo) return;
|
|
420
|
+
const vals = Object.values(xs) || {};
|
|
421
|
+
const delta = left ? alignToMin - Math.min(...vals) : alignToMax - Math.max(...vals);
|
|
422
|
+
for (const id of Object.keys(xs)) {
|
|
423
|
+
xs[id] += delta;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
updateCoords(xss.ur, false);
|
|
428
|
+
updateCoords(xss.ul, true);
|
|
429
|
+
updateCoords(xss.dr, false);
|
|
430
|
+
updateCoords(xss.dl, true);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function getAlignment(bounds: CoordinateBounds, v: "u" | "d", h: "l" | "r") {
|
|
434
|
+
if (v === "u" && h === "l") return bounds.ul;
|
|
435
|
+
if (v === "u" && h === "r") return bounds.ur;
|
|
436
|
+
if (v === "d" && h === "l") return bounds.dl;
|
|
437
|
+
if (v === "d" && h === "r") return bounds.dr;
|
|
438
|
+
throw new Error(`Invalid alignment: ${v}${h}`);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Given a set of bounds, find the one with the smallest layout width.
|
|
443
|
+
*/
|
|
444
|
+
function findSmallestWidthAlignment(g: FullGraph, bounds: CoordinateBounds): Coordinates {
|
|
445
|
+
let best = Number.POSITIVE_INFINITY;
|
|
446
|
+
let bestCoords: Coordinates | undefined;
|
|
447
|
+
|
|
448
|
+
for (const coords of Object.values(bounds)) {
|
|
449
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
450
|
+
let min = Number.POSITIVE_INFINITY;
|
|
451
|
+
|
|
452
|
+
const entries: Array<[NodeId, number]> = Object.entries(coords);
|
|
453
|
+
for (const [id, x] of entries) {
|
|
454
|
+
const node = g.node(id);
|
|
455
|
+
if (!node) continue;
|
|
456
|
+
|
|
457
|
+
const halfWidth = util.getNodeProps(node).geometry.width / 2;
|
|
458
|
+
|
|
459
|
+
max = Math.max(x + halfWidth, max);
|
|
460
|
+
min = Math.min(x - halfWidth, min);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const nextBest = max - min;
|
|
464
|
+
if (nextBest < best) {
|
|
465
|
+
best = nextBest;
|
|
466
|
+
bestCoords = coords;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return assertExists(bestCoords, "No best coordinates possible");
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Layouts are created in 4 diagonal directions. Each biases sources to the
|
|
475
|
+
* corresponding corners. To even this out, a final balancing placed each node
|
|
476
|
+
* at the median of its 4 computed positions.
|
|
477
|
+
*/
|
|
478
|
+
function balance(xss: CoordinateBounds, align?: ["u" | "d", "l" | "r"]): Coordinates {
|
|
479
|
+
const balanced: Coordinates = {};
|
|
480
|
+
let minX = Number.MAX_VALUE;
|
|
481
|
+
for (const id of Object.keys(xss.ul)) {
|
|
482
|
+
if (align) {
|
|
483
|
+
balanced[id] = getAlignment(xss, align[0], align[1])[id];
|
|
484
|
+
minX = Math.min(minX, balanced[id]);
|
|
485
|
+
} else {
|
|
486
|
+
const xs = Object.values(xss)
|
|
487
|
+
.map((c) => c[id])
|
|
488
|
+
.sort((a, b) => a - b) as [number, number, number, number];
|
|
489
|
+
// Get the median value of the 4 entries
|
|
490
|
+
balanced[id] = (xs[1] + xs[2]) / 2;
|
|
491
|
+
minX = Math.min(minX, balanced[id]);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Normalize all X values, so that minimum value is set to 0. Prevents the
|
|
496
|
+
// graph from shifting around during positioning, by fixing the left side
|
|
497
|
+
for (const id of Object.keys(balanced)) {
|
|
498
|
+
balanced[id] -= minX;
|
|
499
|
+
}
|
|
500
|
+
return balanced;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export const TEST_ONLY = {
|
|
504
|
+
addConflict,
|
|
505
|
+
alignCoordinates,
|
|
506
|
+
balance,
|
|
507
|
+
findSmallestWidthAlignment,
|
|
508
|
+
findType1Conflicts,
|
|
509
|
+
hasConflict,
|
|
510
|
+
horizontalCompaction,
|
|
511
|
+
positionX,
|
|
512
|
+
positionY,
|
|
513
|
+
verticalAlignment,
|
|
514
|
+
};
|