@js-draw/math 1.16.0 → 1.17.0
Sign up to get free protection for your applications and to get access to all the features.
- package/dist/cjs/Vec3.d.ts +21 -0
- package/dist/cjs/Vec3.js +28 -0
- package/dist/cjs/lib.d.ts +1 -1
- package/dist/cjs/shapes/Abstract2DShape.d.ts +3 -0
- package/dist/cjs/shapes/BezierJSWrapper.d.ts +15 -5
- package/dist/cjs/shapes/BezierJSWrapper.js +135 -18
- package/dist/cjs/shapes/LineSegment2.d.ts +34 -5
- package/dist/cjs/shapes/LineSegment2.js +63 -10
- package/dist/cjs/shapes/Parameterized2DShape.d.ts +31 -0
- package/dist/cjs/shapes/Parameterized2DShape.js +15 -0
- package/dist/cjs/shapes/Path.d.ts +40 -6
- package/dist/cjs/shapes/Path.js +173 -15
- package/dist/cjs/shapes/PointShape2D.d.ts +14 -3
- package/dist/cjs/shapes/PointShape2D.js +28 -5
- package/dist/cjs/shapes/QuadraticBezier.d.ts +4 -0
- package/dist/cjs/shapes/QuadraticBezier.js +19 -4
- package/dist/cjs/shapes/Rect2.d.ts +3 -0
- package/dist/cjs/shapes/Rect2.js +4 -1
- package/dist/mjs/Vec3.d.ts +21 -0
- package/dist/mjs/Vec3.mjs +28 -0
- package/dist/mjs/lib.d.ts +1 -1
- package/dist/mjs/shapes/Abstract2DShape.d.ts +3 -0
- package/dist/mjs/shapes/BezierJSWrapper.d.ts +15 -5
- package/dist/mjs/shapes/BezierJSWrapper.mjs +133 -18
- package/dist/mjs/shapes/LineSegment2.d.ts +34 -5
- package/dist/mjs/shapes/LineSegment2.mjs +63 -10
- package/dist/mjs/shapes/Parameterized2DShape.d.ts +31 -0
- package/dist/mjs/shapes/Parameterized2DShape.mjs +8 -0
- package/dist/mjs/shapes/Path.d.ts +40 -6
- package/dist/mjs/shapes/Path.mjs +173 -15
- package/dist/mjs/shapes/PointShape2D.d.ts +14 -3
- package/dist/mjs/shapes/PointShape2D.mjs +28 -5
- package/dist/mjs/shapes/QuadraticBezier.d.ts +4 -0
- package/dist/mjs/shapes/QuadraticBezier.mjs +19 -4
- package/dist/mjs/shapes/Rect2.d.ts +3 -0
- package/dist/mjs/shapes/Rect2.mjs +4 -1
- package/package.json +5 -5
- package/src/Vec3.test.ts +26 -7
- package/src/Vec3.ts +30 -0
- package/src/lib.ts +2 -0
- package/src/shapes/Abstract2DShape.ts +3 -0
- package/src/shapes/BezierJSWrapper.ts +154 -14
- package/src/shapes/LineSegment2.test.ts +35 -1
- package/src/shapes/LineSegment2.ts +79 -11
- package/src/shapes/Parameterized2DShape.ts +39 -0
- package/src/shapes/Path.test.ts +63 -3
- package/src/shapes/Path.ts +209 -25
- package/src/shapes/PointShape2D.ts +33 -6
- package/src/shapes/QuadraticBezier.test.ts +48 -12
- package/src/shapes/QuadraticBezier.ts +23 -5
- package/src/shapes/Rect2.ts +4 -1
package/dist/mjs/shapes/Path.mjs
CHANGED
@@ -230,7 +230,7 @@ export class Path {
|
|
230
230
|
for (const { part, distFn, bbox } of uncheckedDistFunctions) {
|
231
231
|
// Skip if impossible for the distance to the target to be lesser than
|
232
232
|
// the current minimum.
|
233
|
-
if (!bbox.grownBy(minDist).containsPoint(point)) {
|
233
|
+
if (isFinite(minDist) && !bbox.grownBy(minDist).containsPoint(point)) {
|
234
234
|
continue;
|
235
235
|
}
|
236
236
|
const currentDist = distFn(point);
|
@@ -268,7 +268,7 @@ export class Path {
|
|
268
268
|
});
|
269
269
|
const result = [];
|
270
270
|
const stoppingThreshold = strokeRadius / 1000;
|
271
|
-
// Returns the maximum
|
271
|
+
// Returns the maximum parameter value explored
|
272
272
|
const raymarchFrom = (startPoint,
|
273
273
|
// Direction to march in (multiplies line.direction)
|
274
274
|
directionMultiplier,
|
@@ -312,9 +312,14 @@ export class Path {
|
|
312
312
|
if (lastPart && isOnLineSegment && Math.abs(lastDist) < stoppingThreshold) {
|
313
313
|
result.push({
|
314
314
|
point: currentPoint,
|
315
|
-
parameterValue: NaN,
|
315
|
+
parameterValue: NaN, // lastPart.nearestPointTo(currentPoint).parameterValue,
|
316
316
|
curve: lastPart,
|
317
|
+
curveIndex: this.geometry.indexOf(lastPart),
|
317
318
|
});
|
319
|
+
// Slightly increase the parameter value to prevent the same point from being
|
320
|
+
// added to the results twice.
|
321
|
+
const parameterIncrease = strokeRadius / 20 / line.length;
|
322
|
+
lastParameter += isFinite(parameterIncrease) ? parameterIncrease : 0;
|
318
323
|
}
|
319
324
|
return lastParameter;
|
320
325
|
};
|
@@ -347,14 +352,18 @@ export class Path {
|
|
347
352
|
if (!line.bbox.intersects(this.bbox.grownBy(strokeRadius ?? 0))) {
|
348
353
|
return [];
|
349
354
|
}
|
355
|
+
let index = 0;
|
350
356
|
for (const part of this.geometry) {
|
351
|
-
const
|
352
|
-
|
357
|
+
const intersections = part.argIntersectsLineSegment(line);
|
358
|
+
for (const intersection of intersections) {
|
353
359
|
result.push({
|
354
360
|
curve: part,
|
355
|
-
|
361
|
+
curveIndex: index,
|
362
|
+
point: part.at(intersection),
|
363
|
+
parameterValue: intersection,
|
356
364
|
});
|
357
365
|
}
|
366
|
+
index++;
|
358
367
|
}
|
359
368
|
// If given a non-zero strokeWidth, attempt to raymarch.
|
360
369
|
// Even if raymarching, we need to collect starting points.
|
@@ -367,6 +376,42 @@ export class Path {
|
|
367
376
|
}
|
368
377
|
return result;
|
369
378
|
}
|
379
|
+
/**
|
380
|
+
* @returns the nearest point on this path to the given `point`.
|
381
|
+
*
|
382
|
+
* @internal
|
383
|
+
* @beta
|
384
|
+
*/
|
385
|
+
nearestPointTo(point) {
|
386
|
+
// Find the closest point on this
|
387
|
+
let closestSquareDist = Infinity;
|
388
|
+
let closestPartIndex = 0;
|
389
|
+
let closestParameterValue = 0;
|
390
|
+
let closestPoint = this.startPoint;
|
391
|
+
for (let i = 0; i < this.geometry.length; i++) {
|
392
|
+
const current = this.geometry[i];
|
393
|
+
const nearestPoint = current.nearestPointTo(point);
|
394
|
+
const sqareDist = nearestPoint.point.squareDistanceTo(point);
|
395
|
+
if (i === 0 || sqareDist < closestSquareDist) {
|
396
|
+
closestPartIndex = i;
|
397
|
+
closestSquareDist = sqareDist;
|
398
|
+
closestParameterValue = nearestPoint.parameterValue;
|
399
|
+
closestPoint = nearestPoint.point;
|
400
|
+
}
|
401
|
+
}
|
402
|
+
return {
|
403
|
+
curve: this.geometry[closestPartIndex],
|
404
|
+
curveIndex: closestPartIndex,
|
405
|
+
parameterValue: closestParameterValue,
|
406
|
+
point: closestPoint,
|
407
|
+
};
|
408
|
+
}
|
409
|
+
at(index) {
|
410
|
+
return this.geometry[index.curveIndex].at(index.parameterValue);
|
411
|
+
}
|
412
|
+
tangentAt(index) {
|
413
|
+
return this.geometry[index.curveIndex].tangentAt(index.parameterValue);
|
414
|
+
}
|
370
415
|
static mapPathCommand(part, mapping) {
|
371
416
|
switch (part.kind) {
|
372
417
|
case PathCommandType.MoveTo:
|
@@ -410,18 +455,85 @@ export class Path {
|
|
410
455
|
return this.mapPoints(point => affineTransfm.transformVec2(point));
|
411
456
|
}
|
412
457
|
// Creates a new path by joining [other] to the end of this path
|
413
|
-
union(other
|
458
|
+
union(other,
|
459
|
+
// allowReverse: true iff reversing other or this is permitted if it means
|
460
|
+
// no moveTo command is necessary when unioning the paths.
|
461
|
+
options = { allowReverse: true }) {
|
414
462
|
if (!other) {
|
415
463
|
return this;
|
416
464
|
}
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
465
|
+
const thisEnd = this.getEndPoint();
|
466
|
+
let newParts = [];
|
467
|
+
if (thisEnd.eq(other.startPoint)) {
|
468
|
+
newParts = this.parts.concat(other.parts);
|
469
|
+
}
|
470
|
+
else if (options.allowReverse && this.startPoint.eq(other.getEndPoint())) {
|
471
|
+
return other.union(this, { allowReverse: false });
|
472
|
+
}
|
473
|
+
else if (options.allowReverse && this.startPoint.eq(other.startPoint)) {
|
474
|
+
return this.union(other.reversed(), { allowReverse: false });
|
475
|
+
}
|
476
|
+
else {
|
477
|
+
newParts = [
|
478
|
+
...this.parts,
|
479
|
+
{
|
480
|
+
kind: PathCommandType.MoveTo,
|
481
|
+
point: other.startPoint,
|
482
|
+
},
|
483
|
+
...other.parts,
|
484
|
+
];
|
485
|
+
}
|
486
|
+
return new Path(this.startPoint, newParts);
|
487
|
+
}
|
488
|
+
/**
|
489
|
+
* @returns a version of this path with the direction reversed.
|
490
|
+
*
|
491
|
+
* Example:
|
492
|
+
* ```ts,runnable,console
|
493
|
+
* import {Path} from '@js-draw/math';
|
494
|
+
* console.log(Path.fromString('m0,0l1,1').reversed()); // -> M1,1 L0,0
|
495
|
+
* ```
|
496
|
+
*/
|
497
|
+
reversed() {
|
498
|
+
const newStart = this.getEndPoint();
|
499
|
+
const newParts = [];
|
500
|
+
let lastPoint = this.startPoint;
|
501
|
+
for (const part of this.parts) {
|
502
|
+
switch (part.kind) {
|
503
|
+
case PathCommandType.LineTo:
|
504
|
+
case PathCommandType.MoveTo:
|
505
|
+
newParts.push({
|
506
|
+
kind: part.kind,
|
507
|
+
point: lastPoint,
|
508
|
+
});
|
509
|
+
lastPoint = part.point;
|
510
|
+
break;
|
511
|
+
case PathCommandType.CubicBezierTo:
|
512
|
+
newParts.push({
|
513
|
+
kind: part.kind,
|
514
|
+
controlPoint1: part.controlPoint2,
|
515
|
+
controlPoint2: part.controlPoint1,
|
516
|
+
endPoint: lastPoint,
|
517
|
+
});
|
518
|
+
lastPoint = part.endPoint;
|
519
|
+
break;
|
520
|
+
case PathCommandType.QuadraticBezierTo:
|
521
|
+
newParts.push({
|
522
|
+
kind: part.kind,
|
523
|
+
controlPoint: part.controlPoint,
|
524
|
+
endPoint: lastPoint,
|
525
|
+
});
|
526
|
+
lastPoint = part.endPoint;
|
527
|
+
break;
|
528
|
+
default:
|
529
|
+
{
|
530
|
+
const exhaustivenessCheck = part;
|
531
|
+
return exhaustivenessCheck;
|
532
|
+
}
|
533
|
+
}
|
534
|
+
}
|
535
|
+
newParts.reverse();
|
536
|
+
return new Path(newStart, newParts);
|
425
537
|
}
|
426
538
|
getEndPoint() {
|
427
539
|
if (this.parts.length === 0) {
|
@@ -513,6 +625,52 @@ export class Path {
|
|
513
625
|
// Even? Probably no intersection.
|
514
626
|
return false;
|
515
627
|
}
|
628
|
+
/** @returns true if all points on this are equivalent to the points on `other` */
|
629
|
+
eq(other, tolerance) {
|
630
|
+
if (other.parts.length !== this.parts.length) {
|
631
|
+
return false;
|
632
|
+
}
|
633
|
+
for (let i = 0; i < this.parts.length; i++) {
|
634
|
+
const part1 = this.parts[i];
|
635
|
+
const part2 = other.parts[i];
|
636
|
+
switch (part1.kind) {
|
637
|
+
case PathCommandType.LineTo:
|
638
|
+
case PathCommandType.MoveTo:
|
639
|
+
if (part1.kind !== part2.kind) {
|
640
|
+
return false;
|
641
|
+
}
|
642
|
+
else if (!part1.point.eq(part2.point, tolerance)) {
|
643
|
+
return false;
|
644
|
+
}
|
645
|
+
break;
|
646
|
+
case PathCommandType.CubicBezierTo:
|
647
|
+
if (part1.kind !== part2.kind) {
|
648
|
+
return false;
|
649
|
+
}
|
650
|
+
else if (!part1.controlPoint1.eq(part2.controlPoint1, tolerance)
|
651
|
+
|| !part1.controlPoint2.eq(part2.controlPoint2, tolerance)
|
652
|
+
|| !part1.endPoint.eq(part2.endPoint, tolerance)) {
|
653
|
+
return false;
|
654
|
+
}
|
655
|
+
break;
|
656
|
+
case PathCommandType.QuadraticBezierTo:
|
657
|
+
if (part1.kind !== part2.kind) {
|
658
|
+
return false;
|
659
|
+
}
|
660
|
+
else if (!part1.controlPoint.eq(part2.controlPoint, tolerance)
|
661
|
+
|| !part1.endPoint.eq(part2.endPoint, tolerance)) {
|
662
|
+
return false;
|
663
|
+
}
|
664
|
+
break;
|
665
|
+
default:
|
666
|
+
{
|
667
|
+
const exhaustivenessCheck = part1;
|
668
|
+
return exhaustivenessCheck;
|
669
|
+
}
|
670
|
+
}
|
671
|
+
}
|
672
|
+
return true;
|
673
|
+
}
|
516
674
|
/**
|
517
675
|
* Returns a path that outlines `rect`.
|
518
676
|
*
|
@@ -1,18 +1,29 @@
|
|
1
1
|
import { Point2 } from '../Vec2';
|
2
2
|
import Vec3 from '../Vec3';
|
3
|
-
import Abstract2DShape from './Abstract2DShape';
|
4
3
|
import LineSegment2 from './LineSegment2';
|
4
|
+
import Parameterized2DShape from './Parameterized2DShape';
|
5
5
|
import Rect2 from './Rect2';
|
6
6
|
/**
|
7
7
|
* Like a {@link Point2}, but with additional functionality (e.g. SDF).
|
8
8
|
*
|
9
9
|
* Access the internal `Point2` using the `p` property.
|
10
10
|
*/
|
11
|
-
declare class PointShape2D extends
|
11
|
+
declare class PointShape2D extends Parameterized2DShape {
|
12
12
|
readonly p: Point2;
|
13
13
|
constructor(p: Point2);
|
14
14
|
signedDistance(point: Vec3): number;
|
15
|
-
|
15
|
+
argIntersectsLineSegment(lineSegment: LineSegment2, epsilon?: number): number[];
|
16
16
|
getTightBoundingBox(): Rect2;
|
17
|
+
at(_t: number): Vec3;
|
18
|
+
/**
|
19
|
+
* Returns an arbitrary unit-length vector.
|
20
|
+
*/
|
21
|
+
normalAt(_t: number): Vec3;
|
22
|
+
tangentAt(_t: number): Vec3;
|
23
|
+
splitAt(_t: number): [PointShape2D];
|
24
|
+
nearestPointTo(_point: Point2): {
|
25
|
+
point: Vec3;
|
26
|
+
parameterValue: number;
|
27
|
+
};
|
17
28
|
}
|
18
29
|
export default PointShape2D;
|
@@ -1,26 +1,49 @@
|
|
1
|
-
import
|
1
|
+
import { Vec2 } from '../Vec2.mjs';
|
2
|
+
import Parameterized2DShape from './Parameterized2DShape.mjs';
|
2
3
|
import Rect2 from './Rect2.mjs';
|
3
4
|
/**
|
4
5
|
* Like a {@link Point2}, but with additional functionality (e.g. SDF).
|
5
6
|
*
|
6
7
|
* Access the internal `Point2` using the `p` property.
|
7
8
|
*/
|
8
|
-
class PointShape2D extends
|
9
|
+
class PointShape2D extends Parameterized2DShape {
|
9
10
|
constructor(p) {
|
10
11
|
super();
|
11
12
|
this.p = p;
|
12
13
|
}
|
13
14
|
signedDistance(point) {
|
14
|
-
return this.p.
|
15
|
+
return this.p.distanceTo(point);
|
15
16
|
}
|
16
|
-
|
17
|
+
argIntersectsLineSegment(lineSegment, epsilon) {
|
17
18
|
if (lineSegment.containsPoint(this.p, epsilon)) {
|
18
|
-
return [
|
19
|
+
return [0];
|
19
20
|
}
|
20
21
|
return [];
|
21
22
|
}
|
22
23
|
getTightBoundingBox() {
|
23
24
|
return new Rect2(this.p.x, this.p.y, 0, 0);
|
24
25
|
}
|
26
|
+
at(_t) {
|
27
|
+
return this.p;
|
28
|
+
}
|
29
|
+
/**
|
30
|
+
* Returns an arbitrary unit-length vector.
|
31
|
+
*/
|
32
|
+
normalAt(_t) {
|
33
|
+
// Return a vector that makes sense.
|
34
|
+
return Vec2.unitY;
|
35
|
+
}
|
36
|
+
tangentAt(_t) {
|
37
|
+
return Vec2.unitX;
|
38
|
+
}
|
39
|
+
splitAt(_t) {
|
40
|
+
return [this];
|
41
|
+
}
|
42
|
+
nearestPointTo(_point) {
|
43
|
+
return {
|
44
|
+
point: this.p,
|
45
|
+
parameterValue: 0,
|
46
|
+
};
|
47
|
+
}
|
25
48
|
}
|
26
49
|
export default PointShape2D;
|
@@ -18,11 +18,15 @@ export declare class QuadraticBezier extends BezierJSWrapper {
|
|
18
18
|
*/
|
19
19
|
private static componentAt;
|
20
20
|
private static derivativeComponentAt;
|
21
|
+
private static secondDerivativeComponentAt;
|
21
22
|
/**
|
22
23
|
* @returns the curve evaluated at `t`.
|
24
|
+
*
|
25
|
+
* `t` should be a number in `[0, 1]`.
|
23
26
|
*/
|
24
27
|
at(t: number): Point2;
|
25
28
|
derivativeAt(t: number): Point2;
|
29
|
+
secondDerivativeAt(t: number): Point2;
|
26
30
|
normal(t: number): Vec2;
|
27
31
|
/** @returns an overestimate of this shape's bounding box. */
|
28
32
|
getLooseBoundingBox(): Rect2;
|
@@ -25,10 +25,19 @@ export class QuadraticBezier extends BezierJSWrapper {
|
|
25
25
|
static derivativeComponentAt(t, p0, p1, p2) {
|
26
26
|
return -2 * p0 + 2 * p1 + 2 * t * (p0 - 2 * p1 + p2);
|
27
27
|
}
|
28
|
+
static secondDerivativeComponentAt(t, p0, p1, p2) {
|
29
|
+
return 2 * (p0 - 2 * p1 + p2);
|
30
|
+
}
|
28
31
|
/**
|
29
32
|
* @returns the curve evaluated at `t`.
|
33
|
+
*
|
34
|
+
* `t` should be a number in `[0, 1]`.
|
30
35
|
*/
|
31
36
|
at(t) {
|
37
|
+
if (t === 0)
|
38
|
+
return this.p0;
|
39
|
+
if (t === 1)
|
40
|
+
return this.p2;
|
32
41
|
const p0 = this.p0;
|
33
42
|
const p1 = this.p1;
|
34
43
|
const p2 = this.p2;
|
@@ -40,6 +49,12 @@ export class QuadraticBezier extends BezierJSWrapper {
|
|
40
49
|
const p2 = this.p2;
|
41
50
|
return Vec2.of(QuadraticBezier.derivativeComponentAt(t, p0.x, p1.x, p2.x), QuadraticBezier.derivativeComponentAt(t, p0.y, p1.y, p2.y));
|
42
51
|
}
|
52
|
+
secondDerivativeAt(t) {
|
53
|
+
const p0 = this.p0;
|
54
|
+
const p1 = this.p1;
|
55
|
+
const p2 = this.p2;
|
56
|
+
return Vec2.of(QuadraticBezier.secondDerivativeComponentAt(t, p0.x, p1.x, p2.x), QuadraticBezier.secondDerivativeComponentAt(t, p0.y, p1.y, p2.y));
|
57
|
+
}
|
43
58
|
normal(t) {
|
44
59
|
const tangent = this.derivativeAt(t);
|
45
60
|
return tangent.orthog().normalized();
|
@@ -100,10 +115,10 @@ export class QuadraticBezier extends BezierJSWrapper {
|
|
100
115
|
}
|
101
116
|
const at1 = this.at(min1);
|
102
117
|
const at2 = this.at(min2);
|
103
|
-
const sqrDist1 = at1.
|
104
|
-
const sqrDist2 = at2.
|
105
|
-
const sqrDist3 = this.at(0).
|
106
|
-
const sqrDist4 = this.at(1).
|
118
|
+
const sqrDist1 = at1.squareDistanceTo(point);
|
119
|
+
const sqrDist2 = at2.squareDistanceTo(point);
|
120
|
+
const sqrDist3 = this.at(0).squareDistanceTo(point);
|
121
|
+
const sqrDist4 = this.at(1).squareDistanceTo(point);
|
107
122
|
return Math.sqrt(Math.min(sqrDist1, sqrDist2, sqrDist3, sqrDist4));
|
108
123
|
}
|
109
124
|
getPoints() {
|
@@ -25,6 +25,9 @@ export declare class Rect2 extends Abstract2DShape {
|
|
25
25
|
resizedTo(size: Vec2): Rect2;
|
26
26
|
containsPoint(other: Point2): boolean;
|
27
27
|
containsRect(other: Rect2): boolean;
|
28
|
+
/**
|
29
|
+
* @returns true iff this and `other` overlap
|
30
|
+
*/
|
28
31
|
intersects(other: Rect2): boolean;
|
29
32
|
intersection(other: Rect2): Rect2 | null;
|
30
33
|
union(other: Rect2): Rect2;
|
@@ -38,6 +38,9 @@ export class Rect2 extends Abstract2DShape {
|
|
38
38
|
&& this.x + this.w >= other.x + other.w
|
39
39
|
&& this.y + this.h >= other.y + other.h;
|
40
40
|
}
|
41
|
+
/**
|
42
|
+
* @returns true iff this and `other` overlap
|
43
|
+
*/
|
41
44
|
intersects(other) {
|
42
45
|
// Project along x/y axes.
|
43
46
|
const thisMinX = this.x;
|
@@ -124,7 +127,7 @@ export class Rect2 extends Abstract2DShape {
|
|
124
127
|
let closest = null;
|
125
128
|
let closestDist = null;
|
126
129
|
for (const point of closestEdgePoints) {
|
127
|
-
const dist = point.
|
130
|
+
const dist = point.distanceTo(target);
|
128
131
|
if (closestDist === null || dist < closestDist) {
|
129
132
|
closest = point;
|
130
133
|
closestDist = dist;
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@js-draw/math",
|
3
|
-
"version": "1.
|
3
|
+
"version": "1.17.0",
|
4
4
|
"description": "A math library for js-draw. ",
|
5
5
|
"types": "./dist/mjs/lib.d.ts",
|
6
6
|
"main": "./dist/cjs/lib.js",
|
@@ -21,14 +21,14 @@
|
|
21
21
|
"scripts": {
|
22
22
|
"dist-test": "cd dist-test/test_imports && npm install && npm run test",
|
23
23
|
"dist": "npm run build && npm run dist-test",
|
24
|
-
"build": "rm -rf ./dist &&
|
25
|
-
"watch": "
|
24
|
+
"build": "rm -rf ./dist && build-tool build",
|
25
|
+
"watch": "build-tool watch"
|
26
26
|
},
|
27
27
|
"dependencies": {
|
28
28
|
"bezier-js": "6.1.3"
|
29
29
|
},
|
30
30
|
"devDependencies": {
|
31
|
-
"@js-draw/build-tool": "^1.
|
31
|
+
"@js-draw/build-tool": "^1.17.0",
|
32
32
|
"@types/bezier-js": "4.1.0",
|
33
33
|
"@types/jest": "29.5.5",
|
34
34
|
"@types/jsdom": "21.1.3"
|
@@ -45,5 +45,5 @@
|
|
45
45
|
"svg",
|
46
46
|
"math"
|
47
47
|
],
|
48
|
-
"gitHead": "
|
48
|
+
"gitHead": "d0eff585750ab5670af3acda8ddff090e8825bd3"
|
49
49
|
}
|
package/src/Vec3.test.ts
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
import Vec3 from './Vec3';
|
3
3
|
|
4
4
|
describe('Vec3', () => {
|
5
|
-
|
5
|
+
test('.xy should contain the x and y components', () => {
|
6
6
|
const vec = Vec3.of(1, 2, 3);
|
7
7
|
expect(vec.xy).toMatchObject({
|
8
8
|
x: 1,
|
@@ -10,42 +10,61 @@ describe('Vec3', () => {
|
|
10
10
|
});
|
11
11
|
});
|
12
12
|
|
13
|
-
|
13
|
+
test('should be combinable with other vectors via .zip', () => {
|
14
14
|
const vec1 = Vec3.unitX;
|
15
15
|
const vec2 = Vec3.unitY;
|
16
16
|
expect(vec1.zip(vec2, Math.min)).objEq(Vec3.zero);
|
17
17
|
expect(vec1.zip(vec2, Math.max)).objEq(Vec3.of(1, 1, 0));
|
18
18
|
});
|
19
19
|
|
20
|
-
|
20
|
+
test('.cross should obey the right hand rule', () => {
|
21
21
|
const vec1 = Vec3.unitX;
|
22
22
|
const vec2 = Vec3.unitY;
|
23
23
|
expect(vec1.cross(vec2)).objEq(Vec3.unitZ);
|
24
24
|
expect(vec2.cross(vec1)).objEq(Vec3.unitZ.times(-1));
|
25
25
|
});
|
26
26
|
|
27
|
-
|
27
|
+
test('.orthog should return an orthogonal vector', () => {
|
28
28
|
expect(Vec3.unitZ.orthog().dot(Vec3.unitZ)).toBe(0);
|
29
29
|
|
30
30
|
// Should return some orthogonal vector, even if given the zero vector
|
31
31
|
expect(Vec3.zero.orthog().dot(Vec3.zero)).toBe(0);
|
32
32
|
});
|
33
33
|
|
34
|
-
|
34
|
+
test('.minus should return the difference between two vectors', () => {
|
35
35
|
expect(Vec3.of(1, 2, 3).minus(Vec3.of(4, 5, 6))).objEq(Vec3.of(1 - 4, 2 - 5, 3 - 6));
|
36
36
|
});
|
37
37
|
|
38
|
-
|
38
|
+
test('.orthog should return a unit vector', () => {
|
39
39
|
expect(Vec3.zero.orthog().magnitude()).toBe(1);
|
40
40
|
expect(Vec3.unitZ.orthog().magnitude()).toBe(1);
|
41
41
|
expect(Vec3.unitX.orthog().magnitude()).toBe(1);
|
42
42
|
expect(Vec3.unitY.orthog().magnitude()).toBe(1);
|
43
43
|
});
|
44
44
|
|
45
|
-
|
45
|
+
test('.normalizedOrZero should normalize the given vector or return zero', () => {
|
46
46
|
expect(Vec3.zero.normalizedOrZero()).objEq(Vec3.zero);
|
47
47
|
expect(Vec3.unitX.normalizedOrZero()).objEq(Vec3.unitX);
|
48
48
|
expect(Vec3.unitX.times(22).normalizedOrZero()).objEq(Vec3.unitX);
|
49
49
|
expect(Vec3.of(1, 1, 1).times(22).normalizedOrZero().length()).toBeCloseTo(1);
|
50
50
|
});
|
51
|
+
|
52
|
+
test.each([
|
53
|
+
{ from: Vec3.of(1, 1, 1), to: Vec3.of(1, 2, 1), expected: 1 },
|
54
|
+
{ from: Vec3.of(1, 1, 1), to: Vec3.of(1, 2, 2), expected: 2 },
|
55
|
+
{ from: Vec3.of(1, 1, 1), to: Vec3.of(2, 2, 2), expected: 3 },
|
56
|
+
{ from: Vec3.of(1, 1, 1), to: Vec3.of(0, 1, 1), expected: 1 },
|
57
|
+
{ from: Vec3.of(1, 1, 1), to: Vec3.of(0, 1, 0), expected: 2 },
|
58
|
+
{ from: Vec3.of(1, 1, 1), to: Vec3.of(0, 0, 0), expected: 3 },
|
59
|
+
{ from: Vec3.of(-1, -10, 0), to: Vec3.of(1, 2, 0), expected: 148 },
|
60
|
+
])(
|
61
|
+
'.squareDistanceTo and .distanceTo should return correct square and euclidean distances (%j)',
|
62
|
+
({ from , to, expected }) => {
|
63
|
+
expect(from.squareDistanceTo(to)).toBe(expected);
|
64
|
+
expect(to.squareDistanceTo(from)).toBe(expected);
|
65
|
+
expect(to.distanceTo(from)).toBeCloseTo(Math.sqrt(expected));
|
66
|
+
expect(to.minus(from).magnitudeSquared()).toBe(expected);
|
67
|
+
expect(from.minus(to).magnitudeSquared()).toBe(expected);
|
68
|
+
},
|
69
|
+
);
|
51
70
|
});
|
package/src/Vec3.ts
CHANGED
@@ -63,11 +63,40 @@ export class Vec3 {
|
|
63
63
|
return this.dot(this);
|
64
64
|
}
|
65
65
|
|
66
|
+
/**
|
67
|
+
* Interpreting this vector as a point in ℝ^3, computes the square distance
|
68
|
+
* to another point, `p`.
|
69
|
+
*
|
70
|
+
* Equivalent to `.minus(p).magnitudeSquared()`.
|
71
|
+
*/
|
72
|
+
public squareDistanceTo(p: Vec3) {
|
73
|
+
const dx = this.x - p.x;
|
74
|
+
const dy = this.y - p.y;
|
75
|
+
const dz = this.z - p.z;
|
76
|
+
return dx * dx + dy * dy + dz * dz;
|
77
|
+
}
|
78
|
+
|
79
|
+
/**
|
80
|
+
* Interpreting this vector as a point in ℝ³, returns the distance to the point
|
81
|
+
* `p`.
|
82
|
+
*
|
83
|
+
* Equivalent to `.minus(p).magnitude()`.
|
84
|
+
*/
|
85
|
+
public distanceTo(p: Vec3) {
|
86
|
+
return Math.sqrt(this.squareDistanceTo(p));
|
87
|
+
}
|
88
|
+
|
66
89
|
/**
|
67
90
|
* Returns the entry of this with the greatest magnitude.
|
68
91
|
*
|
69
92
|
* In other words, returns $\max \{ |x| : x \in {\bf v} \}$, where ${\bf v}$ is the set of
|
70
93
|
* all entries of this vector.
|
94
|
+
*
|
95
|
+
* **Example**:
|
96
|
+
* ```ts,runnable,console
|
97
|
+
* import { Vec3 } from '@js-draw/math';
|
98
|
+
* console.log(Vec3.of(-1, -10, 8).maximumEntryMagnitude()); // -> 10
|
99
|
+
* ```
|
71
100
|
*/
|
72
101
|
public maximumEntryMagnitude(): number {
|
73
102
|
return Math.max(Math.abs(this.x), Math.max(Math.abs(this.y), Math.abs(this.z)));
|
@@ -81,6 +110,7 @@ export class Vec3 {
|
|
81
110
|
* As such, observing that `Math.atan2(-0, -1)` $\approx -\pi$ and `Math.atan2(0, -1)`$\approx \pi$
|
82
111
|
* the resultant angle is in the range $[-\pi, pi]$.
|
83
112
|
*
|
113
|
+
* **Example**:
|
84
114
|
* ```ts,runnable,console
|
85
115
|
* import { Vec2 } from '@js-draw/math';
|
86
116
|
* console.log(Vec2.of(-1, -0).angle()); // atan2(-0, -1)
|
package/src/lib.ts
CHANGED
@@ -49,6 +49,9 @@ export abstract class Abstract2DShape {
|
|
49
49
|
|
50
50
|
/**
|
51
51
|
* Returns a bounding box that precisely fits the content of this shape.
|
52
|
+
*
|
53
|
+
* **Note**: This bounding box should aligned with the x/y axes. (Thus, it may be
|
54
|
+
* possible to find a tighter bounding box not axes-aligned).
|
52
55
|
*/
|
53
56
|
public abstract getTightBoundingBox(): Rect2;
|
54
57
|
|