@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.
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
package/package.json CHANGED
@@ -1,6 +1,21 @@
1
1
  {
2
2
  "name": "@davidsouther/agathon",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "description": "Graph data structure, algorithms, and hierarchic layout.",
6
+ "license": "MIT",
7
+ "repository": "git+https://github.com/DavidSouther/agathon.git",
8
+ "sideEffects": false,
9
+ "files": [
10
+ "src",
11
+ "!src/**/*_test.ts",
12
+ "!src/**/testing.ts"
13
+ ],
14
+ "exports": {
15
+ ".": "./src/index.ts",
16
+ "./*": "./src/*.ts"
17
+ },
18
+ "scripts": {
19
+ "test": "node --test \"src/**/*_test.ts\""
20
+ }
21
+ }
package/src/assert.ts ADDED
@@ -0,0 +1,67 @@
1
+ type AssertMessage = string | (() => string);
2
+
3
+ /**
4
+ * Throw an error when a condition is not met.
5
+ */
6
+ export function assert(
7
+ condition: boolean,
8
+ message: AssertMessage = "Assertion failed",
9
+ warn = false,
10
+ ): void | never {
11
+ if (!condition) {
12
+ message = message instanceof Function ? message() : message;
13
+ if (warn) {
14
+ console.warn(message);
15
+ } else {
16
+ throw new Error(message);
17
+ }
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Given a `number|undefined|null`, assert that it is not undefined, not NaN, and not +-Infinity.
23
+ */
24
+ export function assertFinite(
25
+ n: number | undefined | null,
26
+ message: AssertMessage = `Assertion failed: Value ${n} is not finite`,
27
+ ): number | never {
28
+ assert(n != null && !isNaN(n) && n !== Infinity && n !== -Infinity, message);
29
+ return n as number;
30
+ }
31
+
32
+ /**
33
+ * Given a value, return it if it is not null nor undefined. Otherwise throw an
34
+ * error.
35
+ *
36
+ * @template T
37
+ * @returns {NonNullable<T>}
38
+ */
39
+ export function assertExists<T>(
40
+ t: T,
41
+ message: AssertMessage = "Assertion failed: value does not exist",
42
+ ): NonNullable<T> {
43
+ assert(t != null, message);
44
+ return t as NonNullable<T>;
45
+ }
46
+
47
+ /**
48
+ * @param {*} n
49
+ * @returns string
50
+ */
51
+ export function assertString(
52
+ n: unknown,
53
+ message: AssertMessage = () => `Assertion failed: ${n} is not a string`,
54
+ ): string {
55
+ assert(typeof n === "string", message);
56
+ return n as string;
57
+ }
58
+
59
+ /**
60
+ * Compile time assertion that no value will used at this point in control flow.
61
+ */
62
+ export function checkExhaustive(
63
+ value: never,
64
+ message: AssertMessage = `Unexpected value ${value}`,
65
+ ): never {
66
+ throw new Error(message instanceof Function ? message() : message);
67
+ }
@@ -0,0 +1,76 @@
1
+ import type { Bounds, Point } from "../types.ts";
2
+
3
+ /**
4
+ * Given an axis-aligned box and a line defined by two end points, determine if
5
+ * any point of the line is within the box. Takes the start and end points of
6
+ * the line segment, the upper-left point of the box, and the height and width
7
+ * of the box. This aligns with the data types in Layout Managers.
8
+ *
9
+ * @param pointA the starting point of a line segment.
10
+ * @param pointB the end point of a line segment.
11
+ * In normal form, the line will be at pointA at t=0 and pointB at t=1.
12
+ * @param boxPoint the top-left corner of the axis-aligned box.
13
+ * @param boxBounds the height and width of the box.
14
+ */
15
+ export function lineIntersectsBox(
16
+ { x: ax, y: ay }: Point,
17
+ { x: bx, y: by }: Point,
18
+ { x: px, y: py }: Point,
19
+ { height, width }: Bounds,
20
+ ): boolean {
21
+ // Align points to (0, 0)
22
+ ax -= px;
23
+ bx -= px;
24
+ ay -= py;
25
+ by -= py;
26
+
27
+ // Sort line to go from top to bottom
28
+ if (ay > by) {
29
+ const tx = bx;
30
+ const ty = by;
31
+ bx = ax;
32
+ by = ay;
33
+ ax = tx;
34
+ ay = ty;
35
+ }
36
+
37
+ // Line goes left to right or right to left
38
+ if (ax < bx) {
39
+ // Line goes from left to right, so a must be smaller than the bottom-right
40
+ // corner and b must be greater than the top-left corner.
41
+ return ax <= width && ay <= height && bx >= 0 && by >= 0;
42
+ } else {
43
+ // Line goes from right to left, so a must be smaller than the top-left
44
+ // corner and b must be greater than the bottom-right corner.
45
+ return ax >= 0 && ay <= height && bx <= width && by >= 0;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Given a circle and a box, return whether any part of the circle overlaps the
51
+ * box. This check is performed by offseting the center point by its radius in
52
+ * axis-aligned directions, and checking if that point is within the
53
+ * origin-centered axis-aligned box.
54
+ *
55
+ * @param center point of the circle.
56
+ * @param radius of the circle.
57
+ * @param boxPoint the top-left corner of the axis-aligned box.
58
+ * @param boxBounds the height and width of the box.
59
+ */
60
+ export function circleIntersectsBox(
61
+ { x: cx, y: cy }: Point,
62
+ r: number,
63
+ { x: px, y: py }: Point,
64
+ { height, width }: Bounds,
65
+ ): boolean {
66
+ // Align points to (0, 0)
67
+ cx -= px;
68
+ cy -= py;
69
+
70
+ const xl = cx + r;
71
+ const xr = cx - r;
72
+ const yt = cy + r;
73
+ const yb = cy - r;
74
+
75
+ return xl >= 0 && xr <= width && yt >= 0 && yb <= height;
76
+ }
@@ -0,0 +1,296 @@
1
+ import { checkExhaustive } from "../assert.ts";
2
+ import type { Bounds, Circle, Point } from "../types.ts";
3
+
4
+ import {
5
+ BASE_RADIUS,
6
+ BaseGeometry,
7
+ clipicircle,
8
+ EPSILON,
9
+ type GeometryParams,
10
+ origin,
11
+ } from "./geometry.ts";
12
+ import type { Geometry, Line } from "./types.ts";
13
+
14
+ /** Params to create a circle */
15
+ export interface CircleParams extends GeometryParams {
16
+ radius: number;
17
+ }
18
+
19
+ /** Geometry for a circle (or a point with small radius) */
20
+ export class CircleGeometry extends BaseGeometry {
21
+ private innderRadius!: number;
22
+
23
+ get isPoint(): boolean {
24
+ return this.radius < EPSILON;
25
+ }
26
+
27
+ override get radius(): number {
28
+ return this.innderRadius;
29
+ }
30
+
31
+ static point(point: Point = origin()): CircleGeometry {
32
+ return new CircleGeometry({ point, radius: 0 });
33
+ }
34
+
35
+ constructor({ point = origin(), radius = BASE_RADIUS }: Readonly<Partial<CircleParams>> = {}) {
36
+ super({ point });
37
+ this.resize(radius);
38
+ }
39
+
40
+ makeClone(): CircleGeometry {
41
+ return new CircleGeometry({ point: this, radius: this.radius });
42
+ }
43
+
44
+ resize(size: number | Bounds): void {
45
+ if (typeof size === "number") {
46
+ this.innderRadius = size;
47
+ } else {
48
+ this.innderRadius = Math.max(size.width, size.height) / 2;
49
+ }
50
+ this.bounds.height = this.bounds.width = this.radius * 2;
51
+ }
52
+
53
+ clipi(line: Line): void {
54
+ if (this.isPoint) {
55
+ // A point will never clip
56
+ return;
57
+ }
58
+
59
+ const dx = line[0].x - this.point.x;
60
+ const dy = line[0].y - this.point.y;
61
+ if (dx * dx + dy * dy <= this.radius * this.radius) {
62
+ // Start point is inside the circle. While clipicircle can handle this
63
+ // case, to be consistent with the documentation that lines leaving the
64
+ // polygon are not clipped, this returns early.
65
+ return;
66
+ }
67
+ clipicircle(line, this.point, this.radius);
68
+ }
69
+
70
+ /**
71
+ * Given a collection of geometries, fit this geometry to contain them, and
72
+ * update all positions to remain centered.
73
+ */
74
+ override tightestFit(neighborhood: Iterable<Geometry>): Bounds & Point {
75
+ const neighborCircles: Circle[] = [];
76
+
77
+ // TODO: Make getSmallestEnclosingCircle take geometries. Until
78
+ // then, convert all the geometries into circles.
79
+ for (const neighbor of neighborhood) {
80
+ const { x, y, radius } = neighbor;
81
+ neighborCircles.push({ x, y, r: radius });
82
+ }
83
+
84
+ const { x, y, r } = getSmallestEnclosingCircle(neighborCircles, 0);
85
+
86
+ return { x, y, height: r * 2, width: r * 2 };
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Find the smallest radius such that the two circles adjacent with the largest
92
+ * combined radiuses are at least `separation` distance apart.
93
+ *
94
+ * For example, given a list with circles of radius [15, 20, 10, 10], the two
95
+ * largest adjacent circles are 15 and 20. To arrange these four points around a
96
+ * central point <0, 0> with the first two circles being a minimum of 35 pixels
97
+ * apart, the intersecting circle must have a radius of 40 pixels. Each point
98
+ * in the input list can then be projected that far from <0, 0>, and no enclosed
99
+ * circles will intersect.
100
+ */
101
+ export function smallestIntersectingRadius(circles: number[], minimumSpacing: number): number {
102
+ const circlesLength = circles.length;
103
+
104
+ if (circlesLength < 2) return 0;
105
+
106
+ // First, find the pair of circles with the largest combined separation.
107
+ const largestCombinedRadius = Math.max(
108
+ ...circles.map((r, i) => r + circles[(i + 1) % circlesLength]),
109
+ );
110
+
111
+ const separation = largestCombinedRadius + minimumSpacing;
112
+ const theta = (2 * Math.PI) / circlesLength;
113
+ const gammaInv = Math.sqrt(2 * (1 - Math.cos(theta)));
114
+ const radius = separation / gammaInv;
115
+
116
+ return radius;
117
+ }
118
+
119
+ /**
120
+ * Returns the smallest circle that contains the specified circles.
121
+ * Expected time complexity is O(n).
122
+ */
123
+ export function getSmallestEnclosingCircle(circles: Circle[], padding: number): Circle {
124
+ const enclosingCircle = enclosingCircleIntersectingCircles(circles, []) || {
125
+ x: 0,
126
+ y: 0,
127
+ r: 0,
128
+ };
129
+ enclosingCircle.r += padding;
130
+ return enclosingCircle;
131
+ }
132
+
133
+ /**
134
+ * Returns the smallest circle that contains the circles
135
+ * and intersects the boundaryCircles.
136
+ * Based on Emo Welzl's paper:
137
+ * https://www.inf.ethz.ch/personal/emo/PublFiles/SmallEnclDisk_LNCS555_91.ps
138
+ */
139
+ function enclosingCircleIntersectingCircles(
140
+ circles: Circle[],
141
+ boundaryCircles: Circle[],
142
+ ): Circle | null {
143
+ circles = circles.slice(); // don't modify caller's data
144
+
145
+ let minEnclosingCircle: Circle | null = null;
146
+
147
+ if (boundaryCircles.length === 1) {
148
+ minEnclosingCircle = { ...boundaryCircles[0] };
149
+ } else if (boundaryCircles.length === 2) {
150
+ minEnclosingCircle = circleIntersectingTwoCircles(boundaryCircles[0], boundaryCircles[1]);
151
+ } else if (boundaryCircles.length >= 3) {
152
+ const l = boundaryCircles.length;
153
+ return circleIntersectingThreeCircles(
154
+ boundaryCircles[l - 3],
155
+ boundaryCircles[l - 2],
156
+ boundaryCircles[l - 1],
157
+ );
158
+ }
159
+
160
+ const visitedCircles: Circle[] = [];
161
+
162
+ while (circles.length > 0) {
163
+ const currentCircle = circles.pop()!;
164
+
165
+ if (minEnclosingCircle && circleContainsCircle(minEnclosingCircle, currentCircle)) {
166
+ visitedCircles.push(currentCircle);
167
+ } else {
168
+ // Current circle isn't inside current minimal enclosing circle.
169
+ // This means it will define the new minimal enclosing circle.
170
+ // We are going to compute minimal circle for all visited circles with
171
+ // the current circle on it's boundary.
172
+ boundaryCircles.push(currentCircle);
173
+ minEnclosingCircle = enclosingCircleIntersectingCircles(visitedCircles, boundaryCircles);
174
+ boundaryCircles.pop();
175
+
176
+ visitedCircles.push(currentCircle);
177
+ }
178
+ }
179
+
180
+ return minEnclosingCircle;
181
+ }
182
+
183
+ /** Returns true if the specified circle1 contains the specified circle2. */
184
+ function circleContainsCircle(circle1: Circle, circle2: Circle): boolean {
185
+ const EPSILON = 1e-6;
186
+ const xc0 = circle1.x - circle2.x;
187
+ const yc0 = circle1.y - circle2.y;
188
+ return Math.sqrt(xc0 * xc0 + yc0 * yc0) < circle1.r - circle2.r + EPSILON;
189
+ }
190
+
191
+ /** Returns the smallest circle that intersects the two specified circles. */
192
+ function circleIntersectingTwoCircles(circle1: Circle, circle2: Circle): Circle {
193
+ if (circleContainsCircle(circle1, circle2)) return circle1;
194
+ if (circleContainsCircle(circle2, circle1)) return circle2;
195
+ const { x: x1, y: y1, r: r1 } = circle1;
196
+ const { x: x2, y: y2, r: r2 } = circle2;
197
+ const dx = x2 - x1;
198
+ const dy = y2 - y1;
199
+ const dr = r2 - r1;
200
+ const l = Math.sqrt(dx * dx + dy * dy);
201
+ return {
202
+ x: (x1 + x2 + (dx / l) * dr) / 2,
203
+ y: (y1 + y2 + (dy / l) * dr) / 2,
204
+ r: (l + r1 + r2) / 2,
205
+ };
206
+ }
207
+
208
+ /** Returns the smallest circle that intersects the three specified circles. */
209
+ function circleIntersectingThreeCircles(circle1: Circle, circle2: Circle, circle3: Circle): Circle {
210
+ const { x: x1, y: y1, r: r1 } = circle1;
211
+ const { x: x2, y: y2, r: r2 } = circle2;
212
+ const { x: x3, y: y3, r: r3 } = circle3;
213
+ const a2 = 2 * (x1 - x2);
214
+ const b2 = 2 * (y1 - y2);
215
+ const c2 = 2 * (r2 - r1);
216
+ const d2 = x1 * x1 + y1 * y1 - r1 * r1 - x2 * x2 - y2 * y2 + r2 * r2;
217
+ const a3 = 2 * (x1 - x3);
218
+ const b3 = 2 * (y1 - y3);
219
+ const c3 = 2 * (r3 - r1);
220
+ const d3 = x1 * x1 + y1 * y1 - r1 * r1 - x3 * x3 - y3 * y3 + r3 * r3;
221
+ const ab = a3 * b2 - a2 * b3;
222
+ const xa = (b2 * d3 - b3 * d2) / ab - x1;
223
+ const xb = (b3 * c2 - b2 * c3) / ab;
224
+ const ya = (a3 * d2 - a2 * d3) / ab - y1;
225
+ const yb = (a2 * c3 - a3 * c2) / ab;
226
+ const A = xb * xb + yb * yb - 1;
227
+ const B = 2 * (xa * xb + ya * yb + r1);
228
+ const C = xa * xa + ya * ya - r1 * r1;
229
+ const r = (-B - Math.sqrt(B * B - 4 * A * C)) / (2 * A);
230
+ return {
231
+ x: xa + xb * r + x1,
232
+ y: ya + yb * r + y1,
233
+ r,
234
+ };
235
+ }
236
+
237
+ /** returns a point on a circle divided into slices */
238
+ export function getPositionOnCircle(
239
+ radius: number,
240
+ numSlices: number,
241
+ slice: number,
242
+ rotationOffset = 0,
243
+ ): Point {
244
+ if (numSlices === 1) {
245
+ // for one slice return the center point
246
+ return { x: 0, y: 0 };
247
+ }
248
+ const sliceSize = (2 * Math.PI) / numSlices;
249
+ const angle = sliceSize * slice + (rotationOffset * Math.PI) / 180;
250
+ const x = radius * Math.cos(angle);
251
+ const y = radius * Math.sin(angle);
252
+ return { x, y };
253
+ }
254
+
255
+ /** Quadrants of a circle. */
256
+ export const Quadrant = {
257
+ TOP_RIGHT: 1,
258
+ TOP_LEFT: 2,
259
+ BOTTOM_LEFT: 3,
260
+ BOTTOM_RIGHT: 4,
261
+ } as const;
262
+ export type Quadrant = (typeof Quadrant)[keyof typeof Quadrant];
263
+
264
+ /**
265
+ * Return an SVG transform attribute string to translate to a point at the
266
+ * quadrant of a circle.
267
+ */
268
+ export function translateCircleQuadrant(
269
+ radius: number,
270
+ quadrant: Quadrant = Quadrant.TOP_RIGHT,
271
+ ): `translate(${number},${number})` {
272
+ let sx = 1,
273
+ sy = -1;
274
+ switch (quadrant) {
275
+ case Quadrant.TOP_RIGHT:
276
+ sx = 1;
277
+ sy = -1;
278
+ break;
279
+ case Quadrant.TOP_LEFT:
280
+ sx = -1;
281
+ sy = -1;
282
+ break;
283
+ case Quadrant.BOTTOM_LEFT:
284
+ sx = -1;
285
+ sy = 1;
286
+ break;
287
+ case Quadrant.BOTTOM_RIGHT:
288
+ sx = 1;
289
+ sy = -1;
290
+ break;
291
+ default:
292
+ checkExhaustive(quadrant);
293
+ }
294
+ const r = Math.abs(radius) / Math.SQRT2;
295
+ return `translate(${sx * r},${sy * r})`;
296
+ }