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