@altpsyche/maths 0.9.0 → 0.9.1

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.
@@ -10,12 +10,20 @@
10
10
  * is what turns a disc taken out of the middle of another disc into a ring: the
11
11
  * inner loop is wound against the outer one and the nonzero rule the mark
12
12
  * carries leaves it empty.
13
+ *
14
+ * A piece lying along the other path's own edge is decided by which way the two
15
+ * run rather than by which side it is on, because a point on an edge is the one
16
+ * place the winding count has no answer for. Two paths walking a shared stretch
17
+ * the same way have their solid on the same side of it, so the stretch is on the
18
+ * edge of a union and of an overlap and is kept once. Walking it opposite ways
19
+ * puts their solids on opposite sides, so the stretch is inside a union and
20
+ * outside an overlap, and a difference keeps the first path's copy of it.
13
21
  */
14
22
  import { vec2 } from '../values/vec2.js';
15
- import { pointOn } from './path.js';
23
+ import { pointOn, slopeOn } from './path.js';
16
24
  import { cutPath } from './cut.js';
17
25
  import { curveCrossings } from './intersect.js';
18
- import { flattenPath, windingAt } from './inside.js';
26
+ import { flattenPath, nearestEdge, windingAt } from './inside.js';
19
27
  const TOLERANCE = 1e-6;
20
28
  /** A subpath left open closed by the straight run back to where it started,
21
29
  * since a path with an open loop has no inside for any of this to ask about. */
@@ -83,6 +91,22 @@ function cutsBetween(first, second, tolerance) {
83
91
  }
84
92
  return [forFirst, forSecond];
85
93
  }
94
+ function place(point) {
95
+ return `(${point.x.toFixed(6)}, ${point.y.toFixed(6)})`;
96
+ }
97
+ /**
98
+ * What a run of pieces that will not close stops with.
99
+ *
100
+ * Handing it back as a loop anyway is the one failure a caller cannot see: the
101
+ * shape drawn is wrong and nothing about it says so. Every input is closed
102
+ * loops, so a run that will not close is this code being wrong rather than the
103
+ * caller, and stopping is what makes that visible on the frame it happens.
104
+ */
105
+ function refuse(pieces, start, end, tolerance) {
106
+ throw new Error(`the pieces kept do not close into a loop: ${pieces} of them run from ${place(start)} ` +
107
+ `to ${place(end)}, which is ${vec2.distance(end, start).toFixed(6)} apart against a ` +
108
+ `tolerance of ${tolerance}`);
109
+ }
86
110
  /**
87
111
  * The kept pieces joined into loops, by taking each end to the piece that
88
112
  * starts where it finishes.
@@ -116,7 +140,7 @@ function stitch(pieces, tolerance) {
116
140
  }
117
141
  }
118
142
  if (next < 0)
119
- break;
143
+ refuse(curves.length, start, end, tolerance);
120
144
  used[next] = true;
121
145
  curves.push(pieces[next].curve);
122
146
  end = pieces[next].curve.to;
@@ -125,10 +149,40 @@ function stitch(pieces, tolerance) {
125
149
  }
126
150
  return loops;
127
151
  }
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);
152
+ /**
153
+ * Where each piece stands against the other path, read at its middle.
154
+ *
155
+ * The middle stands for the whole piece because the cutting has already put a
156
+ * break wherever the two paths meet, so a piece after it is wholly one thing.
157
+ */
158
+ function sidesAgainst(pieces, other, tolerance) {
159
+ return pieces.map((piece) => {
160
+ const middle = pointOn(piece.from, piece.curve, 0.5);
161
+ const edge = nearestEdge(other, middle);
162
+ if (edge && edge.gap <= tolerance) {
163
+ return vec2.dot(slopeOn(piece.from, piece.curve, 0.5), edge.heading) >= 0 ? 'along' : 'against';
164
+ }
165
+ return windingAt(other, middle) !== 0 ? 'inside' : 'outside';
166
+ });
167
+ }
168
+ /** Whether the first path's piece belongs to the answer. */
169
+ function keepsFirst(keep, side) {
170
+ if (side === 'along')
171
+ return keep !== 'difference';
172
+ if (side === 'against')
173
+ return keep === 'difference';
174
+ if (keep === 'intersection')
175
+ return side === 'inside';
176
+ return side === 'outside';
177
+ }
178
+ /** Whether the second path's piece belongs to the answer. A shared stretch is
179
+ * never taken from here, since the first path's copy of it is already in. */
180
+ function keepsSecond(keep, side) {
181
+ if (side === 'along' || side === 'against')
182
+ return false;
183
+ if (keep === 'union')
184
+ return side === 'outside';
185
+ return side === 'inside';
132
186
  }
133
187
  function combine(first, second, keep, options) {
134
188
  const tolerance = options.tolerance ?? TOLERANCE;
@@ -141,21 +195,17 @@ function combine(first, second, keep, options) {
141
195
  const [leftCuts, rightCuts] = cutsBetween(left, right, tolerance);
142
196
  const leftPieces = piecesOf(cutPath(left, leftCuts, { tolerance }));
143
197
  const rightPieces = piecesOf(cutPath(right, rightCuts, { tolerance }));
144
- const leftInside = insideOther(leftPieces, flattenPath(right, { tolerance }));
145
- const rightInside = insideOther(rightPieces, flattenPath(left, { tolerance }));
198
+ const leftSide = sidesAgainst(leftPieces, flattenPath(right, { tolerance }), tolerance);
199
+ const rightSide = sidesAgainst(rightPieces, flattenPath(left, { tolerance }), tolerance);
146
200
  const kept = [];
147
201
  for (let at = 0; at < leftPieces.length; at++) {
148
- if (leftInside[at] === (keep === 'intersection'))
202
+ if (keepsFirst(keep, leftSide[at]))
149
203
  kept.push(leftPieces[at]);
150
204
  }
151
205
  for (let at = 0; at < rightPieces.length; at++) {
152
- if (keep === 'difference') {
153
- if (rightInside[at])
154
- kept.push(reversed(rightPieces[at]));
206
+ if (!keepsSecond(keep, rightSide[at]))
155
207
  continue;
156
- }
157
- if (rightInside[at] === (keep === 'intersection'))
158
- kept.push(rightPieces[at]);
208
+ kept.push(keep === 'difference' ? reversed(rightPieces[at]) : rightPieces[at]);
159
209
  }
160
210
  return stitch(kept, tolerance);
161
211
  }
@@ -30,6 +30,20 @@ export declare function flattenPath(path: Path, options?: FlattenOptions): Vec2[
30
30
  /** How many times the loops wind round a point, counted along the ray heading
31
31
  * in the positive x direction. */
32
32
  export declare function windingAt(loops: readonly (readonly Vec2[])[], point: Vec2): number;
33
+ /** The nearest straight run of a flattening to a point: how far off it is, and
34
+ * which way that run goes. */
35
+ export interface Edge {
36
+ readonly gap: number;
37
+ readonly heading: Vec2;
38
+ }
39
+ /**
40
+ * Which edge of a flattening a point sits nearest, and which way it runs.
41
+ *
42
+ * This is what tells a piece lying along another path's edge from one merely
43
+ * near it, which the winding count cannot answer because a point on the edge
44
+ * itself is the one place the count has no answer for.
45
+ */
46
+ export declare function nearestEdge(loops: readonly (readonly Vec2[])[], point: Vec2): Edge | null;
33
47
  /**
34
48
  * Whether a path holds a point, under the nonzero rule.
35
49
  *
@@ -18,6 +18,15 @@ const TOLERANCE = 1e-6;
18
18
  /** How many times a piece may be halved, which a tolerance of zero would
19
19
  * otherwise leave unbounded. */
20
20
  const DEPTH = 24;
21
+ /**
22
+ * How many points a whole flattening may hold before it is refused.
23
+ *
24
+ * Halving is the only bound the depth gives, and sixteen million points for one
25
+ * piece is a machine out of memory rather than a fine flattening. A circle of
26
+ * radius 1 wants 4096 of these at a tolerance of a millionth, so the room here
27
+ * is a thousandfold.
28
+ */
29
+ const POINTS = 1_000_000;
21
30
  /** How far the two controls sit from the straight run between the ends.
22
31
  *
23
32
  * The curve itself stays within three quarters of this, so measuring the
@@ -37,6 +46,10 @@ function walk(from, curve, tolerance, depth, into) {
37
46
  into.push(curve.to);
38
47
  return;
39
48
  }
49
+ if (into.length >= POINTS) {
50
+ throw new Error(`a tolerance of ${tolerance} asks for more than ${POINTS} straight runs to stand for one ` +
51
+ `curve, which is finer than this flattens`);
52
+ }
40
53
  const a = vec2.lerp(from, curve.control1, 0.5);
41
54
  const b = vec2.lerp(curve.control1, curve.control2, 0.5);
42
55
  const c = vec2.lerp(curve.control2, curve.to, 0.5);
@@ -95,6 +108,29 @@ export function windingAt(loops, point) {
95
108
  }
96
109
  return winding;
97
110
  }
111
+ /**
112
+ * Which edge of a flattening a point sits nearest, and which way it runs.
113
+ *
114
+ * This is what tells a piece lying along another path's edge from one merely
115
+ * near it, which the winding count cannot answer because a point on the edge
116
+ * itself is the one place the count has no answer for.
117
+ */
118
+ export function nearestEdge(loops, point) {
119
+ let nearest = null;
120
+ for (const loop of loops) {
121
+ for (let at = 1; at < loop.length; at++) {
122
+ const from = loop[at - 1];
123
+ const to = loop[at];
124
+ const run = vec2.sub(to, from);
125
+ const square = vec2.dot(run, run);
126
+ const along = square === 0 ? 0 : Math.min(1, Math.max(0, vec2.dot(vec2.sub(point, from), run) / square));
127
+ const gap = vec2.distance(point, vec2.add(from, vec2.scale(run, along)));
128
+ if (!nearest || gap < nearest.gap)
129
+ nearest = { gap, heading: run };
130
+ }
131
+ }
132
+ return nearest;
133
+ }
98
134
  /**
99
135
  * Whether a path holds a point, under the nonzero rule.
100
136
  *
@@ -29,8 +29,8 @@ export interface CrossingOptions {
29
29
  * Every place two cubics cross, as a fraction along each and the point.
30
30
  *
31
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.
32
+ * it ends and not where it began. Two curves covering the same stretch answer
33
+ * with the two ends of that stretch, so a caller reading the answer as places to
34
+ * cut at gets the stretch marked off rather than chopped into slivers.
35
35
  */
36
36
  export declare function curveCrossings(fromFirst: Vec2, first: Cubic, fromSecond: Vec2, second: Cubic, options?: CrossingOptions): Crossing[];
@@ -16,10 +16,15 @@ import { vec2 } from '../values/vec2.js';
16
16
  * meetings are told apart rather than how sharp one is, since Newton's method
17
17
  * supplies the sharpness afterwards. */
18
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;
19
+ /**
20
+ * How many pairs the search may make before it answers with where it had got to.
21
+ *
22
+ * It counts pairs made rather than pairs looked at, because each one looked at
23
+ * makes up to four more: counting the ones looked at leaves the pile of pairs
24
+ * waiting growing four times faster than it drains, and a tolerance small enough
25
+ * ran the machine out of memory before the count was ever reached.
26
+ */
27
+ const BUDGET = 200_000;
23
28
  /** How deep the halving goes, which a tolerance of zero would otherwise leave
24
29
  * unbounded. */
25
30
  const DEPTH = 60;
@@ -117,8 +122,8 @@ function pointAt(hull, along) {
117
122
  const d = along * along * along;
118
123
  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
124
  }
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. */
125
+ /** Which way the curve is heading, read off the four points the search already
126
+ * holds rather than off a piece, so following a pair down allocates nothing. */
122
127
  function slopeAt(hull, along) {
123
128
  const u = 1 - along;
124
129
  const a = 3 * u * u;
@@ -215,18 +220,121 @@ function clustered(hits) {
215
220
  point: vec2.scale(group.sum, 1 / group.count),
216
221
  }));
217
222
  }
223
+ /** How many places the coarse sweep looks at before it decides which part of a
224
+ * curve a point is nearest. */
225
+ const SWEEP = 32;
226
+ /** How hard the curve is turning at a place, which Newton needs because the
227
+ * nearest point moves as the curve bends away from it. */
228
+ function bendAt(hull, along) {
229
+ const u = 1 - along;
230
+ return vec2(6 * (u * (hull[2].x - 2 * hull[1].x + hull[0].x) + along * (hull[3].x - 2 * hull[2].x + hull[1].x)), 6 * (u * (hull[2].y - 2 * hull[1].y + hull[0].y) + along * (hull[3].y - 2 * hull[2].y + hull[1].y)));
231
+ }
232
+ /**
233
+ * Where on a curve a point sits nearest, as a fraction along it, and how far
234
+ * away it is there.
235
+ *
236
+ * A coarse sweep picks which part of the curve to believe, and Newton's method
237
+ * on the distance finishes it, since the nearest point on a cubic is a fifth
238
+ * degree root and solving one is more than this needs. The sweep gives up before
239
+ * Newton when its best is further off than the tolerance plus the most one step
240
+ * of the sweep can be hiding, since no refining brings it under from there.
241
+ */
242
+ function nearestPlace(hull, point, tolerance) {
243
+ let along = 0;
244
+ let gap = Infinity;
245
+ for (let step = 0; step <= SWEEP; step++) {
246
+ const at = step / SWEEP;
247
+ const away = vec2.distance(pointAt(hull, at), point);
248
+ if (away < gap) {
249
+ gap = away;
250
+ along = at;
251
+ }
252
+ }
253
+ const reach = spread(boxOf(hull)) * 2;
254
+ if (gap > tolerance + reach / SWEEP)
255
+ return { along, gap };
256
+ for (let step = 0; step < 12; step++) {
257
+ const away = vec2.sub(pointAt(hull, along), point);
258
+ const heading = slopeAt(hull, along);
259
+ const turning = vec2.dot(heading, heading) + vec2.dot(away, bendAt(hull, along));
260
+ if (Math.abs(turning) < PARALLEL)
261
+ break;
262
+ const next = held(along - vec2.dot(away, heading) / turning);
263
+ const closer = vec2.distance(pointAt(hull, next), point);
264
+ if (!(closer < gap))
265
+ break;
266
+ along = next;
267
+ gap = closer;
268
+ }
269
+ return { along, gap };
270
+ }
271
+ /** How many places along a shared stretch are checked to still be on the other
272
+ * curve before the stretch is believed. */
273
+ const ALONG_SHARED = 12;
274
+ /**
275
+ * The two ends of the stretch two curves cover together, when they cover one.
276
+ *
277
+ * Halving into a stretch like that answers it as a spray of meetings, because
278
+ * every pair of small pieces along it overlaps and the budget runs out before
279
+ * the run is walked. So the stretch is found first, from the ends of each curve
280
+ * that lie on the other, and answered by where it starts and where it ends.
281
+ *
282
+ * Two curves meeting at a single point are not a stretch and are left to the
283
+ * halving, which places one meeting more sharply than a sweep can.
284
+ */
285
+ function sharedStretch(first, second, tolerance) {
286
+ const ends = [];
287
+ for (const along of [0, 1]) {
288
+ const place = nearestPlace(second, pointAt(first, along), tolerance);
289
+ if (place.gap <= tolerance)
290
+ ends.push({ first: along, second: place.along });
291
+ }
292
+ for (const along of [0, 1]) {
293
+ const place = nearestPlace(first, pointAt(second, along), tolerance);
294
+ if (place.gap <= tolerance)
295
+ ends.push({ first: place.along, second: along });
296
+ }
297
+ if (ends.length < 2)
298
+ return null;
299
+ let low = ends[0];
300
+ let high = ends[0];
301
+ for (const end of ends) {
302
+ if (end.first < low.first)
303
+ low = end;
304
+ if (end.first > high.first)
305
+ high = end;
306
+ }
307
+ const from = pointAt(first, low.first);
308
+ const to = pointAt(first, high.first);
309
+ if (vec2.distance(from, to) <= tolerance)
310
+ return null;
311
+ for (let step = 1; step < ALONG_SHARED; step++) {
312
+ const along = low.first + ((high.first - low.first) * step) / ALONG_SHARED;
313
+ if (nearestPlace(second, pointAt(first, along), tolerance).gap > tolerance)
314
+ return null;
315
+ }
316
+ return [
317
+ { alongFirst: low.first, alongSecond: low.second, point: from },
318
+ { alongFirst: high.first, alongSecond: high.second, point: to },
319
+ ];
320
+ }
218
321
  /**
219
322
  * Every place two cubics cross, as a fraction along each and the point.
220
323
  *
221
324
  * 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.
325
+ * it ends and not where it began. Two curves covering the same stretch answer
326
+ * with the two ends of that stretch, so a caller reading the answer as places to
327
+ * cut at gets the stretch marked off rather than chopped into slivers.
225
328
  */
226
329
  export function curveCrossings(fromFirst, first, fromSecond, second, options = {}) {
227
330
  const tolerance = options.tolerance ?? TOLERANCE;
228
331
  const firstHull = hullOf(fromFirst, first);
229
332
  const secondHull = hullOf(fromSecond, second);
333
+ if (apart(boxOf(firstHull), boxOf(secondHull), tolerance))
334
+ return [];
335
+ const shared = sharedStretch(firstHull, secondHull, tolerance);
336
+ if (shared)
337
+ return shared;
230
338
  const stack = [
231
339
  {
232
340
  first: firstHull,
@@ -239,7 +347,7 @@ export function curveCrossings(fromFirst, first, fromSecond, second, options = {
239
347
  },
240
348
  ];
241
349
  const hits = [];
242
- let opened = 0;
350
+ let made = 1;
243
351
  const record = (pair) => {
244
352
  const alongFirst = (pair.firstLow + pair.firstHigh) / 2;
245
353
  const alongSecond = (pair.secondLow + pair.secondHigh) / 2;
@@ -255,12 +363,11 @@ export function curveCrossings(fromFirst, first, fromSecond, second, options = {
255
363
  };
256
364
  while (stack.length > 0) {
257
365
  const pair = stack.pop();
258
- opened += 1;
259
366
  const firstBox = boxOf(pair.first);
260
367
  const secondBox = boxOf(pair.second);
261
368
  if (apart(firstBox, secondBox, tolerance))
262
369
  continue;
263
- if (opened >= BUDGET) {
370
+ if (made >= BUDGET) {
264
371
  record(pair);
265
372
  break;
266
373
  }
@@ -291,6 +398,7 @@ export function curveCrossings(fromFirst, first, fromSecond, second, options = {
291
398
  high: side === 0 ? secondMid : pair.secondHigh,
292
399
  }))
293
400
  : [{ hull: pair.second, low: pair.secondLow, high: pair.secondHigh }];
401
+ made += firstParts.length * secondParts.length;
294
402
  for (const left of firstParts) {
295
403
  for (const right of secondParts) {
296
404
  stack.push({
@@ -48,6 +48,9 @@ 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
+ /** Which way a piece is heading at a fraction along it, which is the derivative
52
+ * of a cubic and so a quadratic over the gaps between neighbouring points. */
53
+ export declare function slopeOn(from: Vec2, curve: Cubic, along: number): Vec2;
51
54
  /**
52
55
  * One piece cut into two at a fraction, both pieces drawing what the whole
53
56
  * drew, by de Casteljau's construction.
@@ -121,6 +121,15 @@ 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
+ /** Which way a piece is heading at a fraction along it, which is the derivative
125
+ * of a cubic and so a quadratic over the gaps between neighbouring points. */
126
+ export function slopeOn(from, curve, along) {
127
+ const u = 1 - along;
128
+ const a = 3 * u * u;
129
+ const b = 6 * u * along;
130
+ const c = 3 * along * along;
131
+ return vec2(a * (curve.control1.x - from.x) + b * (curve.control2.x - curve.control1.x) + c * (curve.to.x - curve.control2.x), a * (curve.control1.y - from.y) + b * (curve.control2.y - curve.control1.y) + c * (curve.to.y - curve.control2.y));
132
+ }
124
133
  /**
125
134
  * One piece cut into two at a fraction, both pieces drawing what the whole
126
135
  * drew, by de Casteljau's construction.
package/dist/index.d.ts CHANGED
@@ -20,14 +20,14 @@ 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, splitCurve, straight, transformPath } from './figure/path.js';
23
+ export { arc, circle, line, polygon, polyline, pointCount, pointOn, rect, slopeOn, 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
26
  export { areaOf } from './figure/area.js';
27
27
  export { differenceOf, intersectionOf, unionOf } from './figure/boolean.js';
28
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';
29
+ export { containsPoint, flattenPath, nearestEdge, windingAt } from './figure/inside.js';
30
+ export type { Edge, FlattenOptions } from './figure/inside.js';
31
31
  export { cutPath } from './figure/cut.js';
32
32
  export type { Cut, CutOptions } from './figure/cut.js';
33
33
  export { curveCrossings } from './figure/intersect.js';
package/dist/index.js CHANGED
@@ -14,11 +14,11 @@ 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, splitCurve, straight, transformPath } from './figure/path.js';
17
+ export { arc, circle, line, polygon, polyline, pointCount, pointOn, rect, slopeOn, splitCurve, straight, transformPath } from './figure/path.js';
18
18
  export { pathFromData } from './figure/path-data.js';
19
19
  export { areaOf } from './figure/area.js';
20
20
  export { differenceOf, intersectionOf, unionOf } from './figure/boolean.js';
21
- export { containsPoint, flattenPath, windingAt } from './figure/inside.js';
21
+ export { containsPoint, flattenPath, nearestEdge, windingAt } from './figure/inside.js';
22
22
  export { cutPath } from './figure/cut.js';
23
23
  export { curveCrossings } from './figure/intersect.js';
24
24
  export { byAspect, fractionOf, matchingAspect, resolveExtent, viewMatrix } from './figure/extent.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@altpsyche/maths",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
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",