@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,94 @@
|
|
|
1
|
+
import { getPositionOnCircle, smallestIntersectingRadius } from "../../geometry/circle.ts";
|
|
2
|
+
import type { NodeCircle, NodeId, NodeRadius, Point } from "../../types.ts";
|
|
3
|
+
import type { LayoutGraph } from "../types.ts";
|
|
4
|
+
import { getGraphProps, getNodeProps } from "../util.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Strategy function to return the positions & locations of the children of a
|
|
8
|
+
* given node within a graph.
|
|
9
|
+
*/
|
|
10
|
+
export type ArrangeChildrenFn = (graph: LayoutGraph, id: NodeId) => NodeCircle[];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Arrange the children of a node in a graph, keeping their current (x, y)
|
|
14
|
+
* positions.
|
|
15
|
+
*/
|
|
16
|
+
export function retainPosition(graph: LayoutGraph, id: NodeId): NodeCircle[] {
|
|
17
|
+
return [...graph.children(id)].map((cid) => {
|
|
18
|
+
const {
|
|
19
|
+
point: { x, y },
|
|
20
|
+
radius: r,
|
|
21
|
+
} = getNodeProps(graph.node(cid)!).geometry;
|
|
22
|
+
return { id, x, y, r };
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Arrange the children of a node in a graph.
|
|
28
|
+
* @param graph
|
|
29
|
+
* @property minimumSpacing - minimum spacing between two neighboring nodes in
|
|
30
|
+
* a circular arrangement.
|
|
31
|
+
* @property hullRotation - see JSDoc for getCircleArrangement.
|
|
32
|
+
* @param id - id to get child nodes from.
|
|
33
|
+
*/
|
|
34
|
+
export function arrangeChildCircles(graph: LayoutGraph, id: NodeId): NodeCircle[] {
|
|
35
|
+
const { baseRadius, hullRotation } = getGraphProps(graph);
|
|
36
|
+
const arrangementOrder: NodeId[] = [...graph.children(id)].sort();
|
|
37
|
+
const sortedChildRadii: NodeRadius[] = getNodeRadii(graph, arrangementOrder);
|
|
38
|
+
|
|
39
|
+
// Find the smallest radius child circles can be arranged on to ensure a
|
|
40
|
+
// minimum distance of minimumSpacing between each circle
|
|
41
|
+
const arrangementRadius = smallestIntersectingRadius(
|
|
42
|
+
sortedChildRadii.map((a) => a.r),
|
|
43
|
+
baseRadius * 2,
|
|
44
|
+
);
|
|
45
|
+
// Lay out child circles on a virtual circle of smallestIntersectingRadius
|
|
46
|
+
const arrangement: NodeCircle[] = getCircleArrangement(
|
|
47
|
+
sortedChildRadii,
|
|
48
|
+
arrangementRadius,
|
|
49
|
+
hullRotation,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
return arrangement;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Evenly arrange an array of circles on a circle of particular radius
|
|
57
|
+
* @param circles
|
|
58
|
+
* @param arrangementRadius - radius of the virtual circle to arrange circles on
|
|
59
|
+
* @param hullRotation - the angle circle arrangement starts from. I.e., with
|
|
60
|
+
* hullRotation = 45, first circle's position will be
|
|
61
|
+
* {
|
|
62
|
+
* x: cos(0 + hullRotation) * arrangementRadius,
|
|
63
|
+
* y: sin(0 + hullRotation) * arrangementRadius
|
|
64
|
+
* }
|
|
65
|
+
*/
|
|
66
|
+
export function getCircleArrangement(
|
|
67
|
+
circles: NodeRadius[],
|
|
68
|
+
arrangementRadius: number,
|
|
69
|
+
hullRotation = 0,
|
|
70
|
+
): NodeCircle[] {
|
|
71
|
+
return circles.map((circle, i) => {
|
|
72
|
+
const position: Point = getPositionOnCircle(
|
|
73
|
+
arrangementRadius,
|
|
74
|
+
circles.length,
|
|
75
|
+
i,
|
|
76
|
+
-hullRotation,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return { ...circle, ...position };
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Obtain an array of nodes' ids and radii for calculations that don't require
|
|
85
|
+
* positions of node circles.
|
|
86
|
+
* @param graph
|
|
87
|
+
* @param nodes - nodes to get {id, r} from.
|
|
88
|
+
*/
|
|
89
|
+
export function getNodeRadii(graph: LayoutGraph, nodes: NodeId[]): NodeRadius[] {
|
|
90
|
+
return nodes.map((id) => ({
|
|
91
|
+
id,
|
|
92
|
+
r: getNodeProps(graph.node(id)!).geometry.radius,
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview When running layouts, children of nodes are handled using Hulls
|
|
3
|
+
* & Segments, as opposed to subgraphs. To not include children during the
|
|
4
|
+
* layout, these helpers will keep track of all the children its found, their
|
|
5
|
+
* edges, and their parent associations, removing them from the graph safely
|
|
6
|
+
* until they can be added back after layout of top-level nodes is complete.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { assertExists } from "../../assert.ts";
|
|
10
|
+
import { getSmallestEnclosingCircle } from "../../geometry/circle.ts";
|
|
11
|
+
import { TsabreGraph } from "../../tsabre/graph.ts";
|
|
12
|
+
import type { Circle, NodeId, Point } from "../../types.ts";
|
|
13
|
+
import type { LayoutEdge, LayoutGraph, LayoutNode } from "../types.ts";
|
|
14
|
+
import {
|
|
15
|
+
bottomUpBFTraversal,
|
|
16
|
+
getEdgeProps,
|
|
17
|
+
getGraphProps,
|
|
18
|
+
getNodeProps,
|
|
19
|
+
getSegmentProps,
|
|
20
|
+
position,
|
|
21
|
+
} from "../util.ts";
|
|
22
|
+
|
|
23
|
+
import { type ArrangeChildrenFn, arrangeChildCircles } from "./arrange.ts";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A childrenHandler returns a pair of functions bound to a known set of
|
|
27
|
+
* children.
|
|
28
|
+
*/
|
|
29
|
+
export function handler(): {
|
|
30
|
+
remove: (input: LayoutGraph) => LayoutGraph;
|
|
31
|
+
replace: (input: LayoutGraph) => LayoutGraph;
|
|
32
|
+
} {
|
|
33
|
+
let graph: LayoutGraph;
|
|
34
|
+
return {
|
|
35
|
+
/**
|
|
36
|
+
* Stash child nodes, their parent links, and their edges. This is done so
|
|
37
|
+
* that only top-level nodes participate in ranking, ordering and
|
|
38
|
+
* positioning.
|
|
39
|
+
*/
|
|
40
|
+
remove(input: LayoutGraph) {
|
|
41
|
+
graph = input;
|
|
42
|
+
const output = new TsabreGraph<LayoutNode, LayoutEdge>();
|
|
43
|
+
for (const id of graph.children()) {
|
|
44
|
+
output.setNode(id, graph.node(id)!);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Create an edge in the new graph for every top level segment. For those
|
|
48
|
+
// edges, sum the underlying edge weights and choose the largest
|
|
49
|
+
// minLength value.
|
|
50
|
+
for (const segment of graph.topSegments) {
|
|
51
|
+
for (const edge of segment.edges) {
|
|
52
|
+
const source = graph.ancestors(edge.source).pop()!;
|
|
53
|
+
const target = graph.ancestors(edge.target).pop()!;
|
|
54
|
+
const layout = getEdgeProps(edge);
|
|
55
|
+
if (output.hasEdge({ source, target })) {
|
|
56
|
+
const { minLength, weight } = getEdgeProps(output.edge({ source, target })!);
|
|
57
|
+
layout.minLength = Math.max(layout.minLength, minLength);
|
|
58
|
+
layout.weight += weight;
|
|
59
|
+
} else {
|
|
60
|
+
output.setEdge({ source, target }, { source, target, layout });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Most of properties of Symbol(layout) are static and don't change from
|
|
66
|
+
// graph to graph, because they're set to default. However, fas and
|
|
67
|
+
// virtualNodeLayout are dynamic, and the values set on first layout() run
|
|
68
|
+
// need to be passed on to subsequent runs of partialLayout(). That's why
|
|
69
|
+
// it's important to transfer Symbol(layout) from one version of graph to
|
|
70
|
+
// another.
|
|
71
|
+
Object.assign(getGraphProps(output), getGraphProps(input));
|
|
72
|
+
|
|
73
|
+
return output;
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Copy updated values out of temporary graph back to original graph.
|
|
78
|
+
*/
|
|
79
|
+
replace(input: LayoutGraph) {
|
|
80
|
+
for (const inSeg of input.topSegments) {
|
|
81
|
+
const outSeg = graph.topSegment(inSeg.source, inSeg.target);
|
|
82
|
+
if (!outSeg) continue;
|
|
83
|
+
getSegmentProps(outSeg).points = getSegmentProps(inSeg).points;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// See end of remove().
|
|
87
|
+
if (graph) {
|
|
88
|
+
Object.assign(getGraphProps(graph), getGraphProps(input));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return graph;
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Options to control how hull layout will proceed. */
|
|
97
|
+
export interface HullLayoutOptions {
|
|
98
|
+
// The node that layout is being triggered for, to minimize the neighborhood
|
|
99
|
+
// to perform layout on.
|
|
100
|
+
triggeredNodeId?: NodeId;
|
|
101
|
+
// Strategy function to use when performing child layout. Defaults to
|
|
102
|
+
// arrangeChildCircles.
|
|
103
|
+
arrangeChildren?: ArrangeChildrenFn;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function fillOptions(options?: NodeId | HullLayoutOptions): HullLayoutOptions {
|
|
107
|
+
if (typeof options === "string") {
|
|
108
|
+
return { triggeredNodeId: options };
|
|
109
|
+
} else {
|
|
110
|
+
return options ?? {};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Traverse the entire graph and calculate relative HULL positions of a
|
|
116
|
+
* particular set of nodes. Traversal order is bottom-up breadth-first.
|
|
117
|
+
*
|
|
118
|
+
* General case:
|
|
119
|
+
* All nodes' hull positions are recalculated or calculated for the first time.
|
|
120
|
+
* Set of nodes = all nodes of the graph.
|
|
121
|
+
*
|
|
122
|
+
* Specific case:
|
|
123
|
+
* A node with triggeredNodeId has been expanded/collapsed -
|
|
124
|
+
* triggered - hull positions of only a subset of nodes will be recalculated.
|
|
125
|
+
*
|
|
126
|
+
* Set of nodes = "children of all ancestors of the triggered
|
|
127
|
+
* node".
|
|
128
|
+
*
|
|
129
|
+
* Children get traversed along the way, so the traversed set is only
|
|
130
|
+
* "all ancestors of the triggered node".
|
|
131
|
+
*
|
|
132
|
+
* @param graph - the graph to be re-laid out
|
|
133
|
+
* @param triggeredNodeId - See manager/manager.ts::run(). ID of the
|
|
134
|
+
* expanded/collapsed node. Each such event triggers a partial layout
|
|
135
|
+
* recalculation. If no event fired and/or we need the complete layout
|
|
136
|
+
* recalculation, no triggeredNodeId is passed.
|
|
137
|
+
*/
|
|
138
|
+
export function layout(graph: LayoutGraph, options?: NodeId | HullLayoutOptions): void {
|
|
139
|
+
options = fillOptions(options);
|
|
140
|
+
const arrangeChildren = options.arrangeChildren ?? arrangeChildCircles;
|
|
141
|
+
const triggeredNodeId = options.triggeredNodeId;
|
|
142
|
+
const subsetToLayout: NodeId[] = triggeredNodeId
|
|
143
|
+
? graph.ancestors(triggeredNodeId)
|
|
144
|
+
: bottomUpBFTraversal(graph);
|
|
145
|
+
|
|
146
|
+
const { baseRadius } = getGraphProps(graph);
|
|
147
|
+
|
|
148
|
+
for (const id of subsetToLayout) {
|
|
149
|
+
const node = graph.node(id)!;
|
|
150
|
+
const nodeProps = getNodeProps(node);
|
|
151
|
+
|
|
152
|
+
// Expanded node's radius (and subsequently height and width) is handled
|
|
153
|
+
// here, by layout(), because it depends on expanded node's children.
|
|
154
|
+
// Collapsed node's radius is handled by
|
|
155
|
+
// drag_manager.ts::NoopLayoutManager::collapsing(), where it is set to
|
|
156
|
+
// its pre-expansion value, which was cached by
|
|
157
|
+
// NoopLayoutManager::expanding().
|
|
158
|
+
if (node.expanded && graph.hasChildren(id)) {
|
|
159
|
+
const childArrangement = arrangeChildren(graph, id);
|
|
160
|
+
|
|
161
|
+
// Having layed out the child circles, find the smallest circle to enclose
|
|
162
|
+
// them. Why not use a circle of existing smallestIntersectingRadius?
|
|
163
|
+
// Considering the sizes of child circles can vary, using an enclosing
|
|
164
|
+
// circle of smallestIntersectingRadius and centered at [0, 0] leads to
|
|
165
|
+
// uneven distances between child circles and the enclosing circle (hull).
|
|
166
|
+
// getEnclosingCircle() finds an enclosing circle potentially of different
|
|
167
|
+
// size and centered at a different point.
|
|
168
|
+
const hull = getSmallestEnclosingCircle(childArrangement, baseRadius);
|
|
169
|
+
|
|
170
|
+
// Align the inner children circles with the enclosing hull
|
|
171
|
+
// Why not add hull position to the parent node position? Due to relative
|
|
172
|
+
// positioning, changing parent's position doesn't change children nodes'
|
|
173
|
+
// positions relative to the parent. If child nodes are misaligned and/or
|
|
174
|
+
// extend beyond parent hull's boundaries, changing
|
|
175
|
+
// parent's position won't change it.
|
|
176
|
+
for (const circle of childArrangement) {
|
|
177
|
+
setPosition(circle.id, { x: circle.x - hull.x, y: circle.y - hull.y });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
nodeProps.geometry.resize(hull.r | 0);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function setPosition(id: NodeId, newPosition: Point) {
|
|
185
|
+
const node = assertExists(graph.node(id), `children::layout::changePosition: ${id} missing`);
|
|
186
|
+
// Truncate x and y to whole numbers
|
|
187
|
+
newPosition.x |= 0;
|
|
188
|
+
newPosition.y |= 0;
|
|
189
|
+
getNodeProps(node).geometry.moveTo(newPosition);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @param graph
|
|
195
|
+
* @param moveId - the node being moved inside its parent's hull. Starts with
|
|
196
|
+
* the dragged node directly, then its parent, then its parent, and so on to
|
|
197
|
+
* the top level.
|
|
198
|
+
* @param delta - drag amount.
|
|
199
|
+
* @param hullPadding - padding for getSmallestEnclosingCircle
|
|
200
|
+
*/
|
|
201
|
+
export function moveNodeBy(
|
|
202
|
+
graph: LayoutGraph,
|
|
203
|
+
moveId: NodeId,
|
|
204
|
+
delta: Point,
|
|
205
|
+
hullPadding: number = getGraphProps(graph).baseRadius,
|
|
206
|
+
): void {
|
|
207
|
+
// Step [1].
|
|
208
|
+
const moved = getNodeProps(graph.node(moveId)!).geometry;
|
|
209
|
+
moved.drag(delta);
|
|
210
|
+
|
|
211
|
+
// There are no siblings and no parent to position.
|
|
212
|
+
if (graph.hasParent(moveId)) {
|
|
213
|
+
const neighborhood: Set<NodeId> = graph.children(graph.parent(moveId));
|
|
214
|
+
const neighborCircles: Circle[] = [];
|
|
215
|
+
|
|
216
|
+
for (const neighborId of neighborhood) {
|
|
217
|
+
neighborCircles.push({
|
|
218
|
+
...position(graph.node(neighborId)!),
|
|
219
|
+
r: getNodeProps(graph.node(neighborId)!).geometry.height / 2,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Step [2]. getSmallestEnclosingCircle takes in the neighborhood of the
|
|
224
|
+
// moved node and returns a minimal enclosing circle, SHIFTED to accommodate
|
|
225
|
+
// for the user-introduced changes to the layout. The minimal enclosing
|
|
226
|
+
// circle is assigned to the parent circle and its children (moved node's
|
|
227
|
+
// neighborhood) are shifted by the parent's new center position.
|
|
228
|
+
const enclosingCircle = getSmallestEnclosingCircle(neighborCircles, hullPadding);
|
|
229
|
+
const props = getNodeProps(graph.node(graph.parent(moveId))!);
|
|
230
|
+
props.geometry.resize(enclosingCircle.r);
|
|
231
|
+
const drag = { x: -enclosingCircle.x, y: -enclosingCircle.y };
|
|
232
|
+
|
|
233
|
+
// Step [3].
|
|
234
|
+
for (const neighborId of neighborhood) {
|
|
235
|
+
const neighbor = getNodeProps(graph.node(neighborId)!).geometry;
|
|
236
|
+
neighbor.drag(drag);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Step [4]. Recursively repeat the entire process, but now, for the
|
|
240
|
+
// ancestor chain. Parent's center shift is the delta now.
|
|
241
|
+
moveNodeBy(graph, graph.parent(moveId), enclosingCircle, hullPadding);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Functions to calculate layouts for graph segments.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { assert, assertExists } from "../../assert.ts";
|
|
6
|
+
import { CircleGeometry as Circle } from "../../geometry.ts";
|
|
7
|
+
import { segmentId } from "../../tsabre/graph.ts";
|
|
8
|
+
import type { Edge, FullGraph, Point, Segment } from "../../types.ts";
|
|
9
|
+
import {
|
|
10
|
+
getGraphProps,
|
|
11
|
+
getLineSegmentBetweenNodes,
|
|
12
|
+
getNodeProps,
|
|
13
|
+
getSegmentProps,
|
|
14
|
+
} from "../util.ts";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* When a segment path ends without crossing a hull (eg an edge between a parent
|
|
18
|
+
* and a child), the final edge will leave at this angle from the child.
|
|
19
|
+
*/
|
|
20
|
+
const DEGREE_ANGLE_TO_PARENT = 45;
|
|
21
|
+
const ANGLE_TO_PARENT = (DEGREE_ANGLE_TO_PARENT * Math.PI) / 180;
|
|
22
|
+
const COS_TO_PARENT = Math.sin(ANGLE_TO_PARENT);
|
|
23
|
+
const SIN_TO_PARENT = Math.cos(ANGLE_TO_PARENT);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Given a graph, update points for a given collection of segments (or all
|
|
27
|
+
* segments).
|
|
28
|
+
*
|
|
29
|
+
* When laying out children relatively, segment points must be relative to the
|
|
30
|
+
* _target_ node.
|
|
31
|
+
*/
|
|
32
|
+
export function assignSegmentPoints(
|
|
33
|
+
graph: FullGraph,
|
|
34
|
+
segments: Iterable<Segment<Edge>> = graph.allSegments,
|
|
35
|
+
): void {
|
|
36
|
+
const { childrenRelative, bendLongEdges } = getGraphProps(graph);
|
|
37
|
+
for (const segment of segments) {
|
|
38
|
+
const props = getSegmentProps(segment);
|
|
39
|
+
if (graph.parent(segment.source) === graph.parent(segment.target)) {
|
|
40
|
+
props.points = topCurve(segment);
|
|
41
|
+
} else {
|
|
42
|
+
props.points = hulls(segment);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (childrenRelative) {
|
|
46
|
+
assert(props.points.length >= 2, `segment ${segmentId(segment)} has insufficient points`);
|
|
47
|
+
|
|
48
|
+
const median = (props.points.length / 2) | 0;
|
|
49
|
+
let { x: lsx, y: lsy } = props.points[median - 1];
|
|
50
|
+
let { x: ltx, y: lty } = props.points[median];
|
|
51
|
+
if (lsx > ltx) {
|
|
52
|
+
const [ttx, tty] = [ltx, lty];
|
|
53
|
+
[ltx, lty] = [lsx, lsy];
|
|
54
|
+
[lsx, lsy] = [ttx, tty];
|
|
55
|
+
props.label.flippedBy180 = true;
|
|
56
|
+
} else {
|
|
57
|
+
props.label.flippedBy180 = false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
props.label.position.x = (lsx + ltx) / 2;
|
|
61
|
+
props.label.position.y = (lsy + lty) / 2;
|
|
62
|
+
props.label.rotation = (Math.atan2(lty - lsy, ltx - lsx) * 180) / Math.PI;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Calculate the points along a path between two nodes who share a parent.
|
|
68
|
+
* These links may have corresponding edges which have their `points` array
|
|
69
|
+
* filled in from the layout algorithm. These points represent the "safe" path
|
|
70
|
+
* for an edge to travel while minimizing crossing other nodes.
|
|
71
|
+
*
|
|
72
|
+
* To build the PointTuple array, we create a list of node positions and sizes
|
|
73
|
+
* for the source, each point in the edge, and the target. Nodes created for
|
|
74
|
+
* edge points have a size 0. If there is no corresponding edge, or no edge
|
|
75
|
+
* points, this is then just a two-element array. Otherwise it is N + 2, for
|
|
76
|
+
* each intervening point.
|
|
77
|
+
*
|
|
78
|
+
* With this array, the PointTuple for each node is the point on its border
|
|
79
|
+
* closest to the center of the adjacent point of the array.
|
|
80
|
+
*/
|
|
81
|
+
function topCurve(segment: Segment<Edge>): Point[] {
|
|
82
|
+
const source = getNodeProps(assertExists(graph.node(segment.source))).geometry;
|
|
83
|
+
const target = getNodeProps(assertExists(graph.node(segment.target))).geometry;
|
|
84
|
+
|
|
85
|
+
let edgePoints = (getSegmentProps(segment).points || []) as Point[];
|
|
86
|
+
|
|
87
|
+
if (edgePoints.length <= 2 || !bendLongEdges) {
|
|
88
|
+
const { source: s, target: t } = getLineSegmentBetweenNodes(source, target);
|
|
89
|
+
edgePoints = [s, t];
|
|
90
|
+
} else {
|
|
91
|
+
const closePoint = Circle.point(edgePoints[1]);
|
|
92
|
+
const farPoint = Circle.point(edgePoints[edgePoints.length - 2]);
|
|
93
|
+
edgePoints[0] = getLineSegmentBetweenNodes(source, closePoint).source;
|
|
94
|
+
edgePoints[edgePoints.length - 1] = getLineSegmentBetweenNodes(farPoint, target).target;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return edgePoints;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* For a segment from a child to a parent, hulls calculates the PointTuple
|
|
102
|
+
* pointing at the "next" point out. This first follows the segment path to
|
|
103
|
+
* the top-level segment (a segment with both nodes having the same parent).
|
|
104
|
+
* After calculating the topCurve, it then finds the closest point on the edge
|
|
105
|
+
* of the child to the adjacent topCurve point. This repeats down until the
|
|
106
|
+
* segment itself is laid out.
|
|
107
|
+
*/
|
|
108
|
+
function hulls(segment: Segment<Edge>): Point[] {
|
|
109
|
+
// From a segment, track up to find the outermost point
|
|
110
|
+
const path: Array<Segment<Edge>> = [segment];
|
|
111
|
+
while (segment.next) {
|
|
112
|
+
segment = segment.next;
|
|
113
|
+
path.unshift(segment);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let line: { source: Point; target: Point } = {
|
|
117
|
+
source: { x: 0, y: 0 },
|
|
118
|
+
target: { x: 0, y: 0 },
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
if (graph.parent(path[0].source) === graph.parent(path[0].target)) {
|
|
122
|
+
// Find the next closest point
|
|
123
|
+
const hullPoints = topCurve(path[0]);
|
|
124
|
+
if (path.length === 1) {
|
|
125
|
+
return hullPoints;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (path[1].target === path[0].source) {
|
|
129
|
+
line.source.x = hullPoints[0].x;
|
|
130
|
+
line.source.y = hullPoints[0].y;
|
|
131
|
+
line.target.x = hullPoints[1].x;
|
|
132
|
+
line.target.y = hullPoints[1].y;
|
|
133
|
+
} else {
|
|
134
|
+
const l = hullPoints.length - 1;
|
|
135
|
+
line.source.x = hullPoints[l].x;
|
|
136
|
+
line.source.y = hullPoints[l].y;
|
|
137
|
+
line.target.x = hullPoints[l - 1].x;
|
|
138
|
+
line.target.y = hullPoints[l - 1].y;
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
// line segment from child to parent
|
|
142
|
+
const parent = getNodeProps(assertExists(graph.node(path[0].source))).geometry;
|
|
143
|
+
const child = getNodeProps(assertExists(graph.node(path[0].target))).geometry;
|
|
144
|
+
|
|
145
|
+
// Get a point `target` on parent that is ANGLE_TO_PARENT from parent's
|
|
146
|
+
// center.
|
|
147
|
+
const target = Circle.point(parent);
|
|
148
|
+
const out = target.clone();
|
|
149
|
+
out.move({
|
|
150
|
+
x: parent.radius * 2 * COS_TO_PARENT,
|
|
151
|
+
y: parent.radius * 2 * SIN_TO_PARENT,
|
|
152
|
+
});
|
|
153
|
+
const tmp = { x: target.x, y: target.y };
|
|
154
|
+
parent.clipi([out, tmp]);
|
|
155
|
+
target.moveTo(tmp);
|
|
156
|
+
|
|
157
|
+
// Get the line from the target point on the parent to the child.
|
|
158
|
+
line = getLineSegmentBetweenNodes(target, child);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (let i = 1; i < path.length; i++) {
|
|
162
|
+
if (childrenRelative) {
|
|
163
|
+
// adjust line source by path[i].target
|
|
164
|
+
const { x, y } = getNodeProps(assertExists(graph.node(path[i].target))).geometry;
|
|
165
|
+
line.source.x -= x;
|
|
166
|
+
line.source.y -= y;
|
|
167
|
+
}
|
|
168
|
+
const source = getNodeProps(assertExists(graph.node(path[i].source))).geometry;
|
|
169
|
+
line = getLineSegmentBetweenNodes(source, Circle.point(line.source));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return [line.source, line.target];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { NodeId } from "../../types.ts";
|
|
2
|
+
import { hulls } from "../hulls.ts";
|
|
3
|
+
|
|
4
|
+
import type { NoopLayoutManager } from "./manager.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ChildManager handles running layout for child nodes.
|
|
8
|
+
*/
|
|
9
|
+
export class ChildManager {
|
|
10
|
+
protected manager: NoopLayoutManager;
|
|
11
|
+
constructor(manager: NoopLayoutManager) {
|
|
12
|
+
this.manager = manager;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
runLayoutAfterNodeExpanded(id: NodeId): void {
|
|
16
|
+
// Collapsed node's bounds are set in collapsing() from the cached
|
|
17
|
+
// pre-expansion bounds.
|
|
18
|
+
this.manager.drag.expanding(id);
|
|
19
|
+
|
|
20
|
+
this.doChildLayout(id);
|
|
21
|
+
|
|
22
|
+
this.doNodeResize(id);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
runLayoutAfterNodeCollapsed(id: NodeId): void {
|
|
26
|
+
// Collapsed node's bounds are set in collapsing() from the cached
|
|
27
|
+
// pre-expansion bounds.
|
|
28
|
+
this.manager.drag.collapsing(id);
|
|
29
|
+
|
|
30
|
+
this.doNodeResize(id);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
protected doChildLayout(parent: NodeId): void {
|
|
34
|
+
hulls.layout(this.manager.graph, parent);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
protected doNodeResize(id: NodeId): void {
|
|
38
|
+
// Expanded node's dimensions & affected nodes' positions are set
|
|
39
|
+
// in run() => hulls.ts::layout().
|
|
40
|
+
this.manager.run(this.manager.graph, id);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { assertExists } from "../../assert.ts";
|
|
2
|
+
import { origin } from "../../geometry/geometry.ts";
|
|
3
|
+
import type { NodeId, Point } from "../../types.ts";
|
|
4
|
+
import { moveNodeBy as moveNodeInHullBy } from "../hulls/hulls.ts";
|
|
5
|
+
import { assignSegmentPoints } from "../hulls/segments.ts";
|
|
6
|
+
import { findSegmentNeighborhood, getGraphProps, getNodeProps } from "../util.ts";
|
|
7
|
+
|
|
8
|
+
import type { NoopLayoutManager } from "./manager.ts";
|
|
9
|
+
|
|
10
|
+
/** A class to delegate node dragging functionality for a layout manager. */
|
|
11
|
+
export class NoopDragManager {
|
|
12
|
+
// tracks the radius of a node pre-expansion, so it can be reset after
|
|
13
|
+
// collapse
|
|
14
|
+
protected readonly preExpansionRadius = new Map<NodeId, number>();
|
|
15
|
+
|
|
16
|
+
protected movingNode: NodeId | undefined = undefined;
|
|
17
|
+
|
|
18
|
+
draggable = false;
|
|
19
|
+
|
|
20
|
+
protected readonly layoutManager: NoopLayoutManager;
|
|
21
|
+
constructor(layoutManager: NoopLayoutManager) {
|
|
22
|
+
this.layoutManager = layoutManager;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
isDragging(id?: NodeId): boolean {
|
|
26
|
+
if (id) {
|
|
27
|
+
return id === this.movingNode;
|
|
28
|
+
} else {
|
|
29
|
+
return this.movingNode !== undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
startMovingNode(id: NodeId): void {
|
|
34
|
+
this.movingNode = id;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
stopMovingNode(): void {
|
|
38
|
+
this.movingNode = undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
moveNodeBy(id: NodeId, delta: Point): void {
|
|
42
|
+
// noop
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
expanding(id: NodeId): void {
|
|
46
|
+
const target = assertExists(
|
|
47
|
+
this.layoutManager.graph.node(id),
|
|
48
|
+
`DragManager::expanding ${id} missing`,
|
|
49
|
+
);
|
|
50
|
+
this.preExpansionRadius.set(id, getNodeProps(target).geometry.height / 2);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
collapsing(id: NodeId): void {
|
|
54
|
+
const graph = this.layoutManager.graph;
|
|
55
|
+
/*
|
|
56
|
+
Children can't have a dx and dy before they are laid out because their
|
|
57
|
+
final position is an offset of their parent's position, and their parent's
|
|
58
|
+
position depends on their final position, so including dx and dy makes a
|
|
59
|
+
dependency loop. Without them, the children's position can be determined.
|
|
60
|
+
*/
|
|
61
|
+
// Reset dx, dy for all descendants.
|
|
62
|
+
for (const cid of graph.children(id)) {
|
|
63
|
+
resetPosition(cid);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const target = assertExists(graph.node(id), `DragManager::collapsing target ${id} missing`);
|
|
67
|
+
const newRadius =
|
|
68
|
+
this.preExpansionRadius.get(id) || getGraphProps(this.layoutManager.graph).baseRadius;
|
|
69
|
+
this.preExpansionRadius.delete(id);
|
|
70
|
+
const { geometry } = getNodeProps(target);
|
|
71
|
+
geometry.resize(newRadius);
|
|
72
|
+
|
|
73
|
+
function resetPosition(nid: NodeId) {
|
|
74
|
+
const node = assertExists(
|
|
75
|
+
graph.node(nid),
|
|
76
|
+
`DragManager::collapsing::resetPosition ${nid} missing`,
|
|
77
|
+
);
|
|
78
|
+
getNodeProps(node).geometry.dragTo(origin());
|
|
79
|
+
for (const cid of graph.children(nid)) {
|
|
80
|
+
resetPosition(cid);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A class to delegate node dragging functionality for a layout manager. */
|
|
87
|
+
export class DragManager extends NoopDragManager {
|
|
88
|
+
override draggable = true;
|
|
89
|
+
|
|
90
|
+
constructor(layoutManager: NoopLayoutManager) {
|
|
91
|
+
super(layoutManager);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
override moveNodeBy(id: NodeId, delta: Point): void {
|
|
95
|
+
if (this.movingNode !== id) {
|
|
96
|
+
if (this.movingNode !== undefined) {
|
|
97
|
+
this.stopMovingNode();
|
|
98
|
+
}
|
|
99
|
+
this.startMovingNode(id);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const graph = this.layoutManager.graph;
|
|
103
|
+
moveNodeInHullBy(graph, id, delta);
|
|
104
|
+
const root = graph.ancestors(id).pop();
|
|
105
|
+
assertExists(root, `DragManager::moveNodeBy ${id} must have ancestor in order to move`);
|
|
106
|
+
|
|
107
|
+
// After a node has been moved, update the end points of segments in its
|
|
108
|
+
// neighborhood.
|
|
109
|
+
assignSegmentPoints(graph, findSegmentNeighborhood(this.layoutManager.graph, root));
|
|
110
|
+
this.layoutManager.updateAt(root);
|
|
111
|
+
}
|
|
112
|
+
}
|