@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
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Fast, efficient, typesafe, custom quadtree that is capable of storing
3
+ * arbitrary polygons.
4
+ *
5
+ * http://www.cs.umd.edu/~meesh/cmsc420/ContentBook/FormalNotes/SametNotes/pm_quadtree_samet.pdf
6
+ *
7
+ * Why not:
8
+ * * goog.struct.Quadtree
9
+ * * d3.Quadtree
10
+ * Only store points; while geometries could be handled with bounds checking.
11
+ * Don't support lines & line crossing.
12
+ */
13
+
14
+ import { assert } from "../assert.ts";
15
+ import type { NodeId, Point, SegmentId } from "../types.ts";
16
+
17
+ import { RectangleGeometry } from "./rectangle.ts";
18
+ import type { Geometry, PolyLine } from "./types.ts";
19
+
20
+ /** Maximum number of elements before splitting the quad tree. */
21
+ const QUAD_TREE_SIZE = 4;
22
+
23
+ /**
24
+ * An interface for a struct holding data managed by a QuadTree.
25
+ *
26
+ * See http://screen/C2qW9hmGE5RQJTg for a diagram.
27
+ */
28
+ interface QuadTreeEntry {
29
+ /**
30
+ * The root QuadTree managing this entry. Needed when re-inserting items
31
+ * during splitting to track bookkeeping apporpriately.
32
+ */
33
+ root: QuadTree;
34
+ /** Whether this Entry is at the leaf, in which case it has no quadrants. */
35
+ isLeaf: boolean;
36
+ /** The four quadrants that make up the children of this QuadTree. */
37
+ quadrants?: [QuadTreeEntry, QuadTreeEntry, QuadTreeEntry, QuadTreeEntry];
38
+ /** Parent quad tree entry, to walk up for larger neighborhoods. */
39
+ parent?: QuadTreeEntry;
40
+ /**
41
+ * The covered bounds of this quad tree. The bounds of the four quadrants
42
+ * will fit inside these bounds.
43
+ */
44
+ bounds: Geometry;
45
+ /**
46
+ * The nodes whose geometries are completely contained in this Entry, but do
47
+ * not completely fit in any single quadrant of this entry.
48
+ */
49
+ children: Map<NodeId, Geometry>;
50
+ segments: Map<SegmentId, PolyLine>;
51
+ }
52
+
53
+ interface RegionSet {
54
+ nodes: Set<NodeId>;
55
+ segments: Set<PolyLine>;
56
+ }
57
+
58
+ function makeQuadTreeEntry(
59
+ root: QuadTree,
60
+ bounds: Geometry,
61
+ parent?: QuadTreeEntry,
62
+ ): QuadTreeEntry {
63
+ return {
64
+ isLeaf: true,
65
+ bounds,
66
+ root,
67
+ parent,
68
+ children: new Map<NodeId, Geometry>(),
69
+ segments: new Map<SegmentId, PolyLine>(),
70
+ };
71
+ }
72
+
73
+ function quadTreeRegion(
74
+ tree: QuadTreeEntry,
75
+ query: RegionSet,
76
+ region: Geometry,
77
+ size: number,
78
+ ): void {
79
+ for (const quadrant of tree.quadrants || []) {
80
+ if (quadrant.bounds.intersects(region)) {
81
+ quadTreeRegion(quadrant, query, region, size);
82
+ }
83
+ }
84
+
85
+ for (const [id, geom] of tree.children.entries()) {
86
+ if (query.nodes.has(id)) continue;
87
+ if (geom.intersects(region)) {
88
+ query.nodes.add(id);
89
+ }
90
+ }
91
+
92
+ for (const segment of tree.segments.values()) {
93
+ if (query.segments.has(segment)) continue;
94
+ const { points } = segment;
95
+ for (let i = 1; i < points.length; i++) {
96
+ const a = points[i - 1];
97
+ const b = points[i];
98
+ if (region.lineIntersects(a, b)) {
99
+ query.segments.add(segment);
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ /* Given a leaf quad tree entry, split it into four child quadrants and reinsert
106
+ * all children to update their positions. Each quadrants is created as a leaf
107
+ * so that the bounds information is always available, but that leaf will not be
108
+ * expanded until it grows larger than the carrying capacity. This results in at
109
+ * worst 3 empty quadrants per level of the tree, or O(1) memory overhead.
110
+ */
111
+ function insertQuadTree(root: QuadTreeEntry, id: NodeId, geometry: Geometry): QuadTreeEntry {
112
+ if (!root.bounds.contains(geometry)) {
113
+ const entry = growQuadTree(root);
114
+ return insertQuadTree(entry, id, geometry);
115
+ }
116
+ if (root.isLeaf) {
117
+ if (root.children.size > QUAD_TREE_SIZE) {
118
+ splitQuadTree(root);
119
+ return insertQuadTree(root, id, geometry);
120
+ } else {
121
+ root.children.set(id, geometry);
122
+ return root;
123
+ }
124
+ } else {
125
+ for (const quadrant of root.quadrants || []) {
126
+ if (quadrant.bounds.contains(geometry)) {
127
+ return insertQuadTree(quadrant, id, geometry);
128
+ }
129
+ }
130
+ // The item spans multiple quadrants, add it to this node directly.
131
+ root.children.set(id, geometry);
132
+ return root;
133
+ }
134
+ }
135
+
136
+ function crossQuadTree(
137
+ tree: QuadTreeEntry,
138
+ set: Set<QuadTreeEntry>,
139
+ id: SegmentId,
140
+ segment: PolyLine,
141
+ a: Point,
142
+ b: Point,
143
+ ): void {
144
+ if (tree.isLeaf) {
145
+ tree.segments.set(id, segment);
146
+ set.add(tree);
147
+ } else {
148
+ for (const quadrant of tree.quadrants || []) {
149
+ if (quadrant.bounds.lineIntersects(a, b)) {
150
+ crossQuadTree(quadrant, set, id, segment, a, b);
151
+ }
152
+ }
153
+ }
154
+ }
155
+
156
+ function splitQuadTree(tree: QuadTreeEntry): void {
157
+ assert(tree.isLeaf, "Can only split leaf QuadTreeEntries");
158
+ tree.isLeaf = false;
159
+ const { width, x, y } = tree.bounds;
160
+ const side = width / 2;
161
+ const halfSide = side / 2;
162
+ tree.quadrants = [
163
+ makeQuadTreeEntry(
164
+ tree.root,
165
+ RectangleGeometry.square({
166
+ side,
167
+ point: { x: x + halfSide, y: y + halfSide },
168
+ }),
169
+ tree,
170
+ ),
171
+ makeQuadTreeEntry(
172
+ tree.root,
173
+ RectangleGeometry.square({
174
+ side,
175
+ point: { x: x - halfSide, y: y + halfSide },
176
+ }),
177
+ tree,
178
+ ),
179
+ makeQuadTreeEntry(
180
+ tree.root,
181
+ RectangleGeometry.square({
182
+ side,
183
+ point: { x: x - halfSide, y: y - halfSide },
184
+ }),
185
+ tree,
186
+ ),
187
+ makeQuadTreeEntry(
188
+ tree.root,
189
+ RectangleGeometry.square({
190
+ side,
191
+ point: { x: x + halfSide, y: y - halfSide },
192
+ }),
193
+ tree,
194
+ ),
195
+ ];
196
+
197
+ const { children, segments } = tree;
198
+ tree.children = new Map();
199
+ for (const [id, geom] of children.entries()) {
200
+ tree.root.set(id, geom);
201
+ }
202
+ for (const [id, polyline] of segments.entries()) {
203
+ tree.root.link(id, polyline);
204
+ }
205
+ }
206
+
207
+ /* Growing a quad tree takes the current root node, and makes it one quadrants
208
+ * of a node the next level up. The next node then has twice the side length and
209
+ * four times the area. The center of the next node up is either the origin, if
210
+ * the old tree is rooted not at the origin, or at the corner opposite the
211
+ * origin, if the old tree is rooted at the origin. Thus, every root will either
212
+ * be centered at the origin, or it will have one corner at the origin. By
213
+ * consistently growing in this manner, the tree will grow neer the origin. It
214
+ * is assumed that points will be preferentially near the origin.
215
+ */
216
+ function growQuadTree(tree: QuadTreeEntry): QuadTreeEntry {
217
+ const { height: side, x, y } = tree.bounds;
218
+ const atOrigin = x === 0 && y === 0;
219
+ const point = atOrigin ? { x: side, y: side } : { x: 0, y: 0 };
220
+
221
+ const entry = makeQuadTreeEntry(tree.root, RectangleGeometry.square({ side: side * 2, point }));
222
+ entry.isLeaf = false;
223
+ if (atOrigin) {
224
+ // Old tree is at <0, 0>, create the other quadrants
225
+ entry.quadrants = [
226
+ tree,
227
+ makeQuadTreeEntry(tree.root, RectangleGeometry.square({ side, point: { x: side, y: 0 } })),
228
+ makeQuadTreeEntry(tree.root, RectangleGeometry.square({ side, point: { x: 0, y: side } })),
229
+ makeQuadTreeEntry(tree.root, RectangleGeometry.square({ side, point: { x: side, y: side } })),
230
+ ];
231
+ } else {
232
+ // Old tree is at <half, half>, create the other quadrants
233
+ const half = side / 2;
234
+ entry.quadrants = [
235
+ tree,
236
+ makeQuadTreeEntry(
237
+ tree.root,
238
+ RectangleGeometry.square({ side, point: { x: -half, y: half } }),
239
+ ),
240
+ makeQuadTreeEntry(
241
+ tree.root,
242
+ RectangleGeometry.square({ side, point: { x: -half, y: -half } }),
243
+ ),
244
+ makeQuadTreeEntry(
245
+ tree.root,
246
+ RectangleGeometry.square({ side, point: { x: half, y: -half } }),
247
+ ),
248
+ ];
249
+ }
250
+ rePlant(entry);
251
+ return entry;
252
+ }
253
+
254
+ function rePlant(entry: QuadTreeEntry) {
255
+ // "friend" method
256
+ (entry.root as unknown as { root: QuadTreeEntry }).root = entry;
257
+ for (const quad of entry.quadrants || []) {
258
+ quad.parent = entry;
259
+ }
260
+ }
261
+
262
+ /** QuadTree implementation. */
263
+ export class QuadTree {
264
+ protected children = new Map<NodeId, QuadTreeEntry>();
265
+ protected segments = new Map<SegmentId, Set<QuadTreeEntry>>();
266
+ protected root: QuadTreeEntry = makeQuadTreeEntry(
267
+ this,
268
+ RectangleGeometry.square({ side: 256, point: { x: 128, y: 128 } }),
269
+ );
270
+
271
+ /** Mark id in the appropriate quad given the geometry. */
272
+ set(id: NodeId, geometry: Geometry): void {
273
+ if (this.children.has(id)) {
274
+ this.children.get(id)!.children.delete(id);
275
+ this.children.delete(id);
276
+ }
277
+ const tree = insertQuadTree(this.root, id, geometry);
278
+ this.children.set(id, tree);
279
+ }
280
+
281
+ /**
282
+ * Mark line as crossing the appropriate quad given the geometry. Both source
283
+ * and target nodes must already be in the quadtree.
284
+ */
285
+ link(segmentId: SegmentId, polyline: PolyLine): void {
286
+ if (this.segments.has(segmentId)) {
287
+ for (const entry of this.segments.get(segmentId)!) {
288
+ entry.segments.delete(segmentId);
289
+ }
290
+ this.segments.delete(segmentId);
291
+ }
292
+ const set = new Set<QuadTreeEntry>();
293
+ for (let i = 1; i < polyline.points.length; i++) {
294
+ crossQuadTree(
295
+ this.root,
296
+ set,
297
+ segmentId,
298
+ polyline,
299
+ polyline.points[i - 1],
300
+ polyline.points[i],
301
+ );
302
+ }
303
+ this.segments.set(segmentId, set);
304
+ }
305
+
306
+ /**
307
+ * Given a geometry, return all nodes and segments that could be in that
308
+ * region. Contained nodes' IDs are included, as well as IDs for nodes ending
309
+ * segments which originate from, terminate in, or cross over this quadtree.
310
+ *
311
+ * For nodes with geometries that would be smaller than a certain size, return
312
+ * an ID for a composite node with all the children. ??
313
+ */
314
+ region(geometry: Geometry, size = 1): RegionSet {
315
+ const nodes = new Set<NodeId>();
316
+ const segments = new Set<PolyLine>();
317
+ const region = { nodes, segments };
318
+ quadTreeRegion(this.root, region, geometry, size);
319
+ return region;
320
+ }
321
+ }
@@ -0,0 +1,129 @@
1
+ import type { Bounds, Point } from "../types.ts";
2
+
3
+ import { BASE_RADIUS, BaseGeometry, clipi, type GeometryParams, origin } from "./geometry.ts";
4
+ import type { Line } from "./types.ts";
5
+
6
+ /** An interface describing the input for a square. */
7
+ export interface SquareParams extends GeometryParams {
8
+ /** Length of one side of the square. */
9
+ side: number;
10
+ }
11
+
12
+ const SQUARE_SIDE = BASE_RADIUS * 2;
13
+
14
+ /** An interface describing the input for a rectangle. */
15
+ export interface RectangleParams extends GeometryParams {
16
+ /** Length along the X axis. */
17
+ width: number;
18
+
19
+ /** Length along the Y axis. */
20
+ height: number;
21
+ }
22
+
23
+ // Constants to help clipi
24
+ const R45 = (-45 * Math.PI) / 180;
25
+ const COS45 = Math.cos(R45);
26
+ const SIN45 = Math.sin(R45);
27
+
28
+ /** Implementation of a Geometry representing an axis-aligned rectangle. */
29
+ export class RectangleGeometry extends BaseGeometry {
30
+ // Width / height
31
+ private aspect!: number;
32
+
33
+ static square({ side, point }: { side?: number; point?: Point }): RectangleGeometry {
34
+ return new RectangleGeometry({ width: side, height: side, point });
35
+ }
36
+
37
+ static plane(): RectangleGeometry {
38
+ return new RectangleGeometry({ width: Number.MAX_SAFE_INTEGER });
39
+ }
40
+
41
+ constructor({
42
+ width = SQUARE_SIDE * 2,
43
+ height = SQUARE_SIDE,
44
+ point = origin(),
45
+ }: Readonly<Partial<RectangleParams>> = {}) {
46
+ super({ point });
47
+ this.resize({ height, width });
48
+ }
49
+
50
+ makeClone(): RectangleGeometry {
51
+ const { height, width } = this;
52
+ return new RectangleGeometry({ point: this, height, width });
53
+ }
54
+
55
+ resize(size: number | Bounds): void {
56
+ let height: number;
57
+ let width: number;
58
+
59
+ if (typeof size === "number") {
60
+ height = width = size;
61
+ } else {
62
+ height = size.height;
63
+ width = size.width;
64
+ }
65
+
66
+ this.bounds.width = width;
67
+ this.bounds.height = height;
68
+ this.aspect = width / height;
69
+ }
70
+
71
+ clipi(line: Line): void {
72
+ // To quickly clip the line to an edge, perform an affine transformation on
73
+ // the line's source point. This affine transoformation aligns the point
74
+ // along the rectangle's diagonals as normal axes. We then rotate the point
75
+ // 45degrees counterclockwise so that the diagonales align the unit axes.
76
+ // This allows a simple quadrant check to choose which rectangle edge to
77
+ // clip to.
78
+
79
+ let { x, y } = line[0];
80
+ x -= this.x;
81
+ y -= this.y;
82
+
83
+ x /= this.aspect;
84
+
85
+ const x2 = x * COS45 - y * SIN45;
86
+ y = x * SIN45 + y * COS45;
87
+ x = x2;
88
+
89
+ // Based on angle from the origin to the line start, choose an edge.
90
+ // Because the line point has been projected and rotated, a simple quadrant
91
+ // check tells the corners to use for the edge.
92
+ const h2 = this.height / 2;
93
+ const w2 = this.width / 2;
94
+ let edge: Line;
95
+ if (x >= 0 && y >= 0) {
96
+ edge = [
97
+ { x: w2, y: h2 },
98
+ { x: -w2, y: h2 },
99
+ ];
100
+ } else if (x <= 0 && y >= 0) {
101
+ edge = [
102
+ { x: -w2, y: h2 },
103
+ { x: -w2, y: -h2 },
104
+ ];
105
+ } else if (x <= 0 && y <= 0) {
106
+ edge = [
107
+ { x: -w2, y: -h2 },
108
+ { x: w2, y: -h2 },
109
+ ];
110
+ } else if (x >= 0 && y <= 0) {
111
+ edge = [
112
+ { x: w2, y: -h2 },
113
+ { x: w2, y: h2 },
114
+ ];
115
+ } else {
116
+ throw new Error("Non-coplanar line clipped to rectangle");
117
+ }
118
+
119
+ // Clip line to edge
120
+ clipi(line, edge);
121
+ }
122
+ }
123
+
124
+ /** Implementation of a geometry representing an axis-aligned square. */
125
+ export class SquareGeometry extends RectangleGeometry {
126
+ constructor({ side = SQUARE_SIDE, point = origin() }: Readonly<Partial<SquareParams>> = {}) {
127
+ super({ width: side, height: side, point });
128
+ }
129
+ }
@@ -0,0 +1,132 @@
1
+ import { assertExists } from "../assert.ts";
2
+ import type { Bounds, Point } from "../types.ts";
3
+
4
+ import { CircleGeometry } from "./circle.ts";
5
+ import { BASE_RADIUS, BaseGeometry, clipi, type GeometryParams, origin } from "./geometry.ts";
6
+ import type { Geometry, Line, Radian } from "./types.ts";
7
+
8
+ /**
9
+ * Parameters to create an rngon.
10
+ *
11
+ * For a surprisingly good overview, see
12
+ * https://www.mathsisfun.com/geometry/regular-polygons.html
13
+ */
14
+ export interface RNGonParams extends GeometryParams {
15
+ /** Number of sides of the RNGon. */
16
+ n: number;
17
+
18
+ rotation: Radian;
19
+
20
+ /**
21
+ * Radius of the NGon. All points along the polygon will be this distance from
22
+ * the center.
23
+ */
24
+ radius: number;
25
+ }
26
+
27
+ const RNGON_N = 6;
28
+ const RNGON_ROTATION = 0;
29
+
30
+ /**
31
+ * An RNGon is a Regular N-verticy Polygon. N must be greater than 2. Regular is
32
+ * a light misnomer, as the class provides rotation and affine transformations.
33
+ * An RNGon with a side length less than 1 creates a circle. The RNGon will be
34
+ * rotated by `rotation` radians. This is easiest to visualize with a triangle,
35
+ * which will "point" to the right with zero rotation. Positive rotation will
36
+ * spin the RNGon counter-clockwise.
37
+ */
38
+ export class RNGon extends BaseGeometry {
39
+ private readonly n: number;
40
+ private rad: number;
41
+ private rotation: Radian;
42
+ private sideAngle!: number;
43
+ // Has N+1 points, as first point is duplicated to make clipi easier
44
+ private readonly points: Array<Readonly<Point>> = [];
45
+
46
+ static circle({ point, radius }: Partial<Omit<RNGonParams, "n">> = {}): Geometry {
47
+ return new CircleGeometry({ point, radius });
48
+ }
49
+
50
+ static point(point: Point = { x: 0, y: 0 }): Geometry {
51
+ return new CircleGeometry({ point, radius: 0 });
52
+ }
53
+
54
+ override get radius(): number {
55
+ return this.rad;
56
+ }
57
+
58
+ constructor({
59
+ n = RNGON_N,
60
+ radius = BASE_RADIUS,
61
+ rotation = RNGON_ROTATION,
62
+ point = origin(),
63
+ }: Readonly<Partial<RNGonParams>> = {}) {
64
+ super({ point });
65
+ this.n = n;
66
+ this.rad = radius;
67
+ this.rotation = rotation;
68
+ this.update();
69
+ }
70
+
71
+ override makeClone(): RNGon {
72
+ const { n, radius, rotation } = this;
73
+ return new RNGon({ n, radius, rotation, point: this });
74
+ }
75
+
76
+ resize(size: number | Bounds): void {
77
+ if (typeof size === "number") {
78
+ this.rad = size;
79
+ } else {
80
+ this.rad = Math.max(size.width, size.height) / 2;
81
+ }
82
+ this.update();
83
+ }
84
+
85
+ private update() {
86
+ const anglePart = Math.PI / this.n;
87
+ this.sideAngle = 2 * anglePart;
88
+
89
+ this.updatePoints();
90
+ this.updateBox();
91
+ }
92
+
93
+ private updatePoints() {
94
+ this.points.splice(0, this.points.length);
95
+ const theta = this.rotation;
96
+ const maxTheta = this.rotation + 2 * Math.PI;
97
+ const step = (2 * Math.PI) / this.n;
98
+ for (let t = theta; t <= maxTheta; t += step) {
99
+ const x = Math.cos(t) * this.radius;
100
+ const y = Math.sin(t) * this.radius;
101
+ this.points.push({ x, y });
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Update the box. Must call updatePoints first. See public `update` method.
107
+ */
108
+ private updateBox() {
109
+ const xs = this.points.map(({ x }) => x);
110
+ const ys = this.points.map(({ y }) => y);
111
+ const maxX = Math.max(...xs);
112
+ const minX = Math.min(...xs);
113
+ const maxY = Math.max(...ys);
114
+ const minY = Math.min(...ys);
115
+ this.bounds.width = maxX - minX;
116
+ this.bounds.height = maxY - minY;
117
+ }
118
+
119
+ clipi(line: Line): void {
120
+ // Find edge closest to start of the line
121
+ const dx = line[1].x - this.x;
122
+ const dy = line[1].y - this.y;
123
+ const theta = (Math.atan2(dx, dy) + this.rotation) % (2 * Math.PI);
124
+ const side = Math.floor(theta / this.sideAngle);
125
+
126
+ const edgeStart = assertExists(this.points[side]);
127
+ const edgeEnd = assertExists(this.points[side + 1]);
128
+
129
+ // Clip to closest edge
130
+ clipi(line, [edgeStart, edgeEnd]);
131
+ }
132
+ }
@@ -0,0 +1,102 @@
1
+ import type { Bounds, NodeId, Point } from "../types.ts";
2
+
3
+ /** Nominal type for radians */
4
+ export type Radian = number;
5
+
6
+ /** A line is a simple 2vec stored as a tuple. */
7
+ export type Line = [Point, Point];
8
+
9
+ /**
10
+ * Polyline has starting and ending nodes, with some number of intermediate
11
+ * points.
12
+ */
13
+ export interface PolyLine {
14
+ source: NodeId;
15
+ target: NodeId;
16
+ points: [Point, Point, ...Point[]];
17
+ }
18
+
19
+ /**
20
+ * A Geometry has a Point and Bound in a vector space, and can clip lines to
21
+ * its boundaries.
22
+ */
23
+ export interface Geometry extends Bounds, Point {
24
+ readonly left: number;
25
+ readonly right: number;
26
+ readonly top: number;
27
+ readonly bottom: number;
28
+
29
+ /** Utility for getting just the {x, y} point. */
30
+ readonly point: Readonly<Point>;
31
+
32
+ /** The smallest radius to completely contain this geometry. */
33
+ readonly radius: number;
34
+
35
+ /** Update a Geometry's position by a point delta. */
36
+ move(delta: Point): void;
37
+
38
+ /** Update a Geometry's position to an explicit point. */
39
+ moveTo(point: Point): void;
40
+
41
+ /** Update a Geometry's user-contributed drag position by a point delta. */
42
+ drag(delta: Point): void;
43
+
44
+ /**
45
+ * Update a Geometry's user-contributed drag position to an explicit point.
46
+ */
47
+ dragTo(point: Point): void;
48
+
49
+ /**
50
+ * Change the second point in line to be on the edge of the Geometry nearest
51
+ * the first point of the line. No change if the starting point is inside the
52
+ * geometry or if the end point is outside the geometry.
53
+ *
54
+ * Short for "clip _i_nplace".
55
+ *
56
+ * Users of a Geometry should prefer this method for performance.
57
+ */
58
+ clipi(line: Line): void;
59
+
60
+ /**
61
+ * Return the point of intersection between the line and the line on the
62
+ * geometry closes to the first point of the line. No change if the starting
63
+ * point is inside the geometry or if the end point is outside the geometry.
64
+ *
65
+ * Users of a Geometry should prefer calling this method for clarity.
66
+ */
67
+ clip(line: Readonly<Line>): Point;
68
+
69
+ /**
70
+ * Resize the geometry as appropriate for the geometry class. Resizing to a
71
+ * radius will attempt to resize to a consistent size (circular/square/regular
72
+ * polygon). Resizing to a bounds will attempt to create the geometry with
73
+ * an aspect ratio matching the bounds, aligned to the axis.
74
+ */
75
+ resize(size: number | Bounds): void;
76
+
77
+ /**
78
+ * Treating both geometries as Axis-Aligned Bounding Boxes, test whether
79
+ * `geometry` is contained inside `this`.
80
+ */
81
+ contains(geometry: Geometry): boolean;
82
+
83
+ /**
84
+ * Treating both geometries as Axis-aligned Bounding Boxes, test whether
85
+ * `geometry` intersects `this`.
86
+ */
87
+ intersects(geometry: Geometry): boolean;
88
+
89
+ /**
90
+ * Given two points, checks whether a line segment connecting those points
91
+ * intersects with the AABB of this geometry.
92
+ */
93
+ lineIntersects(a: Point, b: Point): boolean;
94
+
95
+ /**
96
+ * Given a collection of geometries, change this geometry's size to contain
97
+ * them, and update all positions to remain centered.
98
+ */
99
+ fitTo(children: Iterable<Geometry>, padding: number): void;
100
+
101
+ clone(): Geometry;
102
+ }