@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.
- 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,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
|
+
}
|