@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,425 @@
1
+ import { assertExists } from "../../assert.ts";
2
+ import type { Geometry } from "../../geometry/types.ts";
3
+ import { segmentId } from "../../tsabre/graph.ts";
4
+ import type { Bounds, Box, NodeId, Point, Segment, SegmentId } from "../../types.ts";
5
+ import type {
6
+ GraphLayout,
7
+ LayoutEdge,
8
+ LayoutGraph,
9
+ LayoutManager,
10
+ LayoutNode,
11
+ SegmentPosition,
12
+ SegmentProperties,
13
+ } from "../types.ts";
14
+ import { findSegmentNeighborhood, getNodeProps, getSegmentProps, isSegment } from "../util.ts";
15
+
16
+ import { ChildManager } from "./child_manager.ts";
17
+ import { DragManager, NoopDragManager } from "./drag_manager.ts";
18
+ import { NoopPanManager, PanManager } from "./pan_manager.ts";
19
+ import { VisibilityManager } from "./visibility_manager.ts";
20
+ import { NoopZoomManager, ZoomManager } from "./zoom_manager.ts";
21
+
22
+ /**
23
+ * NoopLayoutManager provides the minimal functionality for handling graph
24
+ * layouts, but does not include mechanisms for pan/zoom/drag behaviors. There
25
+ * is a lot of machinery between asking for node positions in `doLayout` and
26
+ * having the full featureset for the renderer, including visibility culling,
27
+ * renderability checks, segment positioning, etc. This class gives the base
28
+ * minimum with no interaction functionality, rather than needing to turn off
29
+ * interactions in future subclasses. Most clients should use
30
+ * AbstractLayoutManager.
31
+ */
32
+ export abstract class NoopLayoutManager implements LayoutManager, GraphLayout {
33
+ protected readonly nodePositions = new Map<NodeId, Geometry>();
34
+ protected readonly segmentPositions = new Map<string, SegmentPosition>();
35
+ protected readonly entityTouched = new Set<NodeId | SegmentId>();
36
+
37
+ readonly layout = this;
38
+ private deepestExpandedNode = 0;
39
+ get depth(): number {
40
+ return this.deepestExpandedNode;
41
+ }
42
+
43
+ get zoomLevel(): number {
44
+ return this.zoom.level;
45
+ }
46
+
47
+ get box(): Box {
48
+ return this.pan.box;
49
+ }
50
+
51
+ get center(): Point {
52
+ return this.pan.center;
53
+ }
54
+
55
+ get panOffset(): Point {
56
+ return this.pan.panOffset;
57
+ }
58
+
59
+ get pageBounds(): Bounds {
60
+ return this.pan.pageBounds;
61
+ }
62
+
63
+ get draggable(): boolean {
64
+ return this.drag.draggable;
65
+ }
66
+
67
+ get isDragging(): boolean {
68
+ return this.drag.isDragging();
69
+ }
70
+
71
+ // These internal helpers handle the individual parts of layout management.
72
+
73
+ // pan handles positioning the viewbox relative to the graph layout.
74
+ readonly pan = this.buildPanManager();
75
+ // drag handles moving individual nodes around the layout.
76
+ readonly drag = this.buildDragManager();
77
+ // visibility handles calculations for whether an item is visible in the view.
78
+ readonly visibility = this.buildVisibilityManager();
79
+ readonly zoom = this.buildZoomManager();
80
+ readonly children = this.buildChildManager();
81
+
82
+ // Update listeners
83
+ private readonly updateCallbacks = new Set<() => void>();
84
+
85
+ public graph: LayoutGraph;
86
+ constructor(graph: LayoutGraph) {
87
+ this.graph = graph;
88
+ }
89
+
90
+ // buildPanManager, buildDragManager, and buildVisibilityManager allow
91
+ // extending classes to replace the sub-manager behaviors.
92
+
93
+ protected buildPanManager(): PanManager {
94
+ return new NoopPanManager(this);
95
+ }
96
+
97
+ protected buildDragManager(): DragManager {
98
+ return new NoopDragManager(this);
99
+ }
100
+
101
+ protected buildVisibilityManager(): VisibilityManager {
102
+ return new VisibilityManager(this);
103
+ }
104
+
105
+ protected buildChildManager(): ChildManager {
106
+ return new ChildManager(this);
107
+ }
108
+
109
+ protected buildZoomManager(): ZoomManager {
110
+ return new NoopZoomManager(this);
111
+ }
112
+
113
+ /**
114
+ * doLayout allows extending classes to use their own calculations to lay out
115
+ * nodes in the graph. It is responsible for setting the x, y, and radius node
116
+ * properties for every node in an expanded parent. Additionally, it can
117
+ * assign points: Point to each top-level segment, to make bendy rather than
118
+ * straight edges.
119
+ * @param triggeredNodeId - See the same parameter for run()
120
+ */
121
+ protected abstract doLayout(triggeredNodeId?: NodeId): void;
122
+
123
+ /**
124
+ * Rerun the layout.
125
+ * @param graph - The graph that is being recalculated
126
+ * @param triggeredNodeId - ID of the expanded/collapsed node. Each such event
127
+ * triggers a partial layout recalculation. If no event fired and/or we need
128
+ * the complete layout recalculation, no triggeredNodeId is passed.
129
+ * @param panToCenter - Whether or not to pan the viewport to center after
130
+ * layout.
131
+ */
132
+ run(graph: LayoutGraph, triggeredNodeId?: NodeId, panToCenter = true): void {
133
+ this.graph = graph;
134
+
135
+ this.nodePositions.clear();
136
+ this.segmentPositions.clear();
137
+ this.entityTouched.clear();
138
+ this.deepestExpandedNode = 0;
139
+
140
+ this.doLayout(triggeredNodeId);
141
+ this.updateAt(undefined, panToCenter);
142
+ }
143
+
144
+ runFinished(): void {
145
+ this.entityTouched.clear();
146
+ }
147
+
148
+ setPageBounds(pageBounds: Bounds): void {
149
+ this.pan.setPageBounds(pageBounds);
150
+ this.updateVisibility();
151
+ }
152
+
153
+ moveGraphBy(move: Point): void {
154
+ this.pan.moveGraphBy(move);
155
+ this.updateVisibility();
156
+ }
157
+
158
+ panToNode(id: NodeId): void {
159
+ if (this.graph.hasNode(id) && this.isNodeRenderable(id)) {
160
+ this.pan.panToPoint(this.getNodePosition(id));
161
+ this.updateVisibility();
162
+ }
163
+ }
164
+
165
+ resetGraphPan(): void {
166
+ this.pan.reset();
167
+ this.updateAt();
168
+ }
169
+
170
+ resetGraphZoom(): void {
171
+ this.zoom.zoom(0);
172
+ this.updateAt();
173
+ }
174
+
175
+ updateVisibility(): void {
176
+ this.visibility.update();
177
+ }
178
+
179
+ moveNodeBy(id: NodeId, delta: Point): void {
180
+ this.drag.moveNodeBy(id, this.zoom.dragAmount(delta));
181
+ }
182
+
183
+ startMovingNode(id: NodeId): void {
184
+ this.drag.startMovingNode(id);
185
+ }
186
+
187
+ stopMovingNode(): void {
188
+ this.drag.stopMovingNode();
189
+ }
190
+
191
+ didUpdate(entity: NodeId | Segment<LayoutEdge>): boolean {
192
+ const id = isSegment(entity) ? segmentId(entity) : entity;
193
+ return this.entityTouched.has(id);
194
+ }
195
+
196
+ runLayoutAfterNodeExpanded(id: NodeId): void {
197
+ this.children.runLayoutAfterNodeExpanded(id);
198
+ }
199
+
200
+ runLayoutAfterNodeCollapsed(id: NodeId): void {
201
+ this.children.runLayoutAfterNodeCollapsed(id);
202
+ }
203
+
204
+ /**
205
+ * Zoom the graph in one zoom level. If provided, point is treated as a
206
+ * position relative to the page bounds (which start at <0,0>, usually the
207
+ * top-left corner of the containing SVG) to keep at a consistent screen
208
+ * location while zooming, with everything moving away from that point.
209
+ */
210
+ zoomIn(point?: Point): void {
211
+ this.zoom.zoomIn(point);
212
+ }
213
+
214
+ /**
215
+ * Zoom the graph out one zoom level. If provided, point is treated as a
216
+ * position relative to the page bounds (which start at <0,0>, usually the
217
+ * top-left corner of the containing SVG) to keep at a consistent screen
218
+ * location while zooming, with everything moving towards from that point.
219
+ */
220
+ zoomOut(point?: Point): void {
221
+ this.zoom.zoomOut(point);
222
+ }
223
+
224
+ hasNodePosition(id: NodeId): boolean {
225
+ return this.nodePositions.has(id);
226
+ }
227
+
228
+ getNodePosition(id: NodeId): Geometry {
229
+ const msg = `AbstractLayoutManager::getNodePosition ${id} missing`;
230
+ return assertExists(this.nodePositions.get(id), msg);
231
+ }
232
+
233
+ hasSegmentPosition(segment: Segment<LayoutEdge>): boolean {
234
+ const id = segmentId(segment);
235
+ return this.segmentPositions.has(id);
236
+ }
237
+
238
+ getSegmentPosition(segment: Segment<LayoutEdge>): Readonly<SegmentPosition> {
239
+ const id = segmentId(segment);
240
+ const msg = `AbstractLayoutManager::getSegmentPosition ${id} missing`;
241
+ return assertExists(this.segmentPositions.get(id), msg);
242
+ }
243
+
244
+ isNodeVisible(id: NodeId): boolean {
245
+ return this.visibility.isNodeVisible(id) || this.drag.isDragging(id);
246
+ }
247
+
248
+ isSegmentVisible(segment: Segment<LayoutEdge>): boolean {
249
+ return this.visibility.isSegmentVisible(segment);
250
+ }
251
+
252
+ /**
253
+ * A node is renderable if all its ancestors are expanded.
254
+ */
255
+ isNodeRenderable(id: NodeId): boolean {
256
+ // Ancestors returns id as the first item
257
+ for (const pid of this.graph.ancestors(id).slice(1)) {
258
+ if (!this.getNode(pid).expanded) return false;
259
+ }
260
+ return true;
261
+ }
262
+
263
+ /**
264
+ * A segment is renderable if it is a rising segment and the target is
265
+ * expanded, or if it is a level segment and the parent is expanded.
266
+ */
267
+ isSegmentRenderable(segment: Segment): boolean {
268
+ return this.isNodeRenderable(segment.source);
269
+ }
270
+
271
+ getNode(id: NodeId): LayoutNode {
272
+ const msg = `AbstractLayoutManager::getNode: ${id} missing`;
273
+ return assertExists(this.graph.node(id), msg);
274
+ }
275
+
276
+ updateAt(root?: NodeId, panToCenter = false): void {
277
+ this.updateNodes(root);
278
+ this.updateSegments(root);
279
+
280
+ // Calculate the new center of the page
281
+ if (panToCenter) {
282
+ this.pan.updateCenter();
283
+ this.pan.updateOffset();
284
+ }
285
+
286
+ this.updateVisibility();
287
+ this.callUpdateCallbacks();
288
+ }
289
+
290
+ updateLayout(root?: NodeId, panToCenter?: boolean): void {
291
+ this.doLayout();
292
+ this.updateAt(root, panToCenter);
293
+ }
294
+
295
+ registerUpdateCallback(cb: () => void): void {
296
+ this.updateCallbacks.add(cb);
297
+ }
298
+
299
+ unregisterUpdateCallback(cb: () => void): void {
300
+ this.updateCallbacks.delete(cb);
301
+ }
302
+
303
+ protected updateNodes(root?: NodeId): void {
304
+ const traversal = root ? [root] : this.graph.children();
305
+ const depth = root ? this.graph.ancestors(root).length : 1;
306
+ for (const id of traversal) {
307
+ this.updateNode(id, depth);
308
+ }
309
+ }
310
+
311
+ protected updateNode(id: NodeId, depth: number): void {
312
+ if (id && !this.isNodeRenderable(id)) {
313
+ this.clearNode(id);
314
+ return;
315
+ }
316
+
317
+ this.deepestExpandedNode = Math.max(this.deepestExpandedNode, depth);
318
+
319
+ // Update nodes from bottom to top, to have children resolved before
320
+ // parent hulls.
321
+ for (const child of this.graph.children(id)) {
322
+ this.updateNode(child, depth + 1);
323
+ }
324
+
325
+ if (!id) return;
326
+
327
+ const node = this.getNode(id);
328
+ const position = getNodeProps(node).geometry.clone();
329
+ // Remove drag from clone, to simplify zoom later
330
+ position.move((position as Geometry & { dragPt: Point }).dragPt);
331
+ position.dragTo({ x: 0, y: 0 });
332
+ this.assignNode(node, position);
333
+ }
334
+
335
+ protected clearNode(id: NodeId): void {
336
+ this.nodePositions.delete(id);
337
+ this.entityTouched.delete(id);
338
+ }
339
+
340
+ protected assignNode(node: LayoutNode, position: Geometry): void {
341
+ position = this.zoom.diffuseNodePosition(node, position);
342
+ this.nodePositions.set(node.id, position);
343
+ this.entityTouched.add(node.id);
344
+ }
345
+
346
+ protected updateSegments(id?: NodeId): void {
347
+ const segments = findSegmentNeighborhood(this.graph, id);
348
+ const completed = new Set<Segment>();
349
+
350
+ // Assign segments top to bottom, so that upper segments have always been
351
+ // assigned their positions before lower segments. Lower segments need the
352
+ // position of their upper segments to calculate their target position, so
353
+ // going from the top ensures the upper segments are always set first.
354
+ const assignSegmentTree = (segment: Segment<LayoutEdge>) => {
355
+ if (completed.has(segment)) return;
356
+ completed.add(segment);
357
+ if (segment.next) assignSegmentTree(segment.next);
358
+ if (this.isSegmentRenderable(segment)) {
359
+ this.assignSegment(segment, getSegmentProps(segment));
360
+ }
361
+ };
362
+
363
+ for (const segment of segments) {
364
+ assignSegmentTree(segment);
365
+ }
366
+ }
367
+
368
+ protected assignSegment(segment: Segment<LayoutEdge>, pos: SegmentProperties): void {
369
+ const id = segmentId(segment);
370
+
371
+ // update existing values, when present, to cut down on object allocations.
372
+ if (this.segmentPositions.has(id)) {
373
+ const current = this.segmentPositions.get(id)!;
374
+ current.points = [...pos.points];
375
+ current.label.position.x = pos.label.position.x;
376
+ current.label.position.y = pos.label.position.y;
377
+ current.label.rotation = pos.label.rotation;
378
+ current.label.flippedBy180 = pos.label.flippedBy180;
379
+ } else {
380
+ this.segmentPositions.set(id, {
381
+ points: [...pos.points],
382
+ label: {
383
+ position: {
384
+ x: pos.label.position.x,
385
+ y: pos.label.position.y,
386
+ },
387
+ rotation: pos.label.rotation,
388
+ flippedBy180: pos.label.flippedBy180,
389
+ },
390
+ });
391
+ }
392
+
393
+ // Apply zoom transformations to local data copy
394
+ this.segmentPositions.set(
395
+ id,
396
+ this.zoom.diffuseSegmentPosition(segment, this.segmentPositions.get(id)!),
397
+ );
398
+
399
+ this.entityTouched.add(id);
400
+ }
401
+
402
+ protected callUpdateCallbacks(): void {
403
+ for (const cb of this.updateCallbacks) {
404
+ cb();
405
+ }
406
+ }
407
+ }
408
+
409
+ /**
410
+ * AbstractLayoutManager tracks node and segment positions for a LayoutGraph,
411
+ * allowing sub classes to define the mechanism to choose the position of items.
412
+ */
413
+ export abstract class AbstractLayoutManager extends NoopLayoutManager {
414
+ protected override buildPanManager(): PanManager {
415
+ return new PanManager(this);
416
+ }
417
+
418
+ protected override buildDragManager(): DragManager {
419
+ return new DragManager(this);
420
+ }
421
+
422
+ protected override buildZoomManager(): ZoomManager {
423
+ return new ZoomManager(this);
424
+ }
425
+ }
@@ -0,0 +1,146 @@
1
+ import type { Bounds, Box, Point } from "../../types.ts";
2
+
3
+ import type { NoopLayoutManager } from "./manager.ts";
4
+
5
+ /**
6
+ * A PanManager which tracks pan offset and center of graph, but does not
7
+ * update offset on interactions.
8
+ */
9
+ export class NoopPanManager {
10
+ // The cumulative pan offset
11
+ protected pan: Point = { x: 0, y: 0 };
12
+ // The calculated center of the graph
13
+ center: Point = { x: 0, y: 0 };
14
+ // The total bounds of the graph covered
15
+ bounds: Box = { x: 0, y: 0, width: 0, height: 0 };
16
+ // The bounds of the primary viewing window this graph is in
17
+ pageBounds: Bounds = { height: 0, width: 0 };
18
+ // The vector offset to move `center` to the middle of the `pageBounds`
19
+ protected offset: Point = { x: 0, y: 0 };
20
+
21
+ get box(): Box {
22
+ return {
23
+ x: -this.panOffset.x,
24
+ y: -this.panOffset.y,
25
+ width: this.pageBounds.width,
26
+ height: this.pageBounds.height,
27
+ };
28
+ }
29
+
30
+ get panOffset(): Point {
31
+ return {
32
+ x: this.offset.x + this.pan.x,
33
+ y: this.offset.y + this.pan.y,
34
+ };
35
+ }
36
+
37
+ protected readonly layoutManager: NoopLayoutManager;
38
+ constructor(layoutManager: NoopLayoutManager) {
39
+ this.layoutManager = layoutManager;
40
+ }
41
+
42
+ /**
43
+ * Given a point relative to the pageBounds, calculate the equivalent layout
44
+ * coordinate by first removing the panOffset, and then applying the reverse
45
+ * zoom diffusion to project from zoomed space to layout space.
46
+ */
47
+ pagePointToGraphPoint(point: Point): Point {
48
+ return this.layoutManager.zoom.undiffusePoint({
49
+ x: point.x - this.panOffset.x,
50
+ y: point.y - this.panOffset.y,
51
+ });
52
+ }
53
+
54
+ graphPointToPagePoint(point: Point): Point {
55
+ const p = this.layoutManager.zoom.diffusePoint(point);
56
+ return {
57
+ x: this.panOffset.x + p.x,
58
+ y: this.panOffset.y + p.y,
59
+ };
60
+ }
61
+
62
+ moveGraphBy(move: Point): void {
63
+ // noop
64
+ }
65
+
66
+ panToPoint(graphPoint: Point): void {
67
+ // noop
68
+ }
69
+
70
+ reset(): void {
71
+ this.pan.x = this.pan.y = 0;
72
+ this.updateOffset();
73
+ }
74
+
75
+ setPageBounds(pageBounds: Bounds): void {
76
+ this.pageBounds = pageBounds;
77
+ this.updateOffset();
78
+ }
79
+
80
+ /**
81
+ * Find the simple geometric center of the graph by looking at the points +/-
82
+ * radius of all top-level nodes.
83
+ */
84
+ updateCenter(): void {
85
+ let minX = Number.MAX_SAFE_INTEGER;
86
+ let minY = Number.MAX_SAFE_INTEGER;
87
+ let maxX = Number.MIN_SAFE_INTEGER;
88
+ let maxY = Number.MIN_SAFE_INTEGER;
89
+
90
+ for (const id of this.layoutManager.graph.children()) {
91
+ const geometry = this.layoutManager.getNodePosition(id);
92
+
93
+ // Round to 0 (cast to int then bitwise or) for numerical robustness
94
+ minX = Math.min(minX, geometry.x - geometry.radius) | 0;
95
+ minY = Math.min(minY, geometry.y - geometry.radius) | 0;
96
+ maxX = Math.max(maxX, geometry.x + geometry.radius) | 0;
97
+ maxY = Math.max(maxY, geometry.y + geometry.radius) | 0;
98
+ }
99
+
100
+ if (minX > maxX) [minX, maxX] = [maxX, minX];
101
+ if (minY > maxY) [minY, maxY] = [maxY, minY];
102
+
103
+ this.bounds.x = minX;
104
+ this.bounds.width = maxX - minX;
105
+ this.bounds.y = minY;
106
+ this.bounds.height = maxY - minY;
107
+
108
+ // Distribute the division for better numerical stability
109
+ this.center.x = (maxX / 2 + minX / 2) | 0;
110
+ this.center.y = (maxY / 2 + minY / 2) | 0;
111
+ }
112
+
113
+ updateOffset(): void {
114
+ // offset is the vector which moves the center of the layout C to the
115
+ // center of the graph bounds B or O = B - C.
116
+ this.offset = {
117
+ x: this.pageBounds.width / 2 - this.center.x,
118
+ y: this.pageBounds.height / 2 - this.center.y,
119
+ };
120
+ }
121
+ }
122
+
123
+ /** A class to delegate box panning management for layout mananagers. */
124
+ export class PanManager extends NoopPanManager {
125
+ constructor(layoutManager: NoopLayoutManager) {
126
+ super(layoutManager);
127
+ }
128
+
129
+ override moveGraphBy(move: Point): void {
130
+ this.pan.x += move.x;
131
+ this.pan.y += move.y;
132
+ }
133
+
134
+ override panToPoint(graphPoint: Point): void {
135
+ // Calculate a new pan which will put this graph point in the center of the
136
+ // pageBounds, N = (B - P) - O
137
+ const zoomPoint = this.layoutManager.zoom.diffusePoint(graphPoint);
138
+ const b = {
139
+ x: this.pageBounds.width / 2 - zoomPoint.x,
140
+ y: this.pageBounds.height / 2 - zoomPoint.y,
141
+ };
142
+
143
+ this.pan.x = b.x - this.offset.x;
144
+ this.pan.y = b.y - this.offset.y;
145
+ }
146
+ }
@@ -0,0 +1,84 @@
1
+ import { circleIntersectsBox, lineIntersectsBox } from "../../geometry/box.ts";
2
+ import { segmentId } from "../../tsabre/graph.ts";
3
+ import type { NodeId, Point, Segment, SegmentId } from "../../types.ts";
4
+ import type { LayoutEdge } from "../types.ts";
5
+
6
+ import type { NoopLayoutManager } from "./manager.ts";
7
+
8
+ /** A class to delegate visibility checking for layout managers. */
9
+ export class VisibilityManager {
10
+ protected readonly entityVisible = new Map<NodeId | SegmentId, boolean>();
11
+
12
+ protected get offset(): Point {
13
+ return {
14
+ x: -this.layoutManager.panOffset.x,
15
+ y: -this.layoutManager.panOffset.y,
16
+ };
17
+ }
18
+
19
+ protected readonly layoutManager: NoopLayoutManager;
20
+ constructor(layoutManager: NoopLayoutManager) {
21
+ this.layoutManager = layoutManager;
22
+ }
23
+
24
+ isNodeVisible(id: NodeId): boolean {
25
+ return this.entityVisible.get(id) || false;
26
+ }
27
+
28
+ isSegmentVisible(segment: Segment<LayoutEdge>): boolean {
29
+ if (!this.layoutManager.graph.isTopSegment(segment)) return true;
30
+ const id = segmentId(segment);
31
+ return this.entityVisible.get(id) || false;
32
+ }
33
+
34
+ update(): void {
35
+ const graph = this.layoutManager.graph;
36
+ for (const id of graph.children()) {
37
+ this.entityVisible.set(id, this.nodeVisibility(id));
38
+ }
39
+
40
+ // lower level segments are either renderered or not based on the hull,
41
+ // and don't need visibility checks.
42
+ for (const segment of graph.topSegments) {
43
+ const id = segmentId(segment);
44
+ this.entityVisible.set(id, this.segmentVisibility(segment));
45
+ }
46
+ }
47
+
48
+ /**
49
+ * A node is visible when it's top level, or its parent is expanded. If
50
+ * (x, y, r) are provided, a node is visible when any part of it is in page
51
+ * bounds.
52
+ */
53
+ private nodeVisibility(id: NodeId): boolean {
54
+ const pid = this.layoutManager.graph.parent(id);
55
+ // Children use parent visibility
56
+ if (this.layoutManager.graph.hasNode(pid)) return this.nodeVisibility(pid);
57
+
58
+ // If the node hasn't been lain out yet, it's not visible.
59
+ if (!this.layoutManager.hasNodePosition(id)) return false;
60
+
61
+ const position = this.layoutManager.getNodePosition(id);
62
+ if (!position) return false;
63
+ return circleIntersectsBox(
64
+ position,
65
+ position.radius,
66
+ this.offset,
67
+ this.layoutManager.pageBounds,
68
+ );
69
+ }
70
+
71
+ protected segmentVisibility(segment: Segment<LayoutEdge>): boolean {
72
+ // The segment is top level, check if its path passes through the
73
+ // bounding box.
74
+ if (!this.layoutManager.hasSegmentPosition(segment)) return false;
75
+ const segmentPosition = this.layoutManager.getSegmentPosition(segment);
76
+ const { points } = segmentPosition;
77
+ for (let i = 0; i < points.length - 1; i++) {
78
+ if (lineIntersectsBox(points[i], points[i + 1], this.offset, this.layoutManager.pageBounds)) {
79
+ return true;
80
+ }
81
+ }
82
+ return false;
83
+ }
84
+ }