@js-draw/math 1.21.3 → 1.23.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.
- package/build-config.json +1 -1
- package/dist/cjs/Color4.d.ts +24 -1
- package/dist/cjs/Color4.js +35 -3
- package/dist/cjs/Mat33.d.ts +21 -11
- package/dist/cjs/Mat33.js +28 -24
- package/dist/cjs/Vec3.d.ts +12 -3
- package/dist/cjs/Vec3.js +20 -9
- package/dist/cjs/lib.d.ts +3 -0
- package/dist/cjs/lib.js +3 -0
- package/dist/cjs/shapes/BezierJSWrapper.d.ts +2 -0
- package/dist/cjs/shapes/BezierJSWrapper.js +22 -13
- package/dist/cjs/shapes/LineSegment2.js +13 -17
- package/dist/cjs/shapes/Parameterized2DShape.js +1 -1
- package/dist/cjs/shapes/Path.d.ts +1 -0
- package/dist/cjs/shapes/Path.js +50 -47
- package/dist/cjs/shapes/QuadraticBezier.d.ts +19 -2
- package/dist/cjs/shapes/QuadraticBezier.js +26 -3
- package/dist/cjs/shapes/Rect2.d.ts +13 -0
- package/dist/cjs/shapes/Rect2.js +35 -16
- package/dist/cjs/shapes/Triangle.js +4 -5
- package/dist/cjs/utils/convexHull2Of.js +3 -3
- package/dist/mjs/Color4.d.ts +24 -1
- package/dist/mjs/Color4.mjs +35 -3
- package/dist/mjs/Mat33.d.ts +21 -11
- package/dist/mjs/Mat33.mjs +28 -24
- package/dist/mjs/Vec3.d.ts +12 -3
- package/dist/mjs/Vec3.mjs +20 -9
- package/dist/mjs/lib.d.ts +3 -0
- package/dist/mjs/lib.mjs +3 -0
- package/dist/mjs/shapes/BezierJSWrapper.d.ts +2 -0
- package/dist/mjs/shapes/BezierJSWrapper.mjs +22 -13
- package/dist/mjs/shapes/LineSegment2.mjs +13 -17
- package/dist/mjs/shapes/Parameterized2DShape.mjs +1 -1
- package/dist/mjs/shapes/Path.d.ts +1 -0
- package/dist/mjs/shapes/Path.mjs +50 -47
- package/dist/mjs/shapes/QuadraticBezier.d.ts +19 -2
- package/dist/mjs/shapes/QuadraticBezier.mjs +26 -3
- package/dist/mjs/shapes/Rect2.d.ts +13 -0
- package/dist/mjs/shapes/Rect2.mjs +35 -16
- package/dist/mjs/shapes/Triangle.mjs +4 -5
- package/dist/mjs/utils/convexHull2Of.mjs +3 -3
- package/dist-test/test_imports/test-require.cjs +1 -1
- package/package.json +3 -3
- package/src/Color4.test.ts +21 -21
- package/src/Color4.ts +61 -18
- package/src/Mat33.fromCSSMatrix.test.ts +32 -46
- package/src/Mat33.test.ts +64 -102
- package/src/Mat33.ts +81 -104
- package/src/Vec2.test.ts +3 -3
- package/src/Vec3.test.ts +2 -3
- package/src/Vec3.ts +46 -61
- package/src/lib.ts +3 -2
- package/src/polynomial/solveQuadratic.test.ts +39 -13
- package/src/polynomial/solveQuadratic.ts +5 -6
- package/src/rounding/cleanUpNumber.test.ts +1 -1
- package/src/rounding/constants.ts +1 -3
- package/src/rounding/getLenAfterDecimal.ts +1 -2
- package/src/rounding/lib.ts +1 -2
- package/src/rounding/toRoundedString.test.ts +1 -1
- package/src/rounding/toStringOfSamePrecision.test.ts +1 -2
- package/src/rounding/toStringOfSamePrecision.ts +1 -1
- package/src/shapes/BezierJSWrapper.ts +56 -37
- package/src/shapes/CubicBezier.ts +3 -3
- package/src/shapes/LineSegment2.test.ts +24 -17
- package/src/shapes/LineSegment2.ts +26 -29
- package/src/shapes/Parameterized2DShape.ts +5 -4
- package/src/shapes/Path.fromString.test.ts +5 -5
- package/src/shapes/Path.test.ts +122 -120
- package/src/shapes/Path.toString.test.ts +7 -7
- package/src/shapes/Path.ts +379 -352
- package/src/shapes/PointShape2D.ts +3 -3
- package/src/shapes/QuadraticBezier.test.ts +27 -21
- package/src/shapes/QuadraticBezier.ts +26 -11
- package/src/shapes/Rect2.test.ts +44 -75
- package/src/shapes/Rect2.ts +47 -35
- package/src/shapes/Triangle.test.ts +31 -29
- package/src/shapes/Triangle.ts +17 -18
- package/src/utils/convexHull2Of.test.ts +54 -15
- package/src/utils/convexHull2Of.ts +9 -7
- package/tsconfig.json +1 -3
- package/typedoc.json +2 -2
@@ -20,9 +20,9 @@ class PointShape2D extends Parameterized2DShape {
|
|
20
20
|
|
21
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
|
-
return [
|
25
|
+
return [];
|
26
26
|
}
|
27
27
|
|
28
28
|
public override getTightBoundingBox(): Rect2 {
|
@@ -57,4 +57,4 @@ class PointShape2D extends Parameterized2DShape {
|
|
57
57
|
}
|
58
58
|
}
|
59
59
|
|
60
|
-
export default PointShape2D;
|
60
|
+
export default PointShape2D;
|
@@ -27,24 +27,27 @@ describe('QuadraticBezier', () => {
|
|
27
27
|
});
|
28
28
|
|
29
29
|
test.each([
|
30
|
-
[
|
31
|
-
[
|
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
32
|
|
33
|
-
[
|
34
|
-
[
|
35
|
-
[
|
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
36
|
|
37
37
|
// Should not return an out-of-range parameter
|
38
|
-
[
|
39
|
-
[
|
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
40
|
|
41
41
|
// Edge case -- just a point
|
42
|
-
[
|
43
|
-
])(
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
42
|
+
[new QuadraticBezier(Vec2.zero, Vec2.zero, Vec2.zero), Vec2.of(0, 1000), 0],
|
43
|
+
])(
|
44
|
+
'nearestPointTo should return the nearest point and parameter value on %s to %s',
|
45
|
+
(bezier, point, expectedParameter) => {
|
46
|
+
const nearest = bezier.nearestPointTo(point);
|
47
|
+
expect(nearest.parameterValue).toBeCloseTo(expectedParameter, 0.0001);
|
48
|
+
expect(nearest.point).objEq(bezier.at(nearest.parameterValue));
|
49
|
+
},
|
50
|
+
);
|
48
51
|
|
49
52
|
test('.normalAt should return a unit normal vector at the given parameter value', () => {
|
50
53
|
const curves = [
|
@@ -72,17 +75,20 @@ describe('QuadraticBezier', () => {
|
|
72
75
|
new QuadraticBezier(Vec2.zero, Vec2.unitY, Vec2.unitY.times(2)),
|
73
76
|
new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY),
|
74
77
|
new QuadraticBezier(Vec2.zero, Vec2.unitY, Vec2.unitX),
|
75
|
-
])(
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
78
|
+
])(
|
79
|
+
'.derivativeAt should return a derivative vector with the correct direction (curve: %s)',
|
80
|
+
(curve) => {
|
81
|
+
for (let t = 0; t < 1; t += 0.1) {
|
82
|
+
const derivative = curve.derivativeAt(t);
|
83
|
+
const derivativeApprox = curve.at(t + 0.001).minus(curve.at(t - 0.001));
|
84
|
+
expect(derivativeApprox.normalized()).objEq(derivative.normalized(), 0.01);
|
85
|
+
}
|
86
|
+
},
|
87
|
+
);
|
82
88
|
|
83
89
|
test('should support Bezier-Bezier intersections', () => {
|
84
90
|
const b1 = new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY);
|
85
91
|
const b2 = new QuadraticBezier(Vec2.of(-1, 0.5), Vec2.of(0, 0.6), Vec2.of(1, 0.4));
|
86
92
|
expect(b1.intersectsBezier(b2)).toHaveLength(1);
|
87
93
|
});
|
88
|
-
});
|
94
|
+
});
|
@@ -4,15 +4,35 @@ import BezierJSWrapper from './BezierJSWrapper';
|
|
4
4
|
import Rect2 from './Rect2';
|
5
5
|
|
6
6
|
/**
|
7
|
-
* Represents a 2D Bézier curve.
|
7
|
+
* Represents a 2D [Bézier curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve).
|
8
8
|
*
|
9
|
-
*
|
9
|
+
* Example:
|
10
|
+
* ```ts,runnable,console
|
11
|
+
* import { QuadraticBezier, Vec2 } from '@js-draw/math';
|
12
|
+
*
|
13
|
+
* const startPoint = Vec2.of(4, 3);
|
14
|
+
* const controlPoint = Vec2.of(1, 1);
|
15
|
+
* const endPoint = Vec2.of(1, 3);
|
16
|
+
*
|
17
|
+
* const curve = new QuadraticBezier(
|
18
|
+
* startPoint,
|
19
|
+
* controlPoint,
|
20
|
+
* endPoint,
|
21
|
+
* );
|
22
|
+
*
|
23
|
+
* console.log('Curve:', curve);
|
24
|
+
* ```
|
25
|
+
*
|
26
|
+
* **Note**: Some Bézier operations internally use the `bezier-js` library.
|
10
27
|
*/
|
11
28
|
export class QuadraticBezier extends BezierJSWrapper {
|
12
29
|
public constructor(
|
30
|
+
// Start point
|
13
31
|
public readonly p0: Point2,
|
32
|
+
// Control point
|
14
33
|
public readonly p1: Point2,
|
15
|
-
|
34
|
+
// End point
|
35
|
+
public readonly p2: Point2,
|
16
36
|
) {
|
17
37
|
super();
|
18
38
|
}
|
@@ -78,7 +98,7 @@ export class QuadraticBezier extends BezierJSWrapper {
|
|
78
98
|
|
79
99
|
/** @returns an overestimate of this shape's bounding box. */
|
80
100
|
public override getLooseBoundingBox(): Rect2 {
|
81
|
-
return Rect2.bboxOf([
|
101
|
+
return Rect2.bboxOf([this.p0, this.p1, this.p2]);
|
82
102
|
}
|
83
103
|
|
84
104
|
/**
|
@@ -124,14 +144,9 @@ export class QuadraticBezier extends BezierJSWrapper {
|
|
124
144
|
const f2ndDerivAtZero = b;
|
125
145
|
const f3rdDerivAtZero = 2 * c;
|
126
146
|
|
127
|
-
|
128
147
|
// Using the first few terms of a Maclaurin series to approximate f'(t),
|
129
148
|
// f'(t) ≈ f'(0) + t f''(0) + t² f'''(0) / 2
|
130
|
-
let [
|
131
|
-
f3rdDerivAtZero / 2,
|
132
|
-
f2ndDerivAtZero,
|
133
|
-
fDerivAtZero,
|
134
|
-
);
|
149
|
+
let [min1, min2] = solveQuadratic(f3rdDerivAtZero / 2, f2ndDerivAtZero, fDerivAtZero);
|
135
150
|
|
136
151
|
// If the quadratic has no solutions, approximate.
|
137
152
|
if (isNaN(min1)) {
|
@@ -153,7 +168,7 @@ export class QuadraticBezier extends BezierJSWrapper {
|
|
153
168
|
}
|
154
169
|
|
155
170
|
public override getPoints() {
|
156
|
-
return [
|
171
|
+
return [this.p0, this.p1, this.p2];
|
157
172
|
}
|
158
173
|
}
|
159
174
|
export default QuadraticBezier;
|
package/src/shapes/Rect2.test.ts
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
import Rect2 from './Rect2';
|
3
2
|
import { Vec2 } from '../Vec2';
|
4
3
|
import Mat33 from '../Mat33';
|
@@ -7,42 +6,21 @@ describe('Rect2', () => {
|
|
7
6
|
it('width, height should always be positive', () => {
|
8
7
|
expect(new Rect2(-1, -2, -3, 4)).objEq(new Rect2(-4, -2, 3, 4));
|
9
8
|
expect(new Rect2(0, 0, 0, 0).size).objEq(Vec2.zero);
|
10
|
-
expect(Rect2.fromCorners(
|
11
|
-
Vec2.of(-3, -3),
|
12
|
-
Vec2.of(-1, -1)
|
13
|
-
)).objEq(new Rect2(
|
14
|
-
-3, -3,
|
15
|
-
2, 2
|
16
|
-
));
|
9
|
+
expect(Rect2.fromCorners(Vec2.of(-3, -3), Vec2.of(-1, -1))).objEq(new Rect2(-3, -3, 2, 2));
|
17
10
|
});
|
18
11
|
|
19
12
|
it('bounding boxes should be correctly computed', () => {
|
20
|
-
expect(Rect2.bboxOf([
|
21
|
-
|
22
|
-
])).objEq(
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
Vec2.of(3, 4),
|
28
|
-
Vec2.of(1, -4),
|
29
|
-
])).objEq(new Rect2(
|
30
|
-
-1, -4,
|
31
|
-
4, 8
|
32
|
-
));
|
33
|
-
|
34
|
-
expect(Rect2.bboxOf([
|
35
|
-
Vec2.zero,
|
36
|
-
], 10)).objEq(new Rect2(
|
37
|
-
-10, -10,
|
38
|
-
20, 20
|
39
|
-
));
|
13
|
+
expect(Rect2.bboxOf([Vec2.zero])).objEq(Rect2.empty);
|
14
|
+
|
15
|
+
expect(Rect2.bboxOf([Vec2.of(-1, -1), Vec2.of(1, 2), Vec2.of(3, 4), Vec2.of(1, -4)])).objEq(
|
16
|
+
new Rect2(-1, -4, 4, 8),
|
17
|
+
);
|
18
|
+
|
19
|
+
expect(Rect2.bboxOf([Vec2.zero], 10)).objEq(new Rect2(-10, -10, 20, 20));
|
40
20
|
});
|
41
21
|
|
42
22
|
it('"union"s should contain both composite rectangles.', () => {
|
43
|
-
expect(new Rect2(0, 0, 1, 1).union(new Rect2(1, 1, 2, 2))).objEq(
|
44
|
-
new Rect2(0, 0, 3, 3)
|
45
|
-
);
|
23
|
+
expect(new Rect2(0, 0, 1, 1).union(new Rect2(1, 1, 2, 2))).objEq(new Rect2(0, 0, 3, 3));
|
46
24
|
expect(Rect2.empty.union(Rect2.empty)).objEq(Rect2.empty);
|
47
25
|
});
|
48
26
|
|
@@ -51,21 +29,15 @@ describe('Rect2', () => {
|
|
51
29
|
});
|
52
30
|
|
53
31
|
it('should correctly union multiple rectangles', () => {
|
54
|
-
expect(Rect2.union(new Rect2(0, 0, 1, 1), new Rect2(1, 1, 2, 2))).objEq(
|
55
|
-
new Rect2(0, 0, 3, 3)
|
56
|
-
);
|
32
|
+
expect(Rect2.union(new Rect2(0, 0, 1, 1), new Rect2(1, 1, 2, 2))).objEq(new Rect2(0, 0, 3, 3));
|
57
33
|
|
58
34
|
expect(
|
59
|
-
Rect2.union(new Rect2(-1, 0, 1, 1), new Rect2(1, 1, 2, 2), new Rect2(1, 10, 1, 0.1))
|
60
|
-
).objEq(
|
61
|
-
new Rect2(-1, 0, 4, 10.1)
|
62
|
-
);
|
35
|
+
Rect2.union(new Rect2(-1, 0, 1, 1), new Rect2(1, 1, 2, 2), new Rect2(1, 10, 1, 0.1)),
|
36
|
+
).objEq(new Rect2(-1, 0, 4, 10.1));
|
63
37
|
|
64
38
|
expect(
|
65
|
-
Rect2.union(new Rect2(-1, 0, 1, 1), new Rect2(1, -11.1, 2, 2), new Rect2(1, 10, 1, 0.1))
|
66
|
-
).objEq(
|
67
|
-
new Rect2(-1, -11.1, 4, 21.2)
|
68
|
-
);
|
39
|
+
Rect2.union(new Rect2(-1, 0, 1, 1), new Rect2(1, -11.1, 2, 2), new Rect2(1, 10, 1, 0.1)),
|
40
|
+
).objEq(new Rect2(-1, -11.1, 4, 21.2));
|
69
41
|
});
|
70
42
|
|
71
43
|
it('should contain points that are within a rectangle', () => {
|
@@ -112,17 +84,15 @@ describe('Rect2', () => {
|
|
112
84
|
});
|
113
85
|
|
114
86
|
it('should correctly compute the intersection of one rectangle and several others', () => {
|
115
|
-
const mainRect = new Rect2(334,156,333,179);
|
87
|
+
const mainRect = new Rect2(334, 156, 333, 179);
|
116
88
|
const shouldIntersect = [
|
117
89
|
new Rect2(400.8, 134.8, 8.4, 161.4),
|
118
|
-
new Rect2(324.8,93,164.4,75.2),
|
119
|
-
new Rect2(435.8,146.8,213.2,192.6),
|
120
|
-
new Rect2(550.8,211.8,3.4,3.4),
|
121
|
-
new Rect2(478.8,93.8,212.4,95.4),
|
122
|
-
];
|
123
|
-
const shouldNotIntersect = [
|
124
|
-
new Rect2(200, 200, 1, 1),
|
90
|
+
new Rect2(324.8, 93, 164.4, 75.2),
|
91
|
+
new Rect2(435.8, 146.8, 213.2, 192.6),
|
92
|
+
new Rect2(550.8, 211.8, 3.4, 3.4),
|
93
|
+
new Rect2(478.8, 93.8, 212.4, 95.4),
|
125
94
|
];
|
95
|
+
const shouldNotIntersect = [new Rect2(200, 200, 1, 1)];
|
126
96
|
|
127
97
|
for (const rect of shouldIntersect) {
|
128
98
|
expect(mainRect.intersects(rect)).toBe(true);
|
@@ -135,10 +105,10 @@ describe('Rect2', () => {
|
|
135
105
|
it('intersecting rectangles should have their intersections correctly computed', () => {
|
136
106
|
expect(new Rect2(-1, -1, 2, 2).intersection(Rect2.empty)).objEq(Rect2.empty);
|
137
107
|
expect(new Rect2(-1, -1, 2, 2).intersection(new Rect2(0, 0, 3, 3))).objEq(
|
138
|
-
new Rect2(0, 0, 1, 1)
|
108
|
+
new Rect2(0, 0, 1, 1),
|
139
109
|
);
|
140
110
|
expect(new Rect2(-2, 0, 1, 2).intersection(new Rect2(-3, 0, 2, 2))).objEq(
|
141
|
-
new Rect2(-2, 0, 1, 2)
|
111
|
+
new Rect2(-2, 0, 1, 2),
|
142
112
|
);
|
143
113
|
expect(new Rect2(-1, -1, 2, 2).intersection(new Rect2(3, 3, 10, 10))).toBe(null);
|
144
114
|
});
|
@@ -192,39 +162,38 @@ describe('Rect2', () => {
|
|
192
162
|
|
193
163
|
describe('divideIntoGrid', () => {
|
194
164
|
it('division of unit square', () => {
|
195
|
-
expect(Rect2.unitSquare.divideIntoGrid(2, 2)).toMatchObject(
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
);
|
165
|
+
expect(Rect2.unitSquare.divideIntoGrid(2, 2)).toMatchObject([
|
166
|
+
new Rect2(0, 0, 0.5, 0.5),
|
167
|
+
new Rect2(0.5, 0, 0.5, 0.5),
|
168
|
+
new Rect2(0, 0.5, 0.5, 0.5),
|
169
|
+
new Rect2(0.5, 0.5, 0.5, 0.5),
|
170
|
+
]);
|
201
171
|
expect(Rect2.unitSquare.divideIntoGrid(0, 0).length).toBe(0);
|
202
172
|
expect(Rect2.unitSquare.divideIntoGrid(100, 0).length).toBe(0);
|
203
|
-
expect(Rect2.unitSquare.divideIntoGrid(4, 1)).toMatchObject(
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
);
|
173
|
+
expect(Rect2.unitSquare.divideIntoGrid(4, 1)).toMatchObject([
|
174
|
+
new Rect2(0, 0, 0.25, 1),
|
175
|
+
new Rect2(0.25, 0, 0.25, 1),
|
176
|
+
new Rect2(0.5, 0, 0.25, 1),
|
177
|
+
new Rect2(0.75, 0, 0.25, 1),
|
178
|
+
]);
|
209
179
|
});
|
210
180
|
it('division of translated square', () => {
|
211
|
-
expect(new Rect2(3, -3, 4, 4).divideIntoGrid(2, 1)).toMatchObject(
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
);
|
181
|
+
expect(new Rect2(3, -3, 4, 4).divideIntoGrid(2, 1)).toMatchObject([
|
182
|
+
new Rect2(3, -3, 2, 4),
|
183
|
+
new Rect2(5, -3, 2, 4),
|
184
|
+
]);
|
216
185
|
});
|
217
186
|
it('division of empty square', () => {
|
218
187
|
expect(Rect2.empty.divideIntoGrid(1000, 10000).length).toBe(1);
|
219
188
|
});
|
220
189
|
|
221
190
|
it('division of rectangle', () => {
|
222
|
-
expect(new Rect2(0, 0, 2, 1).divideIntoGrid(2, 2)).toMatchObject(
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
);
|
191
|
+
expect(new Rect2(0, 0, 2, 1).divideIntoGrid(2, 2)).toMatchObject([
|
192
|
+
new Rect2(0, 0, 1, 0.5),
|
193
|
+
new Rect2(1, 0, 1, 0.5),
|
194
|
+
new Rect2(0, 0.5, 1, 0.5),
|
195
|
+
new Rect2(1, 0.5, 1, 0.5),
|
196
|
+
]);
|
228
197
|
});
|
229
198
|
});
|
230
199
|
|
package/src/shapes/Rect2.ts
CHANGED
@@ -17,6 +17,18 @@ export interface RectTemplate {
|
|
17
17
|
/**
|
18
18
|
* Represents a rectangle in 2D space, parallel to the XY axes.
|
19
19
|
*
|
20
|
+
* **Example**:
|
21
|
+
* ```ts,runnable,console
|
22
|
+
* import { Rect2, Vec2 } from '@js-draw/math';
|
23
|
+
*
|
24
|
+
* const rect = Rect2.fromCorners(
|
25
|
+
* Vec2.of(0, 0),
|
26
|
+
* Vec2.of(10, 10),
|
27
|
+
* );
|
28
|
+
* console.log('area', rect.area);
|
29
|
+
* console.log('topLeft', rect.topLeft);
|
30
|
+
* ```
|
31
|
+
*
|
20
32
|
* `invariant: w ≥ 0, h ≥ 0, immutable`
|
21
33
|
*/
|
22
34
|
export class Rect2 extends Abstract2DShape {
|
@@ -28,10 +40,14 @@ export class Rect2 extends Abstract2DShape {
|
|
28
40
|
public readonly area: number;
|
29
41
|
|
30
42
|
public constructor(
|
43
|
+
// Top left x coordinate
|
31
44
|
public readonly x: number,
|
45
|
+
// Top left y coordinate
|
32
46
|
public readonly y: number,
|
47
|
+
// Width
|
33
48
|
public readonly w: number,
|
34
|
-
|
49
|
+
// Height
|
50
|
+
public readonly h: number,
|
35
51
|
) {
|
36
52
|
super();
|
37
53
|
|
@@ -61,14 +77,22 @@ export class Rect2 extends Abstract2DShape {
|
|
61
77
|
}
|
62
78
|
|
63
79
|
public override containsPoint(other: Point2): boolean {
|
64
|
-
return
|
65
|
-
|
80
|
+
return (
|
81
|
+
this.x <= other.x &&
|
82
|
+
this.y <= other.y &&
|
83
|
+
this.x + this.w >= other.x &&
|
84
|
+
this.y + this.h >= other.y
|
85
|
+
);
|
66
86
|
}
|
67
87
|
|
88
|
+
/** @returns true iff `other` is completely within this `Rect2`. */
|
68
89
|
public containsRect(other: Rect2): boolean {
|
69
|
-
return
|
70
|
-
|
71
|
-
|
90
|
+
return (
|
91
|
+
this.x <= other.x &&
|
92
|
+
this.y <= other.y &&
|
93
|
+
this.x + this.w >= other.x + other.w &&
|
94
|
+
this.y + this.h >= other.y + other.h
|
95
|
+
);
|
72
96
|
}
|
73
97
|
|
74
98
|
/**
|
@@ -85,7 +109,6 @@ export class Rect2 extends Abstract2DShape {
|
|
85
109
|
return false;
|
86
110
|
}
|
87
111
|
|
88
|
-
|
89
112
|
const thisMinY = this.y;
|
90
113
|
const thisMaxY = thisMinY + this.h;
|
91
114
|
const otherMinY = other.y;
|
@@ -100,7 +123,7 @@ export class Rect2 extends Abstract2DShape {
|
|
100
123
|
|
101
124
|
// Returns the overlap of this and [other], or null, if no such
|
102
125
|
// overlap exists
|
103
|
-
public intersection(other: Rect2): Rect2|null {
|
126
|
+
public intersection(other: Rect2): Rect2 | null {
|
104
127
|
if (!this.intersects(other)) {
|
105
128
|
return null;
|
106
129
|
}
|
@@ -151,10 +174,7 @@ export class Rect2 extends Abstract2DShape {
|
|
151
174
|
// [margin] is the minimum distance between the new point and the edge
|
152
175
|
// of the resultant rectangle.
|
153
176
|
public grownToPoint(point: Point2, margin: number = 0): Rect2 {
|
154
|
-
const otherRect = new Rect2(
|
155
|
-
point.x - margin, point.y - margin,
|
156
|
-
margin * 2, margin * 2
|
157
|
-
);
|
177
|
+
const otherRect = new Rect2(point.x - margin, point.y - margin, margin * 2, margin * 2);
|
158
178
|
return this.union(otherRect);
|
159
179
|
}
|
160
180
|
|
@@ -170,23 +190,23 @@ export class Rect2 extends Abstract2DShape {
|
|
170
190
|
const yMargin = -Math.min(-margin, this.h / 2);
|
171
191
|
|
172
192
|
return new Rect2(
|
173
|
-
this.x - xMargin,
|
174
|
-
this.
|
193
|
+
this.x - xMargin,
|
194
|
+
this.y - yMargin,
|
195
|
+
this.w + xMargin * 2,
|
196
|
+
this.h + yMargin * 2,
|
175
197
|
);
|
176
198
|
}
|
177
199
|
|
178
|
-
return new Rect2(
|
179
|
-
this.x - margin, this.y - margin, this.w + margin * 2, this.h + margin * 2
|
180
|
-
);
|
200
|
+
return new Rect2(this.x - margin, this.y - margin, this.w + margin * 2, this.h + margin * 2);
|
181
201
|
}
|
182
202
|
|
183
203
|
public getClosestPointOnBoundaryTo(target: Point2) {
|
184
|
-
const closestEdgePoints = this.getEdges().map(edge => {
|
204
|
+
const closestEdgePoints = this.getEdges().map((edge) => {
|
185
205
|
return edge.closestPointTo(target);
|
186
206
|
});
|
187
207
|
|
188
|
-
let closest: Point2|null = null;
|
189
|
-
let closestDist: number|null = null;
|
208
|
+
let closest: Point2 | null = null;
|
209
|
+
let closestDist: number | null = null;
|
190
210
|
for (const point of closestEdgePoints) {
|
191
211
|
const dist = point.distanceTo(target);
|
192
212
|
if (closestDist === null || dist < closestDist) {
|
@@ -211,16 +231,11 @@ export class Rect2 extends Abstract2DShape {
|
|
211
231
|
}
|
212
232
|
|
213
233
|
const squareRadius = radius * radius;
|
214
|
-
return this.corners.every(corner => corner.minus(point).magnitudeSquared() < squareRadius);
|
234
|
+
return this.corners.every((corner) => corner.minus(point).magnitudeSquared() < squareRadius);
|
215
235
|
}
|
216
236
|
|
217
237
|
public get corners(): Point2[] {
|
218
|
-
return [
|
219
|
-
this.bottomRight,
|
220
|
-
this.topRight,
|
221
|
-
this.topLeft,
|
222
|
-
this.bottomLeft,
|
223
|
-
];
|
238
|
+
return [this.bottomRight, this.topRight, this.topLeft, this.bottomLeft];
|
224
239
|
}
|
225
240
|
|
226
241
|
public get maxDimension() {
|
@@ -272,7 +287,7 @@ export class Rect2 extends Abstract2DShape {
|
|
272
287
|
|
273
288
|
for (const edge of this.getEdges()) {
|
274
289
|
const intersection = edge.intersectsLineSegment(lineSegment);
|
275
|
-
intersection.forEach(point => result.push(point));
|
290
|
+
intersection.forEach((point) => result.push(point));
|
276
291
|
}
|
277
292
|
|
278
293
|
return result;
|
@@ -295,7 +310,7 @@ export class Rect2 extends Abstract2DShape {
|
|
295
310
|
// [affineTransform] is a transformation matrix that both scales and **translates**.
|
296
311
|
// the bounding box of this' four corners after transformed by the given affine transformation.
|
297
312
|
public transformedBoundingBox(affineTransform: Mat33): Rect2 {
|
298
|
-
return Rect2.bboxOf(this.corners.map(corner => affineTransform.transformVec2(corner)));
|
313
|
+
return Rect2.bboxOf(this.corners.map((corner) => affineTransform.transformVec2(corner)));
|
299
314
|
}
|
300
315
|
|
301
316
|
/** @return true iff this is equal to `other ± tolerance` */
|
@@ -307,13 +322,12 @@ export class Rect2 extends Abstract2DShape {
|
|
307
322
|
return `Rect(point(${this.x}, ${this.y}), size(${this.w}, ${this.h}))`;
|
308
323
|
}
|
309
324
|
|
310
|
-
|
311
325
|
public static fromCorners(corner1: Point2, corner2: Point2) {
|
312
326
|
return new Rect2(
|
313
327
|
Math.min(corner1.x, corner2.x),
|
314
328
|
Math.min(corner1.y, corner2.y),
|
315
329
|
Math.abs(corner1.x - corner2.x),
|
316
|
-
Math.abs(corner1.y - corner2.y)
|
330
|
+
Math.abs(corner1.y - corner2.y),
|
317
331
|
);
|
318
332
|
}
|
319
333
|
|
@@ -344,7 +358,7 @@ export class Rect2 extends Abstract2DShape {
|
|
344
358
|
|
345
359
|
return Rect2.fromCorners(
|
346
360
|
Vec2.of(minX - margin, minY - margin),
|
347
|
-
Vec2.of(maxX + margin, maxY + margin)
|
361
|
+
Vec2.of(maxX + margin, maxY + margin),
|
348
362
|
);
|
349
363
|
}
|
350
364
|
|
@@ -369,9 +383,7 @@ export class Rect2 extends Abstract2DShape {
|
|
369
383
|
maxY = Math.max(maxY, rect.y + rect.h);
|
370
384
|
}
|
371
385
|
|
372
|
-
return new Rect2(
|
373
|
-
minX, minY, maxX - minX, maxY - minY,
|
374
|
-
);
|
386
|
+
return new Rect2(minX, minY, maxX - minX, maxY - minY);
|
375
387
|
}
|
376
388
|
|
377
389
|
public static of(template: RectTemplate) {
|
@@ -19,43 +19,45 @@ describe('Triangle', () => {
|
|
19
19
|
}
|
20
20
|
});
|
21
21
|
|
22
|
-
it(
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
expect(testTriangle.signedDistance(Vec2.of(0, 2))).toBeCloseTo(1);
|
22
|
+
it(
|
23
|
+
'signed distance function should be the negative distance to the edge ' +
|
24
|
+
'of the triangle on the interior of a shape, same as distance outside of shape',
|
25
|
+
() => {
|
26
|
+
const testTriangle = Triangle.fromVertices(Vec2.of(-1, -1), Vec2.of(0, 1), Vec2.of(1, -1));
|
28
27
|
|
29
|
-
|
30
|
-
|
28
|
+
// A point vertically above the triangle: Outside, so positive SDF
|
29
|
+
expect(testTriangle.signedDistance(Vec2.of(0, 2))).toBeCloseTo(1);
|
31
30
|
|
32
|
-
|
33
|
-
|
31
|
+
// Similarly, a point vertically below the triangle is outside, so should have positive SDF
|
32
|
+
expect(testTriangle.signedDistance(Vec2.of(0, -2))).toBeCloseTo(1);
|
34
33
|
|
34
|
+
// A point just above the left side (and outside the triangle) should also have positive SDF
|
35
|
+
expect(testTriangle.signedDistance(Vec2.of(-0.8, 0.8))).toBeGreaterThan(0);
|
35
36
|
|
36
|
-
|
37
|
-
|
38
|
-
|
37
|
+
const firstSide = testTriangle.getEdges()[0];
|
38
|
+
const firstSideMidpoint = firstSide.at(0.5);
|
39
|
+
const firstSideNormal = firstSide.direction.orthog();
|
39
40
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
41
|
+
// Move a point towards the first side
|
42
|
+
for (let t = 0.5; t > -0.5; t -= 0.1) {
|
43
|
+
const point = firstSideMidpoint.minus(firstSideNormal.times(t));
|
44
|
+
const distFromSide1 = firstSide.distance(point);
|
45
|
+
const signedDist = testTriangle.signedDistance(point);
|
45
46
|
|
46
|
-
// Inside the shape
|
47
|
-
if (t > 0) {
|
48
47
|
// Inside the shape
|
49
|
-
|
48
|
+
if (t > 0) {
|
49
|
+
// Inside the shape
|
50
|
+
expect(testTriangle.containsPoint(point)).toBe(true);
|
50
51
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
52
|
+
expect(signedDist).toBeCloseTo(-distFromSide1);
|
53
|
+
} else {
|
54
|
+
// Outside the shape
|
55
|
+
expect(testTriangle.containsPoint(point)).toBe(false);
|
55
56
|
|
56
|
-
|
57
|
+
expect(signedDist).toBeCloseTo(distFromSide1);
|
58
|
+
}
|
57
59
|
}
|
58
|
-
}
|
59
|
-
|
60
|
+
},
|
61
|
+
);
|
60
62
|
});
|
61
|
-
});
|
63
|
+
});
|