@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,318 @@
1
+ import type { Bounds, Point } from "../types.ts";
2
+
3
+ import { lineIntersectsBox } from "./box.ts";
4
+ import type { Geometry, Line } from "./types.ts";
5
+
6
+ /** Math.EPSILON is much to small for graphical geometry. */
7
+ export const EPSILON = 1e-2;
8
+
9
+ /** The base radius for Agathon geometry shapes. */
10
+ export const BASE_RADIUS = 20;
11
+
12
+ /** Origin point for default positions. */
13
+ export function origin(): Point {
14
+ return { x: 0, y: 0 };
15
+ }
16
+
17
+ /** Parameters for a base geometry. */
18
+ export interface GeometryParams {
19
+ point: Point;
20
+ }
21
+
22
+ /**
23
+ * BaseGeometry provides an implementation of clip, which provides convenience
24
+ * at the possible expense of small object performance.
25
+ */
26
+ export abstract class BaseGeometry implements Geometry {
27
+ protected position: Point = { x: 0, y: 0 };
28
+ protected dragPt: Point = { x: 0, y: 0 };
29
+ protected movePt: Point = { x: 0, y: 0 };
30
+ protected bounds: Bounds = { height: 0, width: 0 };
31
+
32
+ get point(): Readonly<Point> {
33
+ return this.position;
34
+ }
35
+ get x(): number {
36
+ return this.position.x;
37
+ }
38
+ get y(): number {
39
+ return this.position.y;
40
+ }
41
+ get radius(): number {
42
+ return Math.sqrt(this.height * this.height + this.width * this.width) / 2;
43
+ }
44
+ get height(): number {
45
+ return this.bounds.height;
46
+ }
47
+ get width(): number {
48
+ return this.bounds.width;
49
+ }
50
+ get left(): number {
51
+ return this.x - this.bounds.width / 2;
52
+ }
53
+ get right(): number {
54
+ return this.x + this.bounds.width / 2;
55
+ }
56
+ get bottom(): number {
57
+ return this.y - this.bounds.height / 2;
58
+ }
59
+ get top(): number {
60
+ return this.y + this.bounds.height / 2;
61
+ }
62
+
63
+ constructor({ point = origin() }: Readonly<Partial<GeometryParams>>) {
64
+ // Defensive copy of the point
65
+ this.movePt.x = point.x;
66
+ this.movePt.y = point.y;
67
+ this.place();
68
+ }
69
+
70
+ private place(): void {
71
+ this.position.x = this.movePt.x + this.dragPt.x;
72
+ this.position.y = this.movePt.y + this.dragPt.y;
73
+ }
74
+
75
+ drag({ x: dx, y: dy }: Point): void {
76
+ this.dragPt.x += dx;
77
+ this.dragPt.y += dy;
78
+ this.place();
79
+ }
80
+
81
+ dragTo({ x, y }: Point): void {
82
+ this.dragPt.x = x;
83
+ this.dragPt.y = y;
84
+ this.place();
85
+ }
86
+
87
+ move({ x: dx, y: dy }: Point): void {
88
+ this.movePt.x += dx;
89
+ this.movePt.y += dy;
90
+ this.place();
91
+ }
92
+
93
+ moveTo({ x, y }: Point): void {
94
+ this.movePt.x = x;
95
+ this.movePt.y = y;
96
+ this.place();
97
+ }
98
+
99
+ abstract resize(size: number | Bounds): void;
100
+
101
+ abstract clipi(line: Line): void;
102
+
103
+ abstract makeClone(): Geometry;
104
+
105
+ clone(): Geometry {
106
+ const geom = this.makeClone();
107
+ geom.moveTo(this.movePt);
108
+ geom.dragTo(this.dragPt);
109
+ return geom;
110
+ }
111
+
112
+ clip(line: Readonly<Line>): Point {
113
+ const [{ x: ax, y: ay }, { x: bx, y: by }] = line;
114
+ const b: Point = { x: bx, y: by };
115
+ const newLine: Line = [{ x: ax, y: ay }, b];
116
+ this.clipi(newLine);
117
+ return b;
118
+ }
119
+
120
+ intersects(geometry: Geometry): boolean {
121
+ // Check if the geometries' AABBs overlap on either X or Y axes.
122
+ // clang-format off
123
+ return !(
124
+ geometry.bottom > this.top ||
125
+ geometry.top < this.bottom ||
126
+ geometry.left > this.right ||
127
+ geometry.right < this.left
128
+ );
129
+ // clang-format on
130
+ }
131
+
132
+ lineIntersects(a: Point, b: Point): boolean {
133
+ return lineIntersectsBox(a, b, { x: this.left, y: this.bottom }, this);
134
+ }
135
+
136
+ contains(geometry: Geometry): boolean {
137
+ // treating both geometries as AABBs, is geometry contained inside this?
138
+ // clang-format off
139
+ return (
140
+ geometry.left >= this.left &&
141
+ geometry.right <= this.right &&
142
+ geometry.top <= this.top &&
143
+ geometry.bottom >= this.bottom
144
+ );
145
+ // clang-format on
146
+ }
147
+
148
+ /**
149
+ * For this geometry type, calculate the tightest bounds that would fit these
150
+ * children. Naively, calculates the rectangular bounds. Circle, for instance,
151
+ * could calculate a tighter bounds.
152
+ */
153
+ tightestFit(children: Iterable<Geometry>): Bounds & Point {
154
+ let minX = Number.MAX_SAFE_INTEGER;
155
+ let minY = Number.MAX_SAFE_INTEGER;
156
+ let maxX = Number.MIN_SAFE_INTEGER;
157
+ let maxY = Number.MIN_SAFE_INTEGER;
158
+
159
+ for (const { x, y, width, height } of children) {
160
+ const halfWidth = width / 2;
161
+ const halfHeight = height / 2;
162
+ minX = Math.min(minX, x - halfWidth);
163
+ minY = Math.min(minY, y - halfHeight);
164
+ maxX = Math.max(maxX, x + halfWidth);
165
+ maxY = Math.max(maxY, y + halfHeight);
166
+ }
167
+
168
+ if (minX > maxX) [minX, maxX] = [maxX, minX];
169
+ if (minY > maxY) [minY, maxY] = [maxY, minY];
170
+
171
+ return {
172
+ // Divide and add, for numerical stability.
173
+ x: maxX / 2 + minX / 2,
174
+ y: maxY / 2 + minY / 2,
175
+ width: maxX - minX,
176
+ height: maxY - minY,
177
+ };
178
+ }
179
+
180
+ fitTo(children: Iterable<Geometry>, padding = 0): void {
181
+ const fit = this.tightestFit(children);
182
+ this.resize({
183
+ height: fit.height + padding * 2,
184
+ width: fit.width + padding * 2,
185
+ });
186
+
187
+ // fit is new center point of geometry bounds. Normalize children by moving
188
+ // them by -fit, and adjust this geometry by fit to compensate the center.
189
+ this.move(fit);
190
+ const delta: Point = { x: -fit.x, y: -fit.y };
191
+ for (const child of children) child.move(delta);
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Adjust second point in `source` to lie on `target`. If the lines are
197
+ * nonintersecting, source is unchanged.
198
+ */
199
+ export function clipi(source: Line, target: Line): void {
200
+ const [{ x: ax1, y: ay1 }, { x: ax2, y: ay2 }] = source;
201
+ const [{ x: bx1, y: by1 }, { x: bx2, y: by2 }] = target;
202
+
203
+ const axd = ax2 - ax1;
204
+ const ayd = ay2 - ay1;
205
+
206
+ const bxd = bx2 - bx1;
207
+ const byd = by2 - by1;
208
+
209
+ const sxd = bx1 - ax1;
210
+ const syd = by1 - ay1;
211
+
212
+ // Adapted from en.wikipedia.org/wiki/Intersection_(Euclidean_geometry)#Two_line_segments
213
+
214
+ // In parametric form:
215
+ // ax(s) = ax1 + s * axd
216
+ // ay(s) = ay1 + s * ayd
217
+ // bx(t) = bx1 + t * bxd
218
+ // by(t) = by1 + t * byd
219
+
220
+ // Results in the linear equations
221
+ // s * axd - t * bxd = sxd
222
+ // s * ayd - t * byd = syd
223
+
224
+ // Solving s, t by Cramer's rule
225
+ const s = (sxd * -byd - syd * -bxd) / (axd * -byd - ayd * -bxd);
226
+ const t = (axd * syd - ayd * sxd) / (axd * -byd - ayd * -bxd);
227
+
228
+ if (0 <= s && t <= 1) {
229
+ // There is an intersection! Update source parametrically
230
+ source[1].x = ax1 + axd * s;
231
+ source[1].y = ay1 + ayd * s;
232
+ }
233
+ // no intersection, all that to do nothing
234
+ }
235
+
236
+ /**
237
+ * Given a line and a circle, update the second point on the line to lie on the
238
+ * circle closest to the first point on the line.
239
+ */
240
+ export function clipicircle(line: Line, point: Point, radius: number): void {
241
+ let [{ x: x1, y: y1 }, { x: x2, y: y2 }] = line;
242
+
243
+ // Normalize so that circle is at (0, 0)
244
+ x1 -= point.x;
245
+ x2 -= point.x;
246
+ y1 -= point.y;
247
+ y2 -= point.y;
248
+
249
+ // Line is:
250
+ // x(t) = x1 + t dx
251
+ // y(t) = y1 + t dy - r^
252
+ //
253
+ // Circle is: (a^ shorthand for a squared)
254
+ //
255
+ // 0 = x^ + y^ - r^
256
+ //
257
+ // Substituting gives
258
+ //
259
+ // = (x1 + t dx)^ + (y1 + t dy)^ - r^
260
+ //
261
+ // Simplifying gives
262
+ // = x1^ + 2 dx x1 t + dx^ t^ + y1^ + 2 dy y1 t + dy^ t^ - r^
263
+ // = dx^ dy^ t^ + 2 (dx x1 + dy y1) t + x1^ + y1^ - r^
264
+ //
265
+ // Quadratic equation setup for t is therefore
266
+ //
267
+ // a = dx^ + dy^
268
+ // b = 2 (dx x1 + dy y1)
269
+ // c = x1^ + y1^ - r^2
270
+
271
+ const dx = x2 - x1;
272
+ const dy = y2 - y1;
273
+
274
+ const a = dx * dx + dy * dy;
275
+ const b = 2 * (dx * x1 + dy * y1);
276
+ const c = x1 * x1 + y1 * y1 - radius * radius;
277
+
278
+ const det = b * b - 4 * a * c;
279
+
280
+ let t = 0;
281
+ if (det < 0) {
282
+ return;
283
+ }
284
+ const aa = 2 * a;
285
+ if (det < EPSILON) {
286
+ t = -b / aa;
287
+ } else {
288
+ // Use stable quadratic from
289
+ // http://people.csail.mit.edu/bkph/articles/Quadratics.pdf
290
+ const basis = -b - Math.sqrt(det);
291
+ let t1: number;
292
+ let t2: number;
293
+ if (b >= 0) {
294
+ t1 = basis / (2 * a);
295
+ t2 = (2 * c) / basis;
296
+ } else {
297
+ t1 = (2 * c) / basis;
298
+ t2 = basis / (2 * a);
299
+ }
300
+
301
+ // Use best t (smallest t between 0 and 1). All return cases are for
302
+ // intersections outside the line segment.
303
+ if (t1 < 0) {
304
+ if (t2 < 0) return;
305
+ if (t2 > 1) return;
306
+ t = t2;
307
+ } else if (t1 > 1) {
308
+ if (t2 < 0) return;
309
+ if (t2 > 1) return;
310
+ t = t2;
311
+ } else {
312
+ t = Math.min(t1, t2);
313
+ }
314
+ }
315
+
316
+ line[1].x = line[0].x + t * dx;
317
+ line[1].y = line[0].y + t * dy;
318
+ }
@@ -0,0 +1,315 @@
1
+ // d3 has path libraries, however, they don't work on ReadonlyArrays or
2
+ // Readonly<PointTuple>, and I'm trying to keep the amount of copying from
3
+ // various Point forms down. Additionally, they use arbitrary precision
4
+ // floating point arithmetic, when really most of the time there's no need for
5
+ // more than 1 or 2 decimals of precision. Further significant digits only
6
+ // serve to make the path more difficult for humans to read.
7
+
8
+ import type { Box, Point } from "../types.ts";
9
+
10
+ /** A box that everything fits into. */
11
+ export const FULL_BOX = {
12
+ x: -Number.MAX_VALUE,
13
+ y: -Number.MAX_VALUE,
14
+ width: Infinity,
15
+ height: Infinity,
16
+ };
17
+
18
+ // An enum that indicates whether each side of a pair of points are inside or
19
+ // outside a box. the first half indicates the location of the first point
20
+ // (inside or outside), and the second word indicates the location of the second
21
+ // point. See `testInBox` for usage.
22
+ const IntersectionPoint = {
23
+ OUTSIDE_OUTSIDE: 0,
24
+ OUTSIDE_INSIDE: 1,
25
+ INSIDE_OUTSIDE: 2,
26
+ INSIDE_INSIDE: 3,
27
+ UNSET: Number.MAX_VALUE,
28
+ } as const;
29
+ type IntersectionPoint = (typeof IntersectionPoint)[keyof typeof IntersectionPoint];
30
+
31
+ /**
32
+ * Create an SVG `d` attribute encoding a straight-line path between absolutely
33
+ * positioned points, truncating their decimal values to a certain precision. If
34
+ * points lie outside the given box, use non-drawing `Move` commands.
35
+ */
36
+ export function straightLinePath(
37
+ points: ReadonlyArray<Readonly<Point>>,
38
+ box: Box = FULL_BOX,
39
+ precision = 2,
40
+ ): string {
41
+ // This method uses a combination of the interpreter pattern and a register-
42
+ // based execution machine. The input tape is the list of `Point` values. The
43
+ // output is held internally in the `commands` array, which holds a list of
44
+ // SVG d path commands:
45
+ // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
46
+ //
47
+ // The commands available for the interpreter are `move`, `line`, `inBox`,
48
+ // `testInOutIntersection`, and `testOutInIntersection`. "Registers" are
49
+ // documented inline.
50
+
51
+ // The output tap, a list of `d` drawing commands.
52
+ const commands: string[] = [];
53
+
54
+ // lastX and lastY track where the command being calculated will end. They are
55
+ // updated at various points in the algorithm, before calling `command` to
56
+ // generate a line command using lastX, lastY as the <x, y> values for that
57
+ // command.
58
+ let lastX = points[0].x;
59
+ let lastY = points[0].y;
60
+
61
+ // Start by moving to the first point in the list.
62
+ move();
63
+
64
+ // And end there if we only have a single point.
65
+ if (points.length === 1) return commands[0];
66
+
67
+ // left, right, top, bottom are the box constants, used to test for ray/box
68
+ // intersections.
69
+ const boxLeft = box.x;
70
+ const boxRight = boxLeft + box.width;
71
+ const boxBottom = box.y;
72
+ const boxTop = boxBottom + box.height;
73
+
74
+ if (boxRight <= boxLeft || boxTop <= boxBottom) {
75
+ // If you end up here debugging, then this probably should have been an
76
+ // error case. The following string will help you determine what's going on.
77
+ // `path::straightLinePath: degenerate box: ${boxString()}`
78
+ return "";
79
+ }
80
+
81
+ //
82
+ // Error Reporting Utilities
83
+ //
84
+
85
+ function boxString(): string {
86
+ return `t ${boxTop} r ${boxRight} b ${boxBottom} l ${boxLeft}`;
87
+ }
88
+
89
+ function doThrow(message: string): never {
90
+ const last = `${lastX},${lastY}`;
91
+ const current = `${currentX},${currentY}`;
92
+ const fullMessage = `${message} <from ${last} to ${current} with box ${boxString()}>`;
93
+ throw new Error(`path::straighLinePath: ${fullMessage}`);
94
+ }
95
+
96
+ //
97
+ // Drawing commands
98
+ //
99
+
100
+ // set lastX and lastY to the current position & increment i for the next loop
101
+ function advance() {
102
+ // set last point to current point
103
+ lastX = currentX;
104
+ lastY = currentY;
105
+ // increment i
106
+ i++;
107
+ }
108
+
109
+ // Write an M or L command using lastX lastY at a given precision.
110
+ function command(c: "M" | "L") {
111
+ commands.push(`${c}${lastX.toFixed(precision)},${lastY.toFixed(precision)}`);
112
+ }
113
+
114
+ function move() {
115
+ // If the last command was a (M)ove, replace it with this (M)ove.
116
+ if (commands.length > 0 && commands[commands.length - 1][0] === "M") {
117
+ commands.pop();
118
+ }
119
+ command("M");
120
+ }
121
+
122
+ function line() {
123
+ command("L");
124
+ }
125
+
126
+ //
127
+ // Interesction & Box Tests
128
+ //
129
+
130
+ function inBox(x: number, y: number): boolean {
131
+ return x >= boxLeft && x <= boxRight && y <= boxTop && y >= boxBottom;
132
+ }
133
+
134
+ // Set up the "registers" for running the drawing operations
135
+ // testInteresection uses a register inspired approach. The setup for each
136
+ // test is the same, but to cut down on parameter passing, these local
137
+ // registers keep the test values.
138
+ let directionX = 0;
139
+ let directionY = 0;
140
+ let magnitude = 0;
141
+ let normalX = 0;
142
+ let normalY = 0;
143
+ let invX = 0;
144
+ let invY = 0;
145
+ let tTop = 0;
146
+ let tLeft = 0;
147
+ let tRight = 0;
148
+ let tBottom = 0;
149
+ let tBest = 0;
150
+
151
+ function testInBox() {
152
+ if (isNaN(lastX) || isNaN(lastY) || isNaN(currentX) || isNaN(currentY)) {
153
+ inBoxTest = IntersectionPoint.UNSET;
154
+ return;
155
+ }
156
+ // The IntersectionPoint is a bit enum, with bit 0 determining whether
157
+ // the current point is in the box, and bit 1 controlling whether the
158
+ // last point is in the box. Adding them together gives the four states.
159
+ const lastIn = inBox(lastX, lastY) ? 2 : 0;
160
+ const currentIn = inBox(currentX, currentY) ? 1 : 0;
161
+ inBoxTest = (lastIn + currentIn) as IntersectionPoint;
162
+ }
163
+
164
+ // In normalized coordinates, find the t parameter for intersection of the
165
+ // four edges of the box.
166
+ function setupIntersectionTest() {
167
+ directionX = currentX - lastX;
168
+ directionY = currentY - lastY;
169
+
170
+ magnitude = Math.sqrt(directionX * directionX + directionY * directionY);
171
+ if (magnitude === 0) {
172
+ throw new Error(`path: last <${lastX}, ${lastY}> set to current <${currentX}, ${currentY}>`);
173
+ }
174
+ normalX = directionX / magnitude;
175
+ normalY = directionY / magnitude;
176
+
177
+ invX = 1 / normalX;
178
+ invY = 1 / normalY;
179
+
180
+ tLeft = (boxLeft - lastX) * invX;
181
+ tTop = (boxTop - lastY) * invY;
182
+ tRight = (boxRight - lastX) * invX;
183
+ tBottom = (boxBottom - lastY) * invY;
184
+ }
185
+
186
+ // After calculated a new tBest based on the normal, update lastX lastY to
187
+ // be at the point of intersection.
188
+ function updateToPointOfIntersection() {
189
+ lastX += normalX * tBest;
190
+ lastY += normalY * tBest;
191
+
192
+ return true;
193
+ }
194
+
195
+ // find the point of intersection with the box closest to l0 towards l1. If
196
+ // it exists, update last to that point and return true. Otherwise, return
197
+ // false. Algorithm for testing ray/box intersection is available many
198
+ // places, eg
199
+ // https://www.scratchapixel.com/lessons/3d-basic-rendering/minimal-ray-tracer-rendering-simple-shapes/ray-box-intersection
200
+ // https://medium.com/@bromanz/another-view-on-the-classic-ray-aabb-intersection-algorithm-for-bvh-traversal-41125138b525
201
+ function testOutInIntersection(): boolean {
202
+ setupIntersectionTest();
203
+
204
+ const tsmallx = Math.min(tLeft, tRight);
205
+ const tsmally = Math.min(tTop, tBottom);
206
+ const tbigx = Math.max(tLeft, tRight);
207
+ const tbigy = Math.max(tTop, tBottom);
208
+
209
+ tBest = Math.max(0, Math.max(tsmallx, tsmally));
210
+ const tMax = Math.min(tbigx, tbigy);
211
+
212
+ if (tBest > tMax) return false;
213
+ if (tBest <= 0) {
214
+ if (tMax > 0) {
215
+ tBest = tMax;
216
+ } else {
217
+ return false;
218
+ }
219
+ }
220
+ // If the best point is beyond the current point, this doesn't intersect.
221
+ if (tBest / magnitude > 1) return false;
222
+
223
+ return updateToPointOfIntersection();
224
+ }
225
+
226
+ // Find point of intersection from within the box closest to l0 towards l1.
227
+ // This is a slightly different algorithm than above, because
228
+ // testOutInIntersection is optimized for a ray intersecting the exterior (a
229
+ // convex polygon), while testInOutIntersection must look for the interior (a
230
+ // concave polycon).
231
+ function testInOutIntersection(): boolean {
232
+ setupIntersectionTest();
233
+
234
+ // Find the smallest intersection t that is greater than 0.
235
+ tBest = Number.MAX_VALUE;
236
+ if (tLeft > 0 && tLeft < tBest) tBest = tLeft;
237
+ if (tRight > 0 && tRight < tBest) tBest = tRight;
238
+ if (tTop > 0 && tTop < tBest) tBest = tTop;
239
+ if (tBottom > 0 && tBottom < tBest) tBest = tBottom;
240
+
241
+ if (tBest === Number.MAX_VALUE) return false;
242
+
243
+ return updateToPointOfIntersection();
244
+ }
245
+
246
+ //
247
+ // Main
248
+ //
249
+
250
+ // The index into `points` for the current <x, y>.
251
+ let i = 1;
252
+ // The {x, y} values at points[i].
253
+ let currentX: number, currentY: number;
254
+ let inBoxTest: IntersectionPoint = IntersectionPoint.UNSET;
255
+
256
+ // This loop walks through the list of points. The path cursor has already
257
+ // been (M)oved to the first point in the path. From here, it will attempt to
258
+ // (L)ine from point to point while inside the box, (M)ove from point to point
259
+ // while outside the box, and create new points at the path/bounding box
260
+ // intersection.
261
+ while (i < points.length) {
262
+ currentX = points[i].x;
263
+ currentY = points[i].y;
264
+
265
+ testInBox();
266
+
267
+ switch (inBoxTest as IntersectionPoint) {
268
+ case IntersectionPoint.OUTSIDE_OUTSIDE:
269
+ // line from last to current intersects the box
270
+ if (testOutInIntersection()) {
271
+ // testOutInIntersection set last point to intersection point
272
+ move();
273
+ } else {
274
+ // No intersection, so the line doesn't cross the box. Advance to the
275
+ // current (still outside) box point, basically throwing away this
276
+ // point and continue the next iteration.
277
+ advance();
278
+ }
279
+ break;
280
+ case IntersectionPoint.OUTSIDE_INSIDE:
281
+ if (!testOutInIntersection()) {
282
+ return doThrow("no outside to inside intersection");
283
+ }
284
+ // testOutInIntersection set last point to intersection point. Move the
285
+ // line to start at the edge of the box.
286
+ move();
287
+ break;
288
+ case IntersectionPoint.INSIDE_INSIDE:
289
+ advance();
290
+ // L to current point
291
+ line();
292
+ break;
293
+ case IntersectionPoint.INSIDE_OUTSIDE:
294
+ if (!testInOutIntersection()) {
295
+ return doThrow("no inside to outside intersection");
296
+ }
297
+ // L to point of intersection
298
+ line();
299
+ advance();
300
+ break;
301
+ default:
302
+ return doThrow(
303
+ `invalid box test state ${
304
+ Object.keys(IntersectionPoint).find(
305
+ (k) => IntersectionPoint[k as keyof typeof IntersectionPoint] === inBoxTest,
306
+ ) ?? inBoxTest
307
+ }`,
308
+ );
309
+ }
310
+ }
311
+
312
+ // If we only have one command, that means the line isn't in the box and
313
+ // there is nothing to display.
314
+ return commands.length > 1 ? commands.join(" ") : "";
315
+ }