@davidsouther/agathon 0.0.0-stage → 2026.11.1

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 (62) hide show
  1. package/package.json +19 -4
  2. package/src/assert.ts +67 -0
  3. package/src/geometry/box.ts +76 -0
  4. package/src/geometry/circle.ts +296 -0
  5. package/src/geometry/geometry.ts +318 -0
  6. package/src/geometry/path.ts +315 -0
  7. package/src/geometry/quadtree.ts +321 -0
  8. package/src/geometry/rectangle.ts +129 -0
  9. package/src/geometry/rngon.ts +132 -0
  10. package/src/geometry/types.ts +102 -0
  11. package/src/geometry.ts +61 -0
  12. package/src/highlight/highlight.ts +85 -0
  13. package/src/index.ts +5 -0
  14. package/src/layout/grid/manager.ts +55 -0
  15. package/src/layout/grid.ts +1 -0
  16. package/src/layout/hierarchic/README.md +21 -0
  17. package/src/layout/hierarchic/acyclic.ts +249 -0
  18. package/src/layout/hierarchic/children.ts +169 -0
  19. package/src/layout/hierarchic/debug.ts +186 -0
  20. package/src/layout/hierarchic/layout.ts +266 -0
  21. package/src/layout/hierarchic/manager.ts +65 -0
  22. package/src/layout/hierarchic/normalize.ts +177 -0
  23. package/src/layout/hierarchic/order.ts +321 -0
  24. package/src/layout/hierarchic/position.ts +514 -0
  25. package/src/layout/hierarchic/rank.ts +629 -0
  26. package/src/layout/hierarchic.ts +2 -0
  27. package/src/layout/hulls/arrange.ts +94 -0
  28. package/src/layout/hulls/hulls.ts +243 -0
  29. package/src/layout/hulls/segments.ts +174 -0
  30. package/src/layout/hulls.ts +3 -0
  31. package/src/layout/manager/child_manager.ts +42 -0
  32. package/src/layout/manager/drag_manager.ts +112 -0
  33. package/src/layout/manager/manager.ts +425 -0
  34. package/src/layout/manager/pan_manager.ts +146 -0
  35. package/src/layout/manager/visibility_manager.ts +84 -0
  36. package/src/layout/manager/zoom_manager.ts +263 -0
  37. package/src/layout/manager.ts +6 -0
  38. package/src/layout/properties/defaults.ts +99 -0
  39. package/src/layout/properties/symbol.ts +99 -0
  40. package/src/layout/properties/weak_map.ts +73 -0
  41. package/src/layout/properties.ts +111 -0
  42. package/src/layout/types.ts +470 -0
  43. package/src/layout/util.ts +444 -0
  44. package/src/layout.ts +6 -0
  45. package/src/samples/diff_graph.ts +15 -0
  46. package/src/samples/edge_cases.ts +168 -0
  47. package/src/samples/gansner_with_states.ts +67 -0
  48. package/src/samples/load_generator.ts +213 -0
  49. package/src/samples/search_graph.ts +35 -0
  50. package/src/tsabre/algo.ts +126 -0
  51. package/src/tsabre/builder.ts +97 -0
  52. package/src/tsabre/compress.ts +15 -0
  53. package/src/tsabre/dijkstra/dijkstra.ts +76 -0
  54. package/src/tsabre/dijkstra/heap.ts +83 -0
  55. package/src/tsabre/graph.ts +808 -0
  56. package/src/tsabre/parse.ts +634 -0
  57. package/src/tsabre/print.ts +188 -0
  58. package/src/tsabre/sourcebuilder/code_content_buffer.ts +275 -0
  59. package/src/tsabre/sourcebuilder/printable.ts +33 -0
  60. package/src/tsabre.ts +6 -0
  61. package/src/types.ts +424 -0
  62. 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,3 @@
1
+ export * as arrange from "./hulls/arrange.ts";
2
+ export * as hulls from "./hulls/hulls.ts";
3
+ export * as segments from "./hulls/segments.ts";
@@ -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
+ }