@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/src/shapes/Path.ts
CHANGED
@@ -2,12 +2,12 @@ import LineSegment2 from './LineSegment2';
|
|
2
2
|
import Mat33 from '../Mat33';
|
3
3
|
import Rect2 from './Rect2';
|
4
4
|
import { Point2, Vec2 } from '../Vec2';
|
5
|
-
import Abstract2DShape from './Abstract2DShape';
|
6
5
|
import CubicBezier from './CubicBezier';
|
7
6
|
import QuadraticBezier from './QuadraticBezier';
|
8
7
|
import PointShape2D from './PointShape2D';
|
9
8
|
import toRoundedString from '../rounding/toRoundedString';
|
10
9
|
import toStringOfSamePrecision from '../rounding/toStringOfSamePrecision';
|
10
|
+
import Parameterized2DShape from './Parameterized2DShape';
|
11
11
|
|
12
12
|
export enum PathCommandType {
|
13
13
|
LineTo,
|
@@ -41,17 +41,29 @@ export interface MoveToPathCommand {
|
|
41
41
|
|
42
42
|
export type PathCommand = CubicBezierPathCommand | QuadraticBezierPathCommand | MoveToPathCommand | LinePathCommand;
|
43
43
|
|
44
|
-
interface IntersectionResult {
|
44
|
+
export interface IntersectionResult {
|
45
45
|
// @internal
|
46
|
-
curve:
|
46
|
+
curve: Parameterized2DShape;
|
47
|
+
// @internal
|
48
|
+
curveIndex: number;
|
47
49
|
|
48
|
-
/** @internal @deprecated */
|
50
|
+
/** Parameter value for the closest point **on** the path to the intersection. @internal @deprecated */
|
49
51
|
parameterValue?: number;
|
50
52
|
|
51
|
-
|
53
|
+
/** Point at which the intersection occured. */
|
52
54
|
point: Point2;
|
53
55
|
}
|
54
56
|
|
57
|
+
/**
|
58
|
+
* Allows indexing a particular part of a path.
|
59
|
+
*
|
60
|
+
* @see {@link Path.at} {@link Path.tangentAt}
|
61
|
+
*/
|
62
|
+
export interface CurveIndexRecord {
|
63
|
+
curveIndex: number;
|
64
|
+
parameterValue: number;
|
65
|
+
}
|
66
|
+
|
55
67
|
/**
|
56
68
|
* Represents a union of lines and curves.
|
57
69
|
*/
|
@@ -97,16 +109,16 @@ export class Path {
|
|
97
109
|
return Rect2.union(...bboxes);
|
98
110
|
}
|
99
111
|
|
100
|
-
private cachedGeometry:
|
112
|
+
private cachedGeometry: Parameterized2DShape[]|null = null;
|
101
113
|
|
102
114
|
// Lazy-loads and returns this path's geometry
|
103
|
-
public get geometry():
|
115
|
+
public get geometry(): Parameterized2DShape[] {
|
104
116
|
if (this.cachedGeometry) {
|
105
117
|
return this.cachedGeometry;
|
106
118
|
}
|
107
119
|
|
108
120
|
let startPoint = this.startPoint;
|
109
|
-
const geometry:
|
121
|
+
const geometry: Parameterized2DShape[] = [];
|
110
122
|
|
111
123
|
for (const part of this.parts) {
|
112
124
|
let exhaustivenessCheck: never;
|
@@ -270,7 +282,7 @@ export class Path {
|
|
270
282
|
|
271
283
|
type DistanceFunction = (point: Point2) => number;
|
272
284
|
type DistanceFunctionRecord = {
|
273
|
-
part:
|
285
|
+
part: Parameterized2DShape,
|
274
286
|
bbox: Rect2,
|
275
287
|
distFn: DistanceFunction,
|
276
288
|
};
|
@@ -309,9 +321,9 @@ export class Path {
|
|
309
321
|
|
310
322
|
// Returns the minimum distance to a part in this stroke, where only parts that the given
|
311
323
|
// line could intersect are considered.
|
312
|
-
const sdf = (point: Point2): [
|
324
|
+
const sdf = (point: Point2): [Parameterized2DShape|null, number] => {
|
313
325
|
let minDist = Infinity;
|
314
|
-
let minDistPart:
|
326
|
+
let minDistPart: Parameterized2DShape|null = null;
|
315
327
|
|
316
328
|
const uncheckedDistFunctions: DistanceFunctionRecord[] = [];
|
317
329
|
|
@@ -338,7 +350,7 @@ export class Path {
|
|
338
350
|
for (const { part, distFn, bbox } of uncheckedDistFunctions) {
|
339
351
|
// Skip if impossible for the distance to the target to be lesser than
|
340
352
|
// the current minimum.
|
341
|
-
if (!bbox.grownBy(minDist).containsPoint(point)) {
|
353
|
+
if (isFinite(minDist) && !bbox.grownBy(minDist).containsPoint(point)) {
|
342
354
|
continue;
|
343
355
|
}
|
344
356
|
|
@@ -388,7 +400,7 @@ export class Path {
|
|
388
400
|
|
389
401
|
const stoppingThreshold = strokeRadius / 1000;
|
390
402
|
|
391
|
-
// Returns the maximum
|
403
|
+
// Returns the maximum parameter value explored
|
392
404
|
const raymarchFrom = (
|
393
405
|
startPoint: Point2,
|
394
406
|
|
@@ -446,9 +458,15 @@ export class Path {
|
|
446
458
|
if (lastPart && isOnLineSegment && Math.abs(lastDist) < stoppingThreshold) {
|
447
459
|
result.push({
|
448
460
|
point: currentPoint,
|
449
|
-
parameterValue: NaN,
|
461
|
+
parameterValue: NaN,// lastPart.nearestPointTo(currentPoint).parameterValue,
|
450
462
|
curve: lastPart,
|
463
|
+
curveIndex: this.geometry.indexOf(lastPart),
|
451
464
|
});
|
465
|
+
|
466
|
+
// Slightly increase the parameter value to prevent the same point from being
|
467
|
+
// added to the results twice.
|
468
|
+
const parameterIncrease = strokeRadius / 20 / line.length;
|
469
|
+
lastParameter += isFinite(parameterIncrease) ? parameterIncrease : 0;
|
452
470
|
}
|
453
471
|
|
454
472
|
return lastParameter;
|
@@ -489,15 +507,20 @@ export class Path {
|
|
489
507
|
return [];
|
490
508
|
}
|
491
509
|
|
510
|
+
let index = 0;
|
492
511
|
for (const part of this.geometry) {
|
493
|
-
const
|
512
|
+
const intersections = part.argIntersectsLineSegment(line);
|
494
513
|
|
495
|
-
|
514
|
+
for (const intersection of intersections) {
|
496
515
|
result.push({
|
497
516
|
curve: part,
|
498
|
-
|
517
|
+
curveIndex: index,
|
518
|
+
point: part.at(intersection),
|
519
|
+
parameterValue: intersection,
|
499
520
|
});
|
500
521
|
}
|
522
|
+
|
523
|
+
index ++;
|
501
524
|
}
|
502
525
|
|
503
526
|
// If given a non-zero strokeWidth, attempt to raymarch.
|
@@ -513,6 +536,47 @@ export class Path {
|
|
513
536
|
return result;
|
514
537
|
}
|
515
538
|
|
539
|
+
/**
|
540
|
+
* @returns the nearest point on this path to the given `point`.
|
541
|
+
*
|
542
|
+
* @internal
|
543
|
+
* @beta
|
544
|
+
*/
|
545
|
+
public nearestPointTo(point: Point2): IntersectionResult {
|
546
|
+
// Find the closest point on this
|
547
|
+
let closestSquareDist = Infinity;
|
548
|
+
let closestPartIndex = 0;
|
549
|
+
let closestParameterValue = 0;
|
550
|
+
let closestPoint: Point2 = this.startPoint;
|
551
|
+
|
552
|
+
for (let i = 0; i < this.geometry.length; i++) {
|
553
|
+
const current = this.geometry[i];
|
554
|
+
const nearestPoint = current.nearestPointTo(point);
|
555
|
+
const sqareDist = nearestPoint.point.squareDistanceTo(point);
|
556
|
+
if (i === 0 || sqareDist < closestSquareDist) {
|
557
|
+
closestPartIndex = i;
|
558
|
+
closestSquareDist = sqareDist;
|
559
|
+
closestParameterValue = nearestPoint.parameterValue;
|
560
|
+
closestPoint = nearestPoint.point;
|
561
|
+
}
|
562
|
+
}
|
563
|
+
|
564
|
+
return {
|
565
|
+
curve: this.geometry[closestPartIndex],
|
566
|
+
curveIndex: closestPartIndex,
|
567
|
+
parameterValue: closestParameterValue,
|
568
|
+
point: closestPoint,
|
569
|
+
};
|
570
|
+
}
|
571
|
+
|
572
|
+
public at(index: CurveIndexRecord) {
|
573
|
+
return this.geometry[index.curveIndex].at(index.parameterValue);
|
574
|
+
}
|
575
|
+
|
576
|
+
public tangentAt(index: CurveIndexRecord) {
|
577
|
+
return this.geometry[index.curveIndex].tangentAt(index.parameterValue);
|
578
|
+
}
|
579
|
+
|
516
580
|
private static mapPathCommand(part: PathCommand, mapping: (point: Point2)=> Point2): PathCommand {
|
517
581
|
switch (part.kind) {
|
518
582
|
case PathCommandType.MoveTo:
|
@@ -563,19 +627,88 @@ export class Path {
|
|
563
627
|
}
|
564
628
|
|
565
629
|
// Creates a new path by joining [other] to the end of this path
|
566
|
-
public union(
|
630
|
+
public union(
|
631
|
+
other: Path|null,
|
632
|
+
|
633
|
+
// allowReverse: true iff reversing other or this is permitted if it means
|
634
|
+
// no moveTo command is necessary when unioning the paths.
|
635
|
+
options: { allowReverse?: boolean } = { allowReverse: true },
|
636
|
+
): Path {
|
567
637
|
if (!other) {
|
568
638
|
return this;
|
569
639
|
}
|
570
640
|
|
571
|
-
|
572
|
-
|
641
|
+
const thisEnd = this.getEndPoint();
|
642
|
+
|
643
|
+
let newParts: Readonly<PathCommand>[] = [];
|
644
|
+
if (thisEnd.eq(other.startPoint)) {
|
645
|
+
newParts = this.parts.concat(other.parts);
|
646
|
+
} else if (options.allowReverse && this.startPoint.eq(other.getEndPoint())) {
|
647
|
+
return other.union(this, { allowReverse: false });
|
648
|
+
} else if (options.allowReverse && this.startPoint.eq(other.startPoint)) {
|
649
|
+
return this.union(other.reversed(), { allowReverse: false });
|
650
|
+
} else {
|
651
|
+
newParts = [
|
652
|
+
...this.parts,
|
653
|
+
{
|
654
|
+
kind: PathCommandType.MoveTo,
|
655
|
+
point: other.startPoint,
|
656
|
+
},
|
657
|
+
...other.parts,
|
658
|
+
];
|
659
|
+
}
|
660
|
+
return new Path(this.startPoint, newParts);
|
661
|
+
}
|
662
|
+
|
663
|
+
/**
|
664
|
+
* @returns a version of this path with the direction reversed.
|
665
|
+
*
|
666
|
+
* Example:
|
667
|
+
* ```ts,runnable,console
|
668
|
+
* import {Path} from '@js-draw/math';
|
669
|
+
* console.log(Path.fromString('m0,0l1,1').reversed()); // -> M1,1 L0,0
|
670
|
+
* ```
|
671
|
+
*/
|
672
|
+
public reversed() {
|
673
|
+
const newStart = this.getEndPoint();
|
674
|
+
const newParts: Readonly<PathCommand>[] = [];
|
675
|
+
let lastPoint: Point2 = this.startPoint;
|
676
|
+
for (const part of this.parts) {
|
677
|
+
switch (part.kind) {
|
678
|
+
case PathCommandType.LineTo:
|
679
|
+
case PathCommandType.MoveTo:
|
680
|
+
newParts.push({
|
681
|
+
kind: part.kind,
|
682
|
+
point: lastPoint,
|
683
|
+
});
|
684
|
+
lastPoint = part.point;
|
685
|
+
break;
|
686
|
+
case PathCommandType.CubicBezierTo:
|
687
|
+
newParts.push({
|
688
|
+
kind: part.kind,
|
689
|
+
controlPoint1: part.controlPoint2,
|
690
|
+
controlPoint2: part.controlPoint1,
|
691
|
+
endPoint: lastPoint,
|
692
|
+
});
|
693
|
+
lastPoint = part.endPoint;
|
694
|
+
break;
|
695
|
+
case PathCommandType.QuadraticBezierTo:
|
696
|
+
newParts.push({
|
697
|
+
kind: part.kind,
|
698
|
+
controlPoint: part.controlPoint,
|
699
|
+
endPoint: lastPoint,
|
700
|
+
});
|
701
|
+
lastPoint = part.endPoint;
|
702
|
+
break;
|
703
|
+
default:
|
573
704
|
{
|
574
|
-
|
575
|
-
|
576
|
-
}
|
577
|
-
|
578
|
-
|
705
|
+
const exhaustivenessCheck: never = part;
|
706
|
+
return exhaustivenessCheck;
|
707
|
+
}
|
708
|
+
}
|
709
|
+
}
|
710
|
+
newParts.reverse();
|
711
|
+
return new Path(newStart, newParts);
|
579
712
|
}
|
580
713
|
|
581
714
|
private getEndPoint() {
|
@@ -682,6 +815,57 @@ export class Path {
|
|
682
815
|
return false;
|
683
816
|
}
|
684
817
|
|
818
|
+
/** @returns true if all points on this are equivalent to the points on `other` */
|
819
|
+
public eq(other: Path, tolerance?: number) {
|
820
|
+
if (other.parts.length !== this.parts.length) {
|
821
|
+
return false;
|
822
|
+
}
|
823
|
+
|
824
|
+
for (let i = 0; i < this.parts.length; i++) {
|
825
|
+
const part1 = this.parts[i];
|
826
|
+
const part2 = other.parts[i];
|
827
|
+
|
828
|
+
switch (part1.kind) {
|
829
|
+
case PathCommandType.LineTo:
|
830
|
+
case PathCommandType.MoveTo:
|
831
|
+
if (part1.kind !== part2.kind) {
|
832
|
+
return false;
|
833
|
+
} else if(!part1.point.eq(part2.point, tolerance)) {
|
834
|
+
return false;
|
835
|
+
}
|
836
|
+
break;
|
837
|
+
case PathCommandType.CubicBezierTo:
|
838
|
+
if (part1.kind !== part2.kind) {
|
839
|
+
return false;
|
840
|
+
} else if (
|
841
|
+
!part1.controlPoint1.eq(part2.controlPoint1, tolerance)
|
842
|
+
|| !part1.controlPoint2.eq(part2.controlPoint2, tolerance)
|
843
|
+
|| !part1.endPoint.eq(part2.endPoint, tolerance)
|
844
|
+
) {
|
845
|
+
return false;
|
846
|
+
}
|
847
|
+
break;
|
848
|
+
case PathCommandType.QuadraticBezierTo:
|
849
|
+
if (part1.kind !== part2.kind) {
|
850
|
+
return false;
|
851
|
+
} else if (
|
852
|
+
!part1.controlPoint.eq(part2.controlPoint, tolerance)
|
853
|
+
|| !part1.endPoint.eq(part2.endPoint, tolerance)
|
854
|
+
) {
|
855
|
+
return false;
|
856
|
+
}
|
857
|
+
break;
|
858
|
+
default:
|
859
|
+
{
|
860
|
+
const exhaustivenessCheck: never = part1;
|
861
|
+
return exhaustivenessCheck;
|
862
|
+
}
|
863
|
+
}
|
864
|
+
}
|
865
|
+
|
866
|
+
return true;
|
867
|
+
}
|
868
|
+
|
685
869
|
/**
|
686
870
|
* Returns a path that outlines `rect`.
|
687
871
|
*
|
@@ -1,7 +1,7 @@
|
|
1
|
-
import { Point2 } from '../Vec2';
|
1
|
+
import { Point2, Vec2 } 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
|
/**
|
@@ -9,18 +9,18 @@ import Rect2 from './Rect2';
|
|
9
9
|
*
|
10
10
|
* Access the internal `Point2` using the `p` property.
|
11
11
|
*/
|
12
|
-
class PointShape2D extends
|
12
|
+
class PointShape2D extends Parameterized2DShape {
|
13
13
|
public constructor(public readonly p: Point2) {
|
14
14
|
super();
|
15
15
|
}
|
16
16
|
|
17
17
|
public override signedDistance(point: Vec3): number {
|
18
|
-
return this.p.
|
18
|
+
return this.p.distanceTo(point);
|
19
19
|
}
|
20
20
|
|
21
|
-
public override
|
21
|
+
public override argIntersectsLineSegment(lineSegment: LineSegment2, epsilon?: number): number[] {
|
22
22
|
if (lineSegment.containsPoint(this.p, epsilon)) {
|
23
|
-
return [
|
23
|
+
return [ 0 ];
|
24
24
|
}
|
25
25
|
return [ ];
|
26
26
|
}
|
@@ -28,6 +28,33 @@ class PointShape2D extends Abstract2DShape {
|
|
28
28
|
public override getTightBoundingBox(): Rect2 {
|
29
29
|
return new Rect2(this.p.x, this.p.y, 0, 0);
|
30
30
|
}
|
31
|
+
|
32
|
+
public override at(_t: number) {
|
33
|
+
return this.p;
|
34
|
+
}
|
35
|
+
|
36
|
+
/**
|
37
|
+
* Returns an arbitrary unit-length vector.
|
38
|
+
*/
|
39
|
+
public override normalAt(_t: number) {
|
40
|
+
// Return a vector that makes sense.
|
41
|
+
return Vec2.unitY;
|
42
|
+
}
|
43
|
+
|
44
|
+
public override tangentAt(_t: number): Vec3 {
|
45
|
+
return Vec2.unitX;
|
46
|
+
}
|
47
|
+
|
48
|
+
public override splitAt(_t: number): [PointShape2D] {
|
49
|
+
return [this];
|
50
|
+
}
|
51
|
+
|
52
|
+
public override nearestPointTo(_point: Point2) {
|
53
|
+
return {
|
54
|
+
point: this.p,
|
55
|
+
parameterValue: 0,
|
56
|
+
};
|
57
|
+
}
|
31
58
|
}
|
32
59
|
|
33
60
|
export default PointShape2D;
|
@@ -2,13 +2,12 @@ import { Vec2 } from '../Vec2';
|
|
2
2
|
import QuadraticBezier from './QuadraticBezier';
|
3
3
|
|
4
4
|
describe('QuadraticBezier', () => {
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
];
|
5
|
+
test.each([
|
6
|
+
new QuadraticBezier(Vec2.zero, Vec2.of(10, 0), Vec2.of(20, 0)),
|
7
|
+
new QuadraticBezier(Vec2.of(-10, 0), Vec2.of(2, 10), Vec2.of(20, 0)),
|
8
|
+
new QuadraticBezier(Vec2.of(0, 0), Vec2.of(4, -10), Vec2.of(20, 60)),
|
9
|
+
new QuadraticBezier(Vec2.of(0, 0), Vec2.of(4, -10), Vec2.of(-20, 60)),
|
10
|
+
])('approxmiateDistance should approximately return the distance to the curve (%s)', (curve) => {
|
12
11
|
const testPoints = [
|
13
12
|
Vec2.of(1, 1),
|
14
13
|
Vec2.of(-1, 1),
|
@@ -18,13 +17,50 @@ describe('QuadraticBezier', () => {
|
|
18
17
|
Vec2.of(5, 0),
|
19
18
|
];
|
20
19
|
|
20
|
+
for (const point of testPoints) {
|
21
|
+
const actualDist = curve.distance(point);
|
22
|
+
const approxDist = curve.approximateDistance(point);
|
23
|
+
|
24
|
+
expect(approxDist).toBeGreaterThan(actualDist * 0.6 - 0.25);
|
25
|
+
expect(approxDist).toBeLessThan(actualDist * 1.5 + 2.6);
|
26
|
+
}
|
27
|
+
});
|
28
|
+
|
29
|
+
test.each([
|
30
|
+
[ new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY), Vec2.zero, 0 ],
|
31
|
+
[ new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY), Vec2.unitY, 1 ],
|
32
|
+
|
33
|
+
[ new QuadraticBezier(Vec2.zero, Vec2.of(0.5, 0), Vec2.of(1, 0)), Vec2.of(0.4, 0), 0.4],
|
34
|
+
[ new QuadraticBezier(Vec2.zero, Vec2.of(0, 0.5), Vec2.of(0, 1)), Vec2.of(0, 0.4), 0.4],
|
35
|
+
[ new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY), Vec2.unitX, 0.42514 ],
|
36
|
+
|
37
|
+
// Should not return an out-of-range parameter
|
38
|
+
[ new QuadraticBezier(Vec2.zero, Vec2.of(0, 0.5), Vec2.unitY), Vec2.of(0, -1000), 0 ],
|
39
|
+
[ new QuadraticBezier(Vec2.zero, Vec2.of(0, 0.5), Vec2.unitY), Vec2.of(0, 1000), 1 ],
|
40
|
+
])('nearestPointTo should return the nearest point and parameter value on %s to %s', (bezier, point, expectedParameter) => {
|
41
|
+
const nearest = bezier.nearestPointTo(point);
|
42
|
+
expect(nearest.parameterValue).toBeCloseTo(expectedParameter, 0.0001);
|
43
|
+
expect(nearest.point).objEq(bezier.at(nearest.parameterValue));
|
44
|
+
});
|
45
|
+
|
46
|
+
test('.normalAt should return a unit normal vector at the given parameter value', () => {
|
47
|
+
const curves = [
|
48
|
+
new QuadraticBezier(Vec2.zero, Vec2.unitY, Vec2.unitY.times(2)),
|
49
|
+
new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY),
|
50
|
+
new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY.times(-2)),
|
51
|
+
new QuadraticBezier(Vec2.of(2, 3), Vec2.of(4, 5.1), Vec2.of(6, 7)),
|
52
|
+
new QuadraticBezier(Vec2.of(2, 3), Vec2.of(100, 1000), Vec2.unitY.times(-2)),
|
53
|
+
];
|
54
|
+
|
21
55
|
for (const curve of curves) {
|
22
|
-
for (
|
23
|
-
const
|
24
|
-
|
56
|
+
for (let t = 0; t < 1; t += 0.1) {
|
57
|
+
const normal = curve.normalAt(t);
|
58
|
+
expect(normal.length()).toBe(1);
|
59
|
+
|
60
|
+
const tangentApprox = curve.at(t + 0.001).minus(curve.at(t - 0.001));
|
25
61
|
|
26
|
-
|
27
|
-
expect(
|
62
|
+
// The tangent vector should be perpindicular to the normal
|
63
|
+
expect(tangentApprox.dot(normal)).toBeCloseTo(0);
|
28
64
|
}
|
29
65
|
}
|
30
66
|
});
|
@@ -30,10 +30,19 @@ export class QuadraticBezier extends BezierJSWrapper {
|
|
30
30
|
return -2 * p0 + 2 * p1 + 2 * t * (p0 - 2 * p1 + p2);
|
31
31
|
}
|
32
32
|
|
33
|
+
private static secondDerivativeComponentAt(t: number, p0: number, p1: number, p2: number) {
|
34
|
+
return 2 * (p0 - 2 * p1 + p2);
|
35
|
+
}
|
36
|
+
|
33
37
|
/**
|
34
38
|
* @returns the curve evaluated at `t`.
|
39
|
+
*
|
40
|
+
* `t` should be a number in `[0, 1]`.
|
35
41
|
*/
|
36
42
|
public override at(t: number): Point2 {
|
43
|
+
if (t === 0) return this.p0;
|
44
|
+
if (t === 1) return this.p2;
|
45
|
+
|
37
46
|
const p0 = this.p0;
|
38
47
|
const p1 = this.p1;
|
39
48
|
const p2 = this.p2;
|
@@ -53,6 +62,16 @@ export class QuadraticBezier extends BezierJSWrapper {
|
|
53
62
|
);
|
54
63
|
}
|
55
64
|
|
65
|
+
public override secondDerivativeAt(t: number): Point2 {
|
66
|
+
const p0 = this.p0;
|
67
|
+
const p1 = this.p1;
|
68
|
+
const p2 = this.p2;
|
69
|
+
return Vec2.of(
|
70
|
+
QuadraticBezier.secondDerivativeComponentAt(t, p0.x, p1.x, p2.x),
|
71
|
+
QuadraticBezier.secondDerivativeComponentAt(t, p0.y, p1.y, p2.y),
|
72
|
+
);
|
73
|
+
}
|
74
|
+
|
56
75
|
public override normal(t: number): Vec2 {
|
57
76
|
const tangent = this.derivativeAt(t);
|
58
77
|
return tangent.orthog().normalized();
|
@@ -126,11 +145,10 @@ export class QuadraticBezier extends BezierJSWrapper {
|
|
126
145
|
|
127
146
|
const at1 = this.at(min1);
|
128
147
|
const at2 = this.at(min2);
|
129
|
-
const sqrDist1 = at1.
|
130
|
-
const sqrDist2 = at2.
|
131
|
-
const sqrDist3 = this.at(0).
|
132
|
-
const sqrDist4 = this.at(1).
|
133
|
-
|
148
|
+
const sqrDist1 = at1.squareDistanceTo(point);
|
149
|
+
const sqrDist2 = at2.squareDistanceTo(point);
|
150
|
+
const sqrDist3 = this.at(0).squareDistanceTo(point);
|
151
|
+
const sqrDist4 = this.at(1).squareDistanceTo(point);
|
134
152
|
|
135
153
|
return Math.sqrt(Math.min(sqrDist1, sqrDist2, sqrDist3, sqrDist4));
|
136
154
|
}
|
package/src/shapes/Rect2.ts
CHANGED
@@ -67,6 +67,9 @@ export class Rect2 extends Abstract2DShape {
|
|
67
67
|
&& this.y + this.h >= other.y + other.h;
|
68
68
|
}
|
69
69
|
|
70
|
+
/**
|
71
|
+
* @returns true iff this and `other` overlap
|
72
|
+
*/
|
70
73
|
public intersects(other: Rect2): boolean {
|
71
74
|
// Project along x/y axes.
|
72
75
|
const thisMinX = this.x;
|
@@ -181,7 +184,7 @@ export class Rect2 extends Abstract2DShape {
|
|
181
184
|
let closest: Point2|null = null;
|
182
185
|
let closestDist: number|null = null;
|
183
186
|
for (const point of closestEdgePoints) {
|
184
|
-
const dist = point.
|
187
|
+
const dist = point.distanceTo(target);
|
185
188
|
if (closestDist === null || dist < closestDist) {
|
186
189
|
closest = point;
|
187
190
|
closestDist = dist;
|