@altpsyche/maths 0.8.0 → 0.9.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/README.md CHANGED
@@ -163,6 +163,47 @@ matters. The slope in the demo is a value of the walk, so it is worked out by th
163
163
  track and cannot drift from the dot. The rise is counted by the clock, which is honest only because
164
164
  the dot has stopped by the time it counts: two clocks running at once would be free to disagree.
165
165
 
166
+ ## Two shapes combined
167
+
168
+ <img src="docs/boolean.svg" width="720" alt="Two discs drawn three times side by side: everything either one covers, only what both cover, and the first with the second taken out of it.">
169
+
170
+ `unionOf` is everything either path covers, `intersectionOf` is only what both cover, and
171
+ `differenceOf` is the first with the second taken out of it. Each input is closed loops that do not
172
+ cross themselves, and a loop left open is closed by a straight run back to where it started before
173
+ anything else happens.
174
+
175
+ ```ts
176
+ import { circle, differenceOf, intersectionOf, unionOf, vec2 } from '@altpsyche/maths';
177
+
178
+ const first = circle(vec2(0, 0), 0.9);
179
+ const second = circle(vec2(-0.6, 0), 0.36);
180
+
181
+ unionOf(first, second);
182
+ intersectionOf(first, second);
183
+ differenceOf(first, second);
184
+ ```
185
+
186
+ A result may have a hole even though an input may not. A disc with a smaller disc taken out of it is
187
+ a ring, which is an outer loop and an inner loop wound the opposite way, and the nonzero rule the
188
+ mark already carries leaves the middle empty.
189
+
190
+ The work happens in four steps, and each of them is a call of its own. `curveCrossings` says where
191
+ two cubics cross, by halving both curves and following only the halves whose boxes still overlap,
192
+ then sharpening what it finds by Newton's method. `cutPath` puts a cut wherever something crosses,
193
+ so that afterwards every piece is wholly inside the other path or wholly outside it. `containsPoint`
194
+ decides which of those a piece is, by counting how many times the other path winds round its middle.
195
+ `areaOf` says how much a path encloses, in closed form rather than by sampling, which is what every
196
+ claim above is checked against.
197
+
198
+ <img src="docs/boolean-strip.svg" width="960" alt="Four frames side by side, each showing the three panels, as the small disc walks from clear of the large one, through touching it at one point, through overlapping it, to sitting wholly inside it.">
199
+
200
+ Four times of one figure. A small disc walks across a larger one: clear of it, touching it at one
201
+ point, crossing it at two, and wholly inside it. Those are the four cases this kind of code gets
202
+ silently wrong, which is why the demo walks through all of them rather than drawing one.
203
+
204
+ Two edges that lie on top of each other for a stretch have no one answer, and what comes back for
205
+ them is decided by the tolerance rather than by the geometry.
206
+
166
207
  ## The animations
167
208
 
168
209
  `fadeIn`, `fadeOut`, `fadeTo`, `draw`, `morph`, `morphEquation`, `countTo`, `moveBy`, `rotate`,
@@ -0,0 +1,22 @@
1
+ /**
2
+ * How much a path encloses.
3
+ *
4
+ * This is Green's theorem, which turns the area inside a closed loop into an
5
+ * integral round its edge, and for a cubic that integral has a closed form. So
6
+ * the answer is worked out rather than sampled, and it is exact for the shape
7
+ * the cubics actually draw.
8
+ *
9
+ * The sign is the direction the loop is wound in, positive anticlockwise. That
10
+ * is what makes a hole subtract: a loop wound the other way inside another one
11
+ * encloses a negative amount, and the two added together are the ring.
12
+ */
13
+ import type { Path } from './path.js';
14
+ /**
15
+ * How much a path encloses, positive where it is wound anticlockwise.
16
+ *
17
+ * A subpath left open is closed by the straight run back to where it started,
18
+ * since an open loop encloses nothing on its own. Every subpath is added, so a
19
+ * ring written as an outer loop and an inner loop wound the other way comes
20
+ * back as the difference between the two discs.
21
+ */
22
+ export declare function areaOf(path: Path): number;
@@ -0,0 +1,45 @@
1
+ /** Twice the area of the triangle two points make with the origin, positive
2
+ * anticlockwise. */
3
+ function wedge(a, b) {
4
+ return a.x * b.y - b.x * a.y;
5
+ }
6
+ /**
7
+ * What one piece contributes to the integral round the loop.
8
+ *
9
+ * Each pair of the piece's four points contributes the triangle it makes with
10
+ * the origin, and the weights are what integrating a cubic against its own
11
+ * derivative leaves: six, three, one, three, three and six twentieths, in the
12
+ * order the pairs are taken.
13
+ */
14
+ function pieceArea(from, curve) {
15
+ const { control1, control2, to } = curve;
16
+ return ((6 * wedge(from, control1) +
17
+ 3 * wedge(from, control2) +
18
+ wedge(from, to) +
19
+ 3 * wedge(control1, control2) +
20
+ 3 * wedge(control1, to) +
21
+ 6 * wedge(control2, to)) /
22
+ 20);
23
+ }
24
+ /**
25
+ * How much a path encloses, positive where it is wound anticlockwise.
26
+ *
27
+ * A subpath left open is closed by the straight run back to where it started,
28
+ * since an open loop encloses nothing on its own. Every subpath is added, so a
29
+ * ring written as an outer loop and an inner loop wound the other way comes
30
+ * back as the difference between the two discs.
31
+ */
32
+ export function areaOf(path) {
33
+ let total = 0;
34
+ for (const subpath of path) {
35
+ if (subpath.curves.length === 0)
36
+ continue;
37
+ let from = subpath.start;
38
+ for (const curve of subpath.curves) {
39
+ total += pieceArea(from, curve);
40
+ from = curve.to;
41
+ }
42
+ total += wedge(from, subpath.start) / 2;
43
+ }
44
+ return total;
45
+ }
@@ -0,0 +1,13 @@
1
+ import { type Path } from './path.js';
2
+ export interface BooleanOptions {
3
+ /** How close two things come before they count as the same place, in the
4
+ * picture's own units. It decides where two paths are read as crossing and
5
+ * which ends are read as meeting. */
6
+ readonly tolerance?: number;
7
+ }
8
+ /** Everything either path covers. */
9
+ export declare function unionOf(first: Path, second: Path, options?: BooleanOptions): Path;
10
+ /** Only what both paths cover. */
11
+ export declare function intersectionOf(first: Path, second: Path, options?: BooleanOptions): Path;
12
+ /** The first path with the second taken out of it. */
13
+ export declare function differenceOf(first: Path, second: Path, options?: BooleanOptions): Path;
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Union, intersection and difference on paths.
3
+ *
4
+ * Both paths are cut at every place they cross, so that after the cutting every
5
+ * piece is wholly inside the other path or wholly outside it. Each piece is
6
+ * then decided one way or the other, the operation says which pieces it wants,
7
+ * and what is kept is stitched back into loops by joining ends that meet.
8
+ *
9
+ * The difference takes the second path's kept pieces the other way round, which
10
+ * is what turns a disc taken out of the middle of another disc into a ring: the
11
+ * inner loop is wound against the outer one and the nonzero rule the mark
12
+ * carries leaves it empty.
13
+ */
14
+ import { vec2 } from '../values/vec2.js';
15
+ import { pointOn } from './path.js';
16
+ import { cutPath } from './cut.js';
17
+ import { curveCrossings } from './intersect.js';
18
+ import { flattenPath, windingAt } from './inside.js';
19
+ const TOLERANCE = 1e-6;
20
+ /** A subpath left open closed by the straight run back to where it started,
21
+ * since a path with an open loop has no inside for any of this to ask about. */
22
+ function closedLoops(path) {
23
+ return path.map((subpath) => {
24
+ if (subpath.curves.length === 0)
25
+ return subpath;
26
+ const end = subpath.curves[subpath.curves.length - 1].to;
27
+ if (subpath.closed && end.x === subpath.start.x && end.y === subpath.start.y) {
28
+ return subpath;
29
+ }
30
+ const curves = end.x === subpath.start.x && end.y === subpath.start.y
31
+ ? subpath.curves
32
+ : [
33
+ ...subpath.curves,
34
+ {
35
+ control1: vec2.lerp(end, subpath.start, 1 / 3),
36
+ control2: vec2.lerp(end, subpath.start, 2 / 3),
37
+ to: subpath.start,
38
+ },
39
+ ];
40
+ return { start: subpath.start, curves, closed: true };
41
+ });
42
+ }
43
+ function piecesOf(path) {
44
+ const pieces = [];
45
+ for (const subpath of path) {
46
+ let from = subpath.start;
47
+ for (const curve of subpath.curves) {
48
+ pieces.push({ from, curve });
49
+ from = curve.to;
50
+ }
51
+ }
52
+ return pieces;
53
+ }
54
+ /** A piece walked the other way, which is what puts a hole the opposite way
55
+ * round from the loop it sits in. */
56
+ function reversed(piece) {
57
+ return {
58
+ from: piece.curve.to,
59
+ curve: { control1: piece.curve.control2, control2: piece.curve.control1, to: piece.from },
60
+ };
61
+ }
62
+ /** Where the two paths cross, as the cuts each of them needs. */
63
+ function cutsBetween(first, second, tolerance) {
64
+ const forFirst = [];
65
+ const forSecond = [];
66
+ for (let left = 0; left < first.length; left++) {
67
+ let leftFrom = first[left].start;
68
+ for (let leftPiece = 0; leftPiece < first[left].curves.length; leftPiece++) {
69
+ const leftCurve = first[left].curves[leftPiece];
70
+ for (let right = 0; right < second.length; right++) {
71
+ let rightFrom = second[right].start;
72
+ for (let rightPiece = 0; rightPiece < second[right].curves.length; rightPiece++) {
73
+ const rightCurve = second[right].curves[rightPiece];
74
+ for (const crossing of curveCrossings(leftFrom, leftCurve, rightFrom, rightCurve, { tolerance })) {
75
+ forFirst.push({ subpath: left, curve: leftPiece, along: crossing.alongFirst });
76
+ forSecond.push({ subpath: right, curve: rightPiece, along: crossing.alongSecond });
77
+ }
78
+ rightFrom = rightCurve.to;
79
+ }
80
+ }
81
+ leftFrom = leftCurve.to;
82
+ }
83
+ }
84
+ return [forFirst, forSecond];
85
+ }
86
+ /**
87
+ * The kept pieces joined into loops, by taking each end to the piece that
88
+ * starts where it finishes.
89
+ *
90
+ * A piece carries its two controls and where it ends, so the loop takes where
91
+ * each piece begins from where the piece before it ended. The two paths put
92
+ * their cut at a place they each worked out on their own, so the two ends of a
93
+ * join differ by whatever the crossing was out by, and that difference is
94
+ * absorbed here rather than left as a gap.
95
+ */
96
+ function stitch(pieces, tolerance) {
97
+ const used = pieces.map(() => false);
98
+ const loops = [];
99
+ for (let seed = 0; seed < pieces.length; seed++) {
100
+ if (used[seed])
101
+ continue;
102
+ used[seed] = true;
103
+ const start = pieces[seed].from;
104
+ const curves = [pieces[seed].curve];
105
+ let end = pieces[seed].curve.to;
106
+ while (vec2.distance(end, start) > tolerance) {
107
+ let next = -1;
108
+ let nearest = tolerance;
109
+ for (let at = 0; at < pieces.length; at++) {
110
+ if (used[at])
111
+ continue;
112
+ const gap = vec2.distance(pieces[at].from, end);
113
+ if (gap <= nearest) {
114
+ nearest = gap;
115
+ next = at;
116
+ }
117
+ }
118
+ if (next < 0)
119
+ break;
120
+ used[next] = true;
121
+ curves.push(pieces[next].curve);
122
+ end = pieces[next].curve.to;
123
+ }
124
+ loops.push({ start, curves, closed: true });
125
+ }
126
+ return loops;
127
+ }
128
+ /** Which side of the other path each piece falls on, taken at its middle, which
129
+ * after the cutting stands for the whole piece. */
130
+ function insideOther(pieces, other) {
131
+ return pieces.map((piece) => windingAt(other, pointOn(piece.from, piece.curve, 0.5)) !== 0);
132
+ }
133
+ function combine(first, second, keep, options) {
134
+ const tolerance = options.tolerance ?? TOLERANCE;
135
+ const left = closedLoops(first);
136
+ const right = closedLoops(second);
137
+ if (left.length === 0)
138
+ return keep === 'union' ? right : [];
139
+ if (right.length === 0)
140
+ return keep === 'intersection' ? [] : left;
141
+ const [leftCuts, rightCuts] = cutsBetween(left, right, tolerance);
142
+ const leftPieces = piecesOf(cutPath(left, leftCuts, { tolerance }));
143
+ const rightPieces = piecesOf(cutPath(right, rightCuts, { tolerance }));
144
+ const leftInside = insideOther(leftPieces, flattenPath(right, { tolerance }));
145
+ const rightInside = insideOther(rightPieces, flattenPath(left, { tolerance }));
146
+ const kept = [];
147
+ for (let at = 0; at < leftPieces.length; at++) {
148
+ if (leftInside[at] === (keep === 'intersection'))
149
+ kept.push(leftPieces[at]);
150
+ }
151
+ for (let at = 0; at < rightPieces.length; at++) {
152
+ if (keep === 'difference') {
153
+ if (rightInside[at])
154
+ kept.push(reversed(rightPieces[at]));
155
+ continue;
156
+ }
157
+ if (rightInside[at] === (keep === 'intersection'))
158
+ kept.push(rightPieces[at]);
159
+ }
160
+ return stitch(kept, tolerance);
161
+ }
162
+ /** Everything either path covers. */
163
+ export function unionOf(first, second, options = {}) {
164
+ return combine(first, second, 'union', options);
165
+ }
166
+ /** Only what both paths cover. */
167
+ export function intersectionOf(first, second, options = {}) {
168
+ return combine(first, second, 'intersection', options);
169
+ }
170
+ /** The first path with the second taken out of it. */
171
+ export function differenceOf(first, second, options = {}) {
172
+ return combine(first, second, 'difference', options);
173
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * A path cut where something crosses it.
3
+ *
4
+ * Each cut names a piece and how far along it the cut falls, and the pieces
5
+ * that come back draw exactly what the pieces they replace drew, because de
6
+ * Casteljau's construction gives both halves of a cubic as cubics. Several cuts
7
+ * in one piece are taken in order, and each one after the first is measured
8
+ * against what is left rather than against the piece it started as.
9
+ */
10
+ import { type Path } from './path.js';
11
+ /** Where one cut falls: which subpath, which piece of it, and how far along
12
+ * that piece. */
13
+ export interface Cut {
14
+ readonly subpath: number;
15
+ readonly curve: number;
16
+ readonly along: number;
17
+ }
18
+ export interface CutOptions {
19
+ /** How close two cuts, or a cut and the end of a piece, are before they count
20
+ * as the same place, in the picture's own units. */
21
+ readonly tolerance?: number;
22
+ }
23
+ /**
24
+ * A path with every cut put in, drawing what it drew and holding one more piece
25
+ * for each cut.
26
+ *
27
+ * The tolerance is a distance rather than a fraction, so it is read against
28
+ * each piece's own length: a cut is worth making only where the piece it would
29
+ * leave behind is long enough to see, and a piece of nothing is one the stitch
30
+ * that follows would have to know to skip.
31
+ *
32
+ * A cut naming a piece the path does not have is ignored, since a caller that
33
+ * has already thrown one subpath away should not have to renumber the cuts it
34
+ * gathered before it did.
35
+ */
36
+ export declare function cutPath(path: Path, cuts: readonly Cut[], options?: CutOptions): Path;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * A path cut where something crosses it.
3
+ *
4
+ * Each cut names a piece and how far along it the cut falls, and the pieces
5
+ * that come back draw exactly what the pieces they replace drew, because de
6
+ * Casteljau's construction gives both halves of a cubic as cubics. Several cuts
7
+ * in one piece are taken in order, and each one after the first is measured
8
+ * against what is left rather than against the piece it started as.
9
+ */
10
+ import { splitCurve } from './path.js';
11
+ import { measurePath } from './length.js';
12
+ const TOLERANCE = 1e-9;
13
+ /** The fractions for one piece, in order, with the ones that would leave a
14
+ * piece of nothing dropped. */
15
+ function wanted(fractions, tolerance) {
16
+ const kept = [];
17
+ for (const fraction of [...fractions].sort((a, b) => a - b)) {
18
+ if (fraction <= tolerance || fraction >= 1 - tolerance)
19
+ continue;
20
+ if (kept.length > 0 && fraction - kept[kept.length - 1] <= tolerance)
21
+ continue;
22
+ kept.push(fraction);
23
+ }
24
+ return kept;
25
+ }
26
+ /** One piece as the run of pieces the cuts leave, each cut measured against
27
+ * what is left of the piece rather than against the whole of it. */
28
+ function cutCurve(from, curve, fractions) {
29
+ const pieces = [];
30
+ let rest = curve;
31
+ let restFrom = from;
32
+ let taken = 0;
33
+ for (const fraction of fractions) {
34
+ const [head, tail] = splitCurve(restFrom, rest, (fraction - taken) / (1 - taken));
35
+ pieces.push(head);
36
+ restFrom = head.to;
37
+ rest = tail;
38
+ taken = fraction;
39
+ }
40
+ pieces.push(rest);
41
+ return pieces;
42
+ }
43
+ /**
44
+ * A path with every cut put in, drawing what it drew and holding one more piece
45
+ * for each cut.
46
+ *
47
+ * The tolerance is a distance rather than a fraction, so it is read against
48
+ * each piece's own length: a cut is worth making only where the piece it would
49
+ * leave behind is long enough to see, and a piece of nothing is one the stitch
50
+ * that follows would have to know to skip.
51
+ *
52
+ * A cut naming a piece the path does not have is ignored, since a caller that
53
+ * has already thrown one subpath away should not have to renumber the cuts it
54
+ * gathered before it did.
55
+ */
56
+ export function cutPath(path, cuts, options = {}) {
57
+ const tolerance = options.tolerance ?? TOLERANCE;
58
+ if (cuts.length === 0)
59
+ return path;
60
+ const gathered = new Map();
61
+ for (const cut of cuts) {
62
+ const key = `${cut.subpath}:${cut.curve}`;
63
+ const already = gathered.get(key);
64
+ if (already)
65
+ already.push(cut.along);
66
+ else
67
+ gathered.set(key, [cut.along]);
68
+ }
69
+ const measured = measurePath(path);
70
+ return path.map((subpath, at) => {
71
+ let from = subpath.start;
72
+ const curves = [];
73
+ for (let piece = 0; piece < subpath.curves.length; piece++) {
74
+ const curve = subpath.curves[piece];
75
+ const span = measured.per[at][piece].total;
76
+ const asFraction = span > 0 ? Math.min(tolerance / span, 0.5) : 1;
77
+ const fractions = wanted(gathered.get(`${at}:${piece}`) ?? [], asFraction);
78
+ curves.push(...cutCurve(from, curve, fractions));
79
+ from = curve.to;
80
+ }
81
+ return { start: subpath.start, curves, closed: subpath.closed };
82
+ });
83
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Whether a point is inside a path.
3
+ *
4
+ * The count is taken on a flattening rather than on the cubics, because a ray
5
+ * against a cubic is a cubic to solve and what is wanted here is a yes or a no
6
+ * rather than a place. The geometry a boolean operation keeps stays the exact
7
+ * cubics; only this decision is taken on the flattening.
8
+ *
9
+ * The rule is the nonzero winding rule, which is the rule a mark is already
10
+ * drawn under, so a loop wound the other way inside another loop is a hole.
11
+ * Every edge crossing the ray is counted with the sign of the direction it
12
+ * crosses in, and an edge is counted at its lower end and not at its upper one,
13
+ * which is what makes a ray leaving through a corner answer what every other
14
+ * ray answers.
15
+ */
16
+ import { type Vec2 } from '../values/vec2.js';
17
+ import type { Path } from './path.js';
18
+ export interface FlattenOptions {
19
+ /** How far a straight run may sit from the curve it stands for, in the
20
+ * picture's own units. */
21
+ readonly tolerance?: number;
22
+ }
23
+ /**
24
+ * Every subpath as a run of points, each loop closed.
25
+ *
26
+ * A subpath that was left open is closed by the straight run back to where it
27
+ * started, since a path with an open loop has no inside until it has one.
28
+ */
29
+ export declare function flattenPath(path: Path, options?: FlattenOptions): Vec2[][];
30
+ /** How many times the loops wind round a point, counted along the ray heading
31
+ * in the positive x direction. */
32
+ export declare function windingAt(loops: readonly (readonly Vec2[])[], point: Vec2): number;
33
+ /**
34
+ * Whether a path holds a point, under the nonzero rule.
35
+ *
36
+ * A point sitting on the edge itself has no answer this can be right about, and
37
+ * what comes back for one is whichever side the tolerance put it on.
38
+ */
39
+ export declare function containsPoint(path: Path, point: Vec2, options?: FlattenOptions): boolean;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Whether a point is inside a path.
3
+ *
4
+ * The count is taken on a flattening rather than on the cubics, because a ray
5
+ * against a cubic is a cubic to solve and what is wanted here is a yes or a no
6
+ * rather than a place. The geometry a boolean operation keeps stays the exact
7
+ * cubics; only this decision is taken on the flattening.
8
+ *
9
+ * The rule is the nonzero winding rule, which is the rule a mark is already
10
+ * drawn under, so a loop wound the other way inside another loop is a hole.
11
+ * Every edge crossing the ray is counted with the sign of the direction it
12
+ * crosses in, and an edge is counted at its lower end and not at its upper one,
13
+ * which is what makes a ray leaving through a corner answer what every other
14
+ * ray answers.
15
+ */
16
+ import { vec2 } from '../values/vec2.js';
17
+ const TOLERANCE = 1e-6;
18
+ /** How many times a piece may be halved, which a tolerance of zero would
19
+ * otherwise leave unbounded. */
20
+ const DEPTH = 24;
21
+ /** How far the two controls sit from the straight run between the ends.
22
+ *
23
+ * The curve itself stays within three quarters of this, so measuring the
24
+ * controls asks for a little more than the tolerance rather than a little
25
+ * less. */
26
+ function offChord(from, curve) {
27
+ const run = vec2.sub(curve.to, from);
28
+ const span = Math.hypot(run.x, run.y);
29
+ if (span === 0) {
30
+ return Math.max(vec2.distance(curve.control1, from), vec2.distance(curve.control2, from));
31
+ }
32
+ const away = (point) => Math.abs(run.x * (point.y - from.y) - run.y * (point.x - from.x)) / span;
33
+ return Math.max(away(curve.control1), away(curve.control2));
34
+ }
35
+ function walk(from, curve, tolerance, depth, into) {
36
+ if (depth >= DEPTH || offChord(from, curve) <= tolerance) {
37
+ into.push(curve.to);
38
+ return;
39
+ }
40
+ const a = vec2.lerp(from, curve.control1, 0.5);
41
+ const b = vec2.lerp(curve.control1, curve.control2, 0.5);
42
+ const c = vec2.lerp(curve.control2, curve.to, 0.5);
43
+ const d = vec2.lerp(a, b, 0.5);
44
+ const e = vec2.lerp(b, c, 0.5);
45
+ const middle = vec2.lerp(d, e, 0.5);
46
+ walk(from, { control1: a, control2: d, to: middle }, tolerance, depth + 1, into);
47
+ walk(middle, { control1: e, control2: c, to: curve.to }, tolerance, depth + 1, into);
48
+ }
49
+ /**
50
+ * Every subpath as a run of points, each loop closed.
51
+ *
52
+ * A subpath that was left open is closed by the straight run back to where it
53
+ * started, since a path with an open loop has no inside until it has one.
54
+ */
55
+ export function flattenPath(path, options = {}) {
56
+ const tolerance = options.tolerance ?? TOLERANCE;
57
+ const loops = [];
58
+ for (const subpath of path) {
59
+ if (subpath.curves.length === 0)
60
+ continue;
61
+ const points = [subpath.start];
62
+ let from = subpath.start;
63
+ for (const curve of subpath.curves) {
64
+ walk(from, curve, tolerance, 0, points);
65
+ from = curve.to;
66
+ }
67
+ const last = points[points.length - 1];
68
+ if (last.x !== points[0].x || last.y !== points[0].y)
69
+ points.push(points[0]);
70
+ loops.push(points);
71
+ }
72
+ return loops;
73
+ }
74
+ /** Which side of the run from one point to another a third point falls on,
75
+ * positive to the left of it. */
76
+ function leftOf(from, to, point) {
77
+ return (to.x - from.x) * (point.y - from.y) - (point.x - from.x) * (to.y - from.y);
78
+ }
79
+ /** How many times the loops wind round a point, counted along the ray heading
80
+ * in the positive x direction. */
81
+ export function windingAt(loops, point) {
82
+ let winding = 0;
83
+ for (const loop of loops) {
84
+ for (let at = 1; at < loop.length; at++) {
85
+ const from = loop[at - 1];
86
+ const to = loop[at];
87
+ if (from.y <= point.y) {
88
+ if (to.y > point.y && leftOf(from, to, point) > 0)
89
+ winding += 1;
90
+ }
91
+ else if (to.y <= point.y && leftOf(from, to, point) < 0) {
92
+ winding -= 1;
93
+ }
94
+ }
95
+ }
96
+ return winding;
97
+ }
98
+ /**
99
+ * Whether a path holds a point, under the nonzero rule.
100
+ *
101
+ * A point sitting on the edge itself has no answer this can be right about, and
102
+ * what comes back for one is whichever side the tolerance put it on.
103
+ */
104
+ export function containsPoint(path, point, options = {}) {
105
+ return windingAt(flattenPath(path, options), point) !== 0;
106
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Where two cubics cross.
3
+ *
4
+ * Two curves whose boxes miss each other cannot cross, so a pair is halved by
5
+ * de Casteljau's construction and only the halves whose boxes still overlap are
6
+ * followed down, until both are smaller than the tolerance. A box is bigger
7
+ * than the curve inside it, so a pair that small is kept only when the straight
8
+ * runs across the two pieces come within the tolerance of each other. What is
9
+ * left is a run of touching hits per meeting rather than one hit, so the run is
10
+ * joined up and answered once, and then sharpened by Newton's method on the
11
+ * pair, which is what takes a crossing of two straight pieces from the
12
+ * tolerance to the last few bits.
13
+ */
14
+ import { type Vec2 } from '../values/vec2.js';
15
+ import type { Cubic } from './path.js';
16
+ /** One place two curves meet, as a fraction along each of them and the point
17
+ * itself. */
18
+ export interface Crossing {
19
+ readonly alongFirst: number;
20
+ readonly alongSecond: number;
21
+ readonly point: Vec2;
22
+ }
23
+ export interface CrossingOptions {
24
+ /** How close two pieces come before they count as meeting, in the picture's
25
+ * own units. */
26
+ readonly tolerance?: number;
27
+ }
28
+ /**
29
+ * Every place two cubics cross, as a fraction along each and the point.
30
+ *
31
+ * Each segment is given the point it starts from, since a segment carries where
32
+ * it ends and not where it began. Two curves that lie on top of each other for
33
+ * a stretch have no one answer, and what comes back for them is decided by the
34
+ * tolerance rather than by the geometry.
35
+ */
36
+ export declare function curveCrossings(fromFirst: Vec2, first: Cubic, fromSecond: Vec2, second: Cubic, options?: CrossingOptions): Crossing[];
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Where two cubics cross.
3
+ *
4
+ * Two curves whose boxes miss each other cannot cross, so a pair is halved by
5
+ * de Casteljau's construction and only the halves whose boxes still overlap are
6
+ * followed down, until both are smaller than the tolerance. A box is bigger
7
+ * than the curve inside it, so a pair that small is kept only when the straight
8
+ * runs across the two pieces come within the tolerance of each other. What is
9
+ * left is a run of touching hits per meeting rather than one hit, so the run is
10
+ * joined up and answered once, and then sharpened by Newton's method on the
11
+ * pair, which is what takes a crossing of two straight pieces from the
12
+ * tolerance to the last few bits.
13
+ */
14
+ import { vec2 } from '../values/vec2.js';
15
+ /** How close two pieces come before they count as meeting. It decides which
16
+ * meetings are told apart rather than how sharp one is, since Newton's method
17
+ * supplies the sharpness afterwards. */
18
+ const TOLERANCE = 1e-6;
19
+ /** How many pairs the search may open before it answers with where it had got
20
+ * to, which is what stops two curves lying on top of each other for a stretch
21
+ * from halving forever. */
22
+ const BUDGET = 500_000;
23
+ /** How deep the halving goes, which a tolerance of zero would otherwise leave
24
+ * unbounded. */
25
+ const DEPTH = 60;
26
+ /** Below this the two tangents are parallel, Newton's step divides by nothing,
27
+ * and the answer stays the one the halving gave. */
28
+ const PARALLEL = 1e-12;
29
+ /** How far Newton is allowed to move a cluster before its step is read as a
30
+ * jump to a different crossing and thrown away. */
31
+ const REACH = 1e-3;
32
+ /** A segment written as its four points, since a segment carries where it ends
33
+ * and not where it began. */
34
+ function hullOf(from, curve) {
35
+ return [from, curve.control1, curve.control2, curve.to];
36
+ }
37
+ /** The box round the four points, which holds the curve because a cubic never
38
+ * leaves the hull of the points it is written from. */
39
+ function boxOf(hull) {
40
+ let lowX = hull[0].x;
41
+ let highX = hull[0].x;
42
+ let lowY = hull[0].y;
43
+ let highY = hull[0].y;
44
+ for (let at = 1; at < 4; at++) {
45
+ const point = hull[at];
46
+ if (point.x < lowX)
47
+ lowX = point.x;
48
+ if (point.x > highX)
49
+ highX = point.x;
50
+ if (point.y < lowY)
51
+ lowY = point.y;
52
+ if (point.y > highY)
53
+ highY = point.y;
54
+ }
55
+ return { lowX, highX, lowY, highY };
56
+ }
57
+ function spread(box) {
58
+ return Math.max(box.highX - box.lowX, box.highY - box.lowY);
59
+ }
60
+ /** Whether two boxes miss, with the tolerance added on both sides so two curves
61
+ * touching at exactly one point are not lost to rounding. */
62
+ function apart(a, b, slack) {
63
+ return (a.lowX - slack > b.highX ||
64
+ b.lowX - slack > a.highX ||
65
+ a.lowY - slack > b.highY ||
66
+ b.lowY - slack > a.highY);
67
+ }
68
+ /** How far a point sits from a straight run between two points. */
69
+ function offSegment(point, from, to) {
70
+ const run = vec2.sub(to, from);
71
+ const square = vec2.dot(run, run);
72
+ const along = square === 0 ? 0 : held(vec2.dot(vec2.sub(point, from), run) / square);
73
+ return vec2.distance(point, vec2.add(from, vec2.scale(run, along)));
74
+ }
75
+ /**
76
+ * How far apart two pieces are, taken between the straight runs across them.
77
+ *
78
+ * A piece no bigger than the tolerance leaves its own chord by a fraction of
79
+ * that, so the chords answer for the curves here, and this is what tells a
80
+ * meeting from two boxes that merely overlap without their curves coming close.
81
+ */
82
+ function chordGap(first, second) {
83
+ const a = first[0];
84
+ const b = first[3];
85
+ const c = second[0];
86
+ const d = second[3];
87
+ const ab = vec2.sub(b, a);
88
+ const cd = vec2.sub(d, c);
89
+ const under = ab.x * cd.y - ab.y * cd.x;
90
+ if (under !== 0) {
91
+ const ac = vec2.sub(c, a);
92
+ const alongFirst = (ac.x * cd.y - ac.y * cd.x) / under;
93
+ const alongSecond = (ac.x * ab.y - ac.y * ab.x) / under;
94
+ if (alongFirst >= 0 && alongFirst <= 1 && alongSecond >= 0 && alongSecond <= 1)
95
+ return 0;
96
+ }
97
+ return Math.min(offSegment(a, c, d), offSegment(b, c, d), offSegment(c, a, b), offSegment(d, a, b));
98
+ }
99
+ /** A segment cut in half, both pieces drawing what the whole drew. */
100
+ function halves(hull) {
101
+ const a = vec2.lerp(hull[0], hull[1], 0.5);
102
+ const b = vec2.lerp(hull[1], hull[2], 0.5);
103
+ const c = vec2.lerp(hull[2], hull[3], 0.5);
104
+ const d = vec2.lerp(a, b, 0.5);
105
+ const e = vec2.lerp(b, c, 0.5);
106
+ const middle = vec2.lerp(d, e, 0.5);
107
+ return [
108
+ [hull[0], a, d, middle],
109
+ [middle, e, c, hull[3]],
110
+ ];
111
+ }
112
+ function pointAt(hull, along) {
113
+ const u = 1 - along;
114
+ const a = u * u * u;
115
+ const b = 3 * u * u * along;
116
+ const c = 3 * u * along * along;
117
+ const d = along * along * along;
118
+ return vec2(a * hull[0].x + b * hull[1].x + c * hull[2].x + d * hull[3].x, a * hull[0].y + b * hull[1].y + c * hull[2].y + d * hull[3].y);
119
+ }
120
+ /** Which way the curve is heading, which is the derivative of a cubic and so a
121
+ * quadratic over the differences between neighbouring points. */
122
+ function slopeAt(hull, along) {
123
+ const u = 1 - along;
124
+ const a = 3 * u * u;
125
+ const b = 6 * u * along;
126
+ const c = 3 * along * along;
127
+ return vec2(a * (hull[1].x - hull[0].x) + b * (hull[2].x - hull[1].x) + c * (hull[3].x - hull[2].x), a * (hull[1].y - hull[0].y) + b * (hull[2].y - hull[1].y) + c * (hull[3].y - hull[2].y));
128
+ }
129
+ /** A fraction pulled back onto the segment, so a crossing at an end lands on
130
+ * the end exactly rather than a hair outside it. */
131
+ function held(along) {
132
+ return along < 0 ? 0 : along > 1 ? 1 : along;
133
+ }
134
+ function gapBetween(first, second, alongFirst, alongSecond) {
135
+ return vec2.distance(pointAt(first, alongFirst), pointAt(second, alongSecond));
136
+ }
137
+ /**
138
+ * One cluster moved onto the crossing itself, by Newton's method on the pair of
139
+ * curves: the step that takes the difference between the two points to zero is
140
+ * the two tangents solved as a two by two system.
141
+ *
142
+ * A tangency has no such step, since the tangents are parallel there and the
143
+ * system has no answer, so the halving's own reading is what stands.
144
+ */
145
+ function sharpened(first, second, start) {
146
+ let alongFirst = start.alongFirst;
147
+ let alongSecond = start.alongSecond;
148
+ let gap = gapBetween(first, second, alongFirst, alongSecond);
149
+ for (let step = 0; step < 12; step++) {
150
+ const apartBy = vec2.sub(pointAt(first, alongFirst), pointAt(second, alongSecond));
151
+ const heading = slopeAt(first, alongFirst);
152
+ const other = slopeAt(second, alongSecond);
153
+ const under = other.x * heading.y - heading.x * other.y;
154
+ if (Math.abs(under) < PARALLEL)
155
+ break;
156
+ const moveFirst = (apartBy.x * other.y - other.x * apartBy.y) / under;
157
+ const moveSecond = (apartBy.x * heading.y - heading.x * apartBy.y) / under;
158
+ if (Math.abs(moveFirst) > REACH || Math.abs(moveSecond) > REACH)
159
+ break;
160
+ const nextFirst = held(alongFirst + moveFirst);
161
+ const nextSecond = held(alongSecond + moveSecond);
162
+ const nextGap = gapBetween(first, second, nextFirst, nextSecond);
163
+ if (nextGap > gap)
164
+ break;
165
+ alongFirst = nextFirst;
166
+ alongSecond = nextSecond;
167
+ gap = nextGap;
168
+ if (gap === 0)
169
+ break;
170
+ }
171
+ return { alongFirst, alongSecond, point: pointAt(first, alongFirst) };
172
+ }
173
+ /**
174
+ * A run of hits reported as the places they gather at.
175
+ *
176
+ * Every hit carries the stretch of each curve it was found in, and two hits
177
+ * whose stretches touch on both curves are the same meeting: the stretch
178
+ * between them was never thrown away, so the curves stayed within the tolerance
179
+ * across it. A gap in the run is the curves moving apart by more than the
180
+ * tolerance, which is two meetings rather than one.
181
+ */
182
+ function clustered(hits) {
183
+ const inOrder = [...hits].sort((a, b) => a.firstLow - b.firstLow);
184
+ const groups = [];
185
+ for (const hit of inOrder) {
186
+ const joined = groups.find((group) => hit.firstLow <= group.firstHigh &&
187
+ hit.firstHigh >= group.firstLow &&
188
+ hit.secondLow <= group.secondHigh &&
189
+ hit.secondHigh >= group.secondLow);
190
+ if (joined) {
191
+ joined.firstHigh = Math.max(joined.firstHigh, hit.firstHigh);
192
+ joined.firstLow = Math.min(joined.firstLow, hit.firstLow);
193
+ joined.secondHigh = Math.max(joined.secondHigh, hit.secondHigh);
194
+ joined.secondLow = Math.min(joined.secondLow, hit.secondLow);
195
+ joined.sumFirst += (hit.firstLow + hit.firstHigh) / 2;
196
+ joined.sumSecond += (hit.secondLow + hit.secondHigh) / 2;
197
+ joined.sum = vec2.add(joined.sum, hit.point);
198
+ joined.count += 1;
199
+ continue;
200
+ }
201
+ groups.push({
202
+ firstLow: hit.firstLow,
203
+ firstHigh: hit.firstHigh,
204
+ secondLow: hit.secondLow,
205
+ secondHigh: hit.secondHigh,
206
+ sumFirst: (hit.firstLow + hit.firstHigh) / 2,
207
+ sumSecond: (hit.secondLow + hit.secondHigh) / 2,
208
+ sum: hit.point,
209
+ count: 1,
210
+ });
211
+ }
212
+ return groups.map((group) => ({
213
+ alongFirst: group.sumFirst / group.count,
214
+ alongSecond: group.sumSecond / group.count,
215
+ point: vec2.scale(group.sum, 1 / group.count),
216
+ }));
217
+ }
218
+ /**
219
+ * Every place two cubics cross, as a fraction along each and the point.
220
+ *
221
+ * Each segment is given the point it starts from, since a segment carries where
222
+ * it ends and not where it began. Two curves that lie on top of each other for
223
+ * a stretch have no one answer, and what comes back for them is decided by the
224
+ * tolerance rather than by the geometry.
225
+ */
226
+ export function curveCrossings(fromFirst, first, fromSecond, second, options = {}) {
227
+ const tolerance = options.tolerance ?? TOLERANCE;
228
+ const firstHull = hullOf(fromFirst, first);
229
+ const secondHull = hullOf(fromSecond, second);
230
+ const stack = [
231
+ {
232
+ first: firstHull,
233
+ firstLow: 0,
234
+ firstHigh: 1,
235
+ second: secondHull,
236
+ secondLow: 0,
237
+ secondHigh: 1,
238
+ depth: 0,
239
+ },
240
+ ];
241
+ const hits = [];
242
+ let opened = 0;
243
+ const record = (pair) => {
244
+ const alongFirst = (pair.firstLow + pair.firstHigh) / 2;
245
+ const alongSecond = (pair.secondLow + pair.secondHigh) / 2;
246
+ hits.push({
247
+ alongFirst,
248
+ alongSecond,
249
+ point: pointAt(firstHull, alongFirst),
250
+ firstLow: pair.firstLow,
251
+ firstHigh: pair.firstHigh,
252
+ secondLow: pair.secondLow,
253
+ secondHigh: pair.secondHigh,
254
+ });
255
+ };
256
+ while (stack.length > 0) {
257
+ const pair = stack.pop();
258
+ opened += 1;
259
+ const firstBox = boxOf(pair.first);
260
+ const secondBox = boxOf(pair.second);
261
+ if (apart(firstBox, secondBox, tolerance))
262
+ continue;
263
+ if (opened >= BUDGET) {
264
+ record(pair);
265
+ break;
266
+ }
267
+ const cutFirst = spread(firstBox) > tolerance;
268
+ const cutSecond = spread(secondBox) > tolerance;
269
+ if (!cutFirst && !cutSecond) {
270
+ if (chordGap(pair.first, pair.second) <= tolerance)
271
+ record(pair);
272
+ continue;
273
+ }
274
+ if (pair.depth >= DEPTH) {
275
+ record(pair);
276
+ continue;
277
+ }
278
+ const firstMid = (pair.firstLow + pair.firstHigh) / 2;
279
+ const secondMid = (pair.secondLow + pair.secondHigh) / 2;
280
+ const firstParts = cutFirst
281
+ ? halves(pair.first).map((hull, side) => ({
282
+ hull,
283
+ low: side === 0 ? pair.firstLow : firstMid,
284
+ high: side === 0 ? firstMid : pair.firstHigh,
285
+ }))
286
+ : [{ hull: pair.first, low: pair.firstLow, high: pair.firstHigh }];
287
+ const secondParts = cutSecond
288
+ ? halves(pair.second).map((hull, side) => ({
289
+ hull,
290
+ low: side === 0 ? pair.secondLow : secondMid,
291
+ high: side === 0 ? secondMid : pair.secondHigh,
292
+ }))
293
+ : [{ hull: pair.second, low: pair.secondLow, high: pair.secondHigh }];
294
+ for (const left of firstParts) {
295
+ for (const right of secondParts) {
296
+ stack.push({
297
+ first: left.hull,
298
+ firstLow: left.low,
299
+ firstHigh: left.high,
300
+ second: right.hull,
301
+ secondLow: right.low,
302
+ secondHigh: right.high,
303
+ depth: pair.depth + 1,
304
+ });
305
+ }
306
+ }
307
+ }
308
+ return clustered(hits).map((crossing) => sharpened(firstHull, secondHull, crossing));
309
+ }
@@ -7,21 +7,7 @@
7
7
  * line has to become an arc, because a line is an arc whose controls sit on it.
8
8
  */
9
9
  import { vec2 } from '../values/vec2.js';
10
- import { pointOn } from './path.js';
11
- /** One segment cut into two at a fraction, both pieces kept, which is how a
12
- * subpath gains a point without changing shape. */
13
- function halves(from, curve, along) {
14
- const a = vec2.lerp(from, curve.control1, along);
15
- const b = vec2.lerp(curve.control1, curve.control2, along);
16
- const c = vec2.lerp(curve.control2, curve.to, along);
17
- const d = vec2.lerp(a, b, along);
18
- const e = vec2.lerp(b, c, along);
19
- const middle = vec2.lerp(d, e, along);
20
- return [
21
- { control1: a, control2: d, to: middle },
22
- { control1: e, control2: c, to: curve.to },
23
- ];
24
- }
10
+ import { pointOn, splitCurve } from './path.js';
25
11
  /**
26
12
  * A subpath rewritten to hold exactly this many segments, drawing the same shape.
27
13
  *
@@ -49,7 +35,7 @@ function withCurves(subpath, wanted) {
49
35
  let start = subpath.start;
50
36
  for (let at = 0; at < cutAt; at++)
51
37
  start = curves[at].to;
52
- const [first, second] = halves(start, curves[cutAt], 0.5);
38
+ const [first, second] = splitCurve(start, curves[cutAt], 0.5);
53
39
  curves = [...curves.slice(0, cutAt), first, second, ...curves.slice(cutAt + 1)];
54
40
  }
55
41
  return { start: subpath.start, curves, closed: subpath.closed };
@@ -48,6 +48,14 @@ export declare function arc(centre: Vec2, radius: number, fromAngle: number, toA
48
48
  /** A point on a cubic, with the segment's own start passed in because a segment
49
49
  * carries where it ends and not where it began. */
50
50
  export declare function pointOn(from: Vec2, curve: Cubic, along: number): Vec2;
51
+ /**
52
+ * One piece cut into two at a fraction, both pieces drawing what the whole
53
+ * drew, by de Casteljau's construction.
54
+ *
55
+ * The piece's own start is passed in because a piece carries where it ends and
56
+ * not where it began.
57
+ */
58
+ export declare function splitCurve(from: Vec2, curve: Cubic, along: number): [Cubic, Cubic];
51
59
  /** Every point of a path moved by a transform, which is how a group's transform
52
60
  * reaches the geometry rather than being carried alongside it. */
53
61
  export declare function transformPath(path: Path, m: Mat3): Path;
@@ -121,6 +121,25 @@ export function pointOn(from, curve, along) {
121
121
  const d = along * along * along;
122
122
  return vec2(a * from.x + b * curve.control1.x + c * curve.control2.x + d * curve.to.x, a * from.y + b * curve.control1.y + c * curve.control2.y + d * curve.to.y);
123
123
  }
124
+ /**
125
+ * One piece cut into two at a fraction, both pieces drawing what the whole
126
+ * drew, by de Casteljau's construction.
127
+ *
128
+ * The piece's own start is passed in because a piece carries where it ends and
129
+ * not where it began.
130
+ */
131
+ export function splitCurve(from, curve, along) {
132
+ const a = vec2.lerp(from, curve.control1, along);
133
+ const b = vec2.lerp(curve.control1, curve.control2, along);
134
+ const c = vec2.lerp(curve.control2, curve.to, along);
135
+ const d = vec2.lerp(a, b, along);
136
+ const e = vec2.lerp(b, c, along);
137
+ const middle = vec2.lerp(d, e, along);
138
+ return [
139
+ { control1: a, control2: d, to: middle },
140
+ { control1: e, control2: c, to: curve.to },
141
+ ];
142
+ }
124
143
  /** Every point of a path moved by a transform, which is how a group's transform
125
144
  * reaches the geometry rather than being carried alongside it. */
126
145
  export function transformPath(path, m) {
package/dist/index.d.ts CHANGED
@@ -20,9 +20,18 @@ export { mat3 } from './values/mat3.js';
20
20
  export type { Mat3 } from './values/mat3.js';
21
21
  export { SAME_TIME, keyAt, sampleTrack, sampleTracks, withKey, withoutKey } from './timing/track.js';
22
22
  export type { Key, Track, TrackValue, Tracks } from './timing/track.js';
23
- export { arc, circle, line, polygon, polyline, pointCount, pointOn, rect, straight, transformPath } from './figure/path.js';
23
+ export { arc, circle, line, polygon, polyline, pointCount, pointOn, rect, splitCurve, straight, transformPath } from './figure/path.js';
24
24
  export type { Cubic, Path, Subpath } from './figure/path.js';
25
25
  export { pathFromData } from './figure/path-data.js';
26
+ export { areaOf } from './figure/area.js';
27
+ export { differenceOf, intersectionOf, unionOf } from './figure/boolean.js';
28
+ export type { BooleanOptions } from './figure/boolean.js';
29
+ export { containsPoint, flattenPath, windingAt } from './figure/inside.js';
30
+ export type { FlattenOptions } from './figure/inside.js';
31
+ export { cutPath } from './figure/cut.js';
32
+ export type { Cut, CutOptions } from './figure/cut.js';
33
+ export { curveCrossings } from './figure/intersect.js';
34
+ export type { Crossing, CrossingOptions } from './figure/intersect.js';
26
35
  export type { Colour, Fill, Mark, PathMark, Stroke, TextMark } from './figure/mark.js';
27
36
  export { byAspect, fractionOf, matchingAspect, resolveExtent, viewMatrix } from './figure/extent.js';
28
37
  export type { Extent, ExtentChoice, Fit } from './figure/extent.js';
package/dist/index.js CHANGED
@@ -14,8 +14,13 @@ export { vec3 } from './values/vec3.js';
14
14
  export { interval } from './values/interval.js';
15
15
  export { mat3 } from './values/mat3.js';
16
16
  export { SAME_TIME, keyAt, sampleTrack, sampleTracks, withKey, withoutKey } from './timing/track.js';
17
- export { arc, circle, line, polygon, polyline, pointCount, pointOn, rect, straight, transformPath } from './figure/path.js';
17
+ export { arc, circle, line, polygon, polyline, pointCount, pointOn, rect, splitCurve, straight, transformPath } from './figure/path.js';
18
18
  export { pathFromData } from './figure/path-data.js';
19
+ export { areaOf } from './figure/area.js';
20
+ export { differenceOf, intersectionOf, unionOf } from './figure/boolean.js';
21
+ export { containsPoint, flattenPath, windingAt } from './figure/inside.js';
22
+ export { cutPath } from './figure/cut.js';
23
+ export { curveCrossings } from './figure/intersect.js';
19
24
  export { byAspect, fractionOf, matchingAspect, resolveExtent, viewMatrix } from './figure/extent.js';
20
25
  export { boundsOf, boundsOfMarks, centreOf } from './figure/bounds.js';
21
26
  export { areaUnder, plot, riemannBars, slopeOf, tangentAt } from './figure/plot.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@altpsyche/maths",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "The mathematics AltPsyche's figures are drawn from: vectors, matrices, curves, and a value walked over time.",
5
5
  "license": "MIT",
6
6
  "author": "Siva",