@js-draw/math 1.16.0 → 1.17.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/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
|
|