@js-draw/math 1.21.3 → 1.22.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/build-config.json +1 -1
- package/dist/cjs/Color4.js +2 -2
- package/dist/cjs/Mat33.d.ts +1 -11
- package/dist/cjs/Mat33.js +8 -24
- package/dist/cjs/Vec3.js +9 -7
- package/dist/cjs/shapes/BezierJSWrapper.js +20 -13
- package/dist/cjs/shapes/LineSegment2.js +13 -17
- package/dist/cjs/shapes/Parameterized2DShape.js +1 -1
- package/dist/cjs/shapes/Path.js +49 -47
- package/dist/cjs/shapes/Rect2.js +13 -15
- package/dist/cjs/shapes/Triangle.js +4 -5
- package/dist/cjs/utils/convexHull2Of.js +3 -3
- package/dist/mjs/Color4.mjs +2 -2
- package/dist/mjs/Mat33.d.ts +1 -11
- package/dist/mjs/Mat33.mjs +8 -24
- package/dist/mjs/Vec3.mjs +9 -7
- package/dist/mjs/shapes/BezierJSWrapper.mjs +20 -13
- package/dist/mjs/shapes/LineSegment2.mjs +13 -17
- package/dist/mjs/shapes/Parameterized2DShape.mjs +1 -1
- package/dist/mjs/shapes/Path.mjs +49 -47
- package/dist/mjs/shapes/Rect2.mjs +13 -15
- 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 +16 -21
- package/src/Color4.ts +22 -17
- package/src/Mat33.fromCSSMatrix.test.ts +31 -45
- package/src/Mat33.test.ts +58 -96
- package/src/Mat33.ts +61 -104
- package/src/Vec2.test.ts +3 -3
- package/src/Vec3.test.ts +2 -3
- package/src/Vec3.ts +34 -58
- package/src/lib.ts +0 -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 +54 -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 +378 -352
- package/src/shapes/PointShape2D.ts +3 -3
- package/src/shapes/QuadraticBezier.test.ts +27 -21
- package/src/shapes/QuadraticBezier.ts +4 -9
- package/src/shapes/Rect2.test.ts +44 -75
- package/src/shapes/Rect2.ts +30 -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
|
+
});
|
@@ -12,7 +12,7 @@ export class QuadraticBezier extends BezierJSWrapper {
|
|
12
12
|
public constructor(
|
13
13
|
public readonly p0: Point2,
|
14
14
|
public readonly p1: Point2,
|
15
|
-
public readonly p2: Point2
|
15
|
+
public readonly p2: Point2,
|
16
16
|
) {
|
17
17
|
super();
|
18
18
|
}
|
@@ -78,7 +78,7 @@ export class QuadraticBezier extends BezierJSWrapper {
|
|
78
78
|
|
79
79
|
/** @returns an overestimate of this shape's bounding box. */
|
80
80
|
public override getLooseBoundingBox(): Rect2 {
|
81
|
-
return Rect2.bboxOf([
|
81
|
+
return Rect2.bboxOf([this.p0, this.p1, this.p2]);
|
82
82
|
}
|
83
83
|
|
84
84
|
/**
|
@@ -124,14 +124,9 @@ export class QuadraticBezier extends BezierJSWrapper {
|
|
124
124
|
const f2ndDerivAtZero = b;
|
125
125
|
const f3rdDerivAtZero = 2 * c;
|
126
126
|
|
127
|
-
|
128
127
|
// Using the first few terms of a Maclaurin series to approximate f'(t),
|
129
128
|
// f'(t) ≈ f'(0) + t f''(0) + t² f'''(0) / 2
|
130
|
-
let [
|
131
|
-
f3rdDerivAtZero / 2,
|
132
|
-
f2ndDerivAtZero,
|
133
|
-
fDerivAtZero,
|
134
|
-
);
|
129
|
+
let [min1, min2] = solveQuadratic(f3rdDerivAtZero / 2, f2ndDerivAtZero, fDerivAtZero);
|
135
130
|
|
136
131
|
// If the quadratic has no solutions, approximate.
|
137
132
|
if (isNaN(min1)) {
|
@@ -153,7 +148,7 @@ export class QuadraticBezier extends BezierJSWrapper {
|
|
153
148
|
}
|
154
149
|
|
155
150
|
public override getPoints() {
|
156
|
-
return [
|
151
|
+
return [this.p0, this.p1, this.p2];
|
157
152
|
}
|
158
153
|
}
|
159
154
|
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
@@ -31,7 +31,7 @@ export class Rect2 extends Abstract2DShape {
|
|
31
31
|
public readonly x: number,
|
32
32
|
public readonly y: number,
|
33
33
|
public readonly w: number,
|
34
|
-
public readonly h: number
|
34
|
+
public readonly h: number,
|
35
35
|
) {
|
36
36
|
super();
|
37
37
|
|
@@ -61,14 +61,21 @@ export class Rect2 extends Abstract2DShape {
|
|
61
61
|
}
|
62
62
|
|
63
63
|
public override containsPoint(other: Point2): boolean {
|
64
|
-
return
|
65
|
-
|
64
|
+
return (
|
65
|
+
this.x <= other.x &&
|
66
|
+
this.y <= other.y &&
|
67
|
+
this.x + this.w >= other.x &&
|
68
|
+
this.y + this.h >= other.y
|
69
|
+
);
|
66
70
|
}
|
67
71
|
|
68
72
|
public containsRect(other: Rect2): boolean {
|
69
|
-
return
|
70
|
-
|
71
|
-
|
73
|
+
return (
|
74
|
+
this.x <= other.x &&
|
75
|
+
this.y <= other.y &&
|
76
|
+
this.x + this.w >= other.x + other.w &&
|
77
|
+
this.y + this.h >= other.y + other.h
|
78
|
+
);
|
72
79
|
}
|
73
80
|
|
74
81
|
/**
|
@@ -85,7 +92,6 @@ export class Rect2 extends Abstract2DShape {
|
|
85
92
|
return false;
|
86
93
|
}
|
87
94
|
|
88
|
-
|
89
95
|
const thisMinY = this.y;
|
90
96
|
const thisMaxY = thisMinY + this.h;
|
91
97
|
const otherMinY = other.y;
|
@@ -100,7 +106,7 @@ export class Rect2 extends Abstract2DShape {
|
|
100
106
|
|
101
107
|
// Returns the overlap of this and [other], or null, if no such
|
102
108
|
// overlap exists
|
103
|
-
public intersection(other: Rect2): Rect2|null {
|
109
|
+
public intersection(other: Rect2): Rect2 | null {
|
104
110
|
if (!this.intersects(other)) {
|
105
111
|
return null;
|
106
112
|
}
|
@@ -151,10 +157,7 @@ export class Rect2 extends Abstract2DShape {
|
|
151
157
|
// [margin] is the minimum distance between the new point and the edge
|
152
158
|
// of the resultant rectangle.
|
153
159
|
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
|
-
);
|
160
|
+
const otherRect = new Rect2(point.x - margin, point.y - margin, margin * 2, margin * 2);
|
158
161
|
return this.union(otherRect);
|
159
162
|
}
|
160
163
|
|
@@ -170,23 +173,23 @@ export class Rect2 extends Abstract2DShape {
|
|
170
173
|
const yMargin = -Math.min(-margin, this.h / 2);
|
171
174
|
|
172
175
|
return new Rect2(
|
173
|
-
this.x - xMargin,
|
174
|
-
this.
|
176
|
+
this.x - xMargin,
|
177
|
+
this.y - yMargin,
|
178
|
+
this.w + xMargin * 2,
|
179
|
+
this.h + yMargin * 2,
|
175
180
|
);
|
176
181
|
}
|
177
182
|
|
178
|
-
return new Rect2(
|
179
|
-
this.x - margin, this.y - margin, this.w + margin * 2, this.h + margin * 2
|
180
|
-
);
|
183
|
+
return new Rect2(this.x - margin, this.y - margin, this.w + margin * 2, this.h + margin * 2);
|
181
184
|
}
|
182
185
|
|
183
186
|
public getClosestPointOnBoundaryTo(target: Point2) {
|
184
|
-
const closestEdgePoints = this.getEdges().map(edge => {
|
187
|
+
const closestEdgePoints = this.getEdges().map((edge) => {
|
185
188
|
return edge.closestPointTo(target);
|
186
189
|
});
|
187
190
|
|
188
|
-
let closest: Point2|null = null;
|
189
|
-
let closestDist: number|null = null;
|
191
|
+
let closest: Point2 | null = null;
|
192
|
+
let closestDist: number | null = null;
|
190
193
|
for (const point of closestEdgePoints) {
|
191
194
|
const dist = point.distanceTo(target);
|
192
195
|
if (closestDist === null || dist < closestDist) {
|
@@ -211,16 +214,11 @@ export class Rect2 extends Abstract2DShape {
|
|
211
214
|
}
|
212
215
|
|
213
216
|
const squareRadius = radius * radius;
|
214
|
-
return this.corners.every(corner => corner.minus(point).magnitudeSquared() < squareRadius);
|
217
|
+
return this.corners.every((corner) => corner.minus(point).magnitudeSquared() < squareRadius);
|
215
218
|
}
|
216
219
|
|
217
220
|
public get corners(): Point2[] {
|
218
|
-
return [
|
219
|
-
this.bottomRight,
|
220
|
-
this.topRight,
|
221
|
-
this.topLeft,
|
222
|
-
this.bottomLeft,
|
223
|
-
];
|
221
|
+
return [this.bottomRight, this.topRight, this.topLeft, this.bottomLeft];
|
224
222
|
}
|
225
223
|
|
226
224
|
public get maxDimension() {
|
@@ -272,7 +270,7 @@ export class Rect2 extends Abstract2DShape {
|
|
272
270
|
|
273
271
|
for (const edge of this.getEdges()) {
|
274
272
|
const intersection = edge.intersectsLineSegment(lineSegment);
|
275
|
-
intersection.forEach(point => result.push(point));
|
273
|
+
intersection.forEach((point) => result.push(point));
|
276
274
|
}
|
277
275
|
|
278
276
|
return result;
|
@@ -295,7 +293,7 @@ export class Rect2 extends Abstract2DShape {
|
|
295
293
|
// [affineTransform] is a transformation matrix that both scales and **translates**.
|
296
294
|
// the bounding box of this' four corners after transformed by the given affine transformation.
|
297
295
|
public transformedBoundingBox(affineTransform: Mat33): Rect2 {
|
298
|
-
return Rect2.bboxOf(this.corners.map(corner => affineTransform.transformVec2(corner)));
|
296
|
+
return Rect2.bboxOf(this.corners.map((corner) => affineTransform.transformVec2(corner)));
|
299
297
|
}
|
300
298
|
|
301
299
|
/** @return true iff this is equal to `other ± tolerance` */
|
@@ -307,13 +305,12 @@ export class Rect2 extends Abstract2DShape {
|
|
307
305
|
return `Rect(point(${this.x}, ${this.y}), size(${this.w}, ${this.h}))`;
|
308
306
|
}
|
309
307
|
|
310
|
-
|
311
308
|
public static fromCorners(corner1: Point2, corner2: Point2) {
|
312
309
|
return new Rect2(
|
313
310
|
Math.min(corner1.x, corner2.x),
|
314
311
|
Math.min(corner1.y, corner2.y),
|
315
312
|
Math.abs(corner1.x - corner2.x),
|
316
|
-
Math.abs(corner1.y - corner2.y)
|
313
|
+
Math.abs(corner1.y - corner2.y),
|
317
314
|
);
|
318
315
|
}
|
319
316
|
|
@@ -344,7 +341,7 @@ export class Rect2 extends Abstract2DShape {
|
|
344
341
|
|
345
342
|
return Rect2.fromCorners(
|
346
343
|
Vec2.of(minX - margin, minY - margin),
|
347
|
-
Vec2.of(maxX + margin, maxY + margin)
|
344
|
+
Vec2.of(maxX + margin, maxY + margin),
|
348
345
|
);
|
349
346
|
}
|
350
347
|
|
@@ -369,9 +366,7 @@ export class Rect2 extends Abstract2DShape {
|
|
369
366
|
maxY = Math.max(maxY, rect.y + rect.h);
|
370
367
|
}
|
371
368
|
|
372
|
-
return new Rect2(
|
373
|
-
minX, minY, maxX - minX, maxY - minY,
|
374
|
-
);
|
369
|
+
return new Rect2(minX, minY, maxX - minX, maxY - minY);
|
375
370
|
}
|
376
371
|
|
377
372
|
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
|
+
});
|
package/src/shapes/Triangle.ts
CHANGED
@@ -5,7 +5,7 @@ import Abstract2DShape from './Abstract2DShape';
|
|
5
5
|
import LineSegment2 from './LineSegment2';
|
6
6
|
import Rect2 from './Rect2';
|
7
7
|
|
8
|
-
type TriangleBoundary = [
|
8
|
+
type TriangleBoundary = [LineSegment2, LineSegment2, LineSegment2];
|
9
9
|
|
10
10
|
export default class Triangle extends Abstract2DShape {
|
11
11
|
/**
|
@@ -27,30 +27,26 @@ export default class Triangle extends Abstract2DShape {
|
|
27
27
|
return new Triangle(vertex1, vertex2, vertex3);
|
28
28
|
}
|
29
29
|
|
30
|
-
public get vertices(): [
|
31
|
-
return [
|
30
|
+
public get vertices(): [Point2, Point2, Point2] {
|
31
|
+
return [this.vertex1, this.vertex2, this.vertex3];
|
32
32
|
}
|
33
33
|
|
34
|
-
public map(mapping: (vertex: Vec3)=>Vec3): Triangle {
|
35
|
-
return new Triangle(
|
36
|
-
mapping(this.vertex1),
|
37
|
-
mapping(this.vertex2),
|
38
|
-
mapping(this.vertex3),
|
39
|
-
);
|
34
|
+
public map(mapping: (vertex: Vec3) => Vec3): Triangle {
|
35
|
+
return new Triangle(mapping(this.vertex1), mapping(this.vertex2), mapping(this.vertex3));
|
40
36
|
}
|
41
37
|
|
42
38
|
// Transform, treating this as composed of 2D points.
|
43
39
|
public transformed2DBy(affineTransform: Mat33) {
|
44
|
-
return this.map(vertex => affineTransform.transformVec2(vertex));
|
40
|
+
return this.map((vertex) => affineTransform.transformVec2(vertex));
|
45
41
|
}
|
46
42
|
|
47
43
|
// Transforms this by a linear transform --- verticies are treated as
|
48
44
|
// 3D points.
|
49
45
|
public transformedBy(linearTransform: Mat33) {
|
50
|
-
return this.map(vertex => linearTransform.transformVec3(vertex));
|
46
|
+
return this.map((vertex) => linearTransform.transformVec3(vertex));
|
51
47
|
}
|
52
48
|
|
53
|
-
#sides: TriangleBoundary|undefined = undefined;
|
49
|
+
#sides: TriangleBoundary | undefined = undefined;
|
54
50
|
|
55
51
|
/**
|
56
52
|
* Returns the sides of this triangle, as an array of `LineSegment2`s.
|
@@ -67,7 +63,7 @@ export default class Triangle extends Abstract2DShape {
|
|
67
63
|
const side2 = new LineSegment2(this.vertex2, this.vertex3);
|
68
64
|
const side3 = new LineSegment2(this.vertex3, this.vertex1);
|
69
65
|
|
70
|
-
const sides: TriangleBoundary = [
|
66
|
+
const sides: TriangleBoundary = [side1, side2, side3];
|
71
67
|
this.#sides = sides;
|
72
68
|
return sides;
|
73
69
|
}
|
@@ -76,15 +72,17 @@ export default class Triangle extends Abstract2DShape {
|
|
76
72
|
const result: Point2[] = [];
|
77
73
|
|
78
74
|
for (const edge of this.getEdges()) {
|
79
|
-
edge.intersectsLineSegment(lineSegment)
|
80
|
-
.forEach(point => result.push(point));
|
75
|
+
edge.intersectsLineSegment(lineSegment).forEach((point) => result.push(point));
|
81
76
|
}
|
82
77
|
|
83
78
|
return result;
|
84
79
|
}
|
85
80
|
|
86
81
|
/** @inheritdoc */
|
87
|
-
public override containsPoint(
|
82
|
+
public override containsPoint(
|
83
|
+
point: Vec3,
|
84
|
+
epsilon: number = Abstract2DShape.smallValue,
|
85
|
+
): boolean {
|
88
86
|
// Project `point` onto normals to each of this' sides.
|
89
87
|
// Uses the Separating Axis Theorem (https://en.wikipedia.org/wiki/Hyperplane_separation_theorem#Use_in_collision_detection)
|
90
88
|
const sides = this.getEdges();
|
@@ -103,7 +101,8 @@ export default class Triangle extends Abstract2DShape {
|
|
103
101
|
|
104
102
|
const projPoint = orthog.dot(point);
|
105
103
|
|
106
|
-
const inProjection =
|
104
|
+
const inProjection =
|
105
|
+
projPoint >= minProjVertex - epsilon && projPoint <= maxProjVertex + epsilon;
|
107
106
|
if (!inProjection) {
|
108
107
|
return false;
|
109
108
|
}
|
@@ -120,7 +119,7 @@ export default class Triangle extends Abstract2DShape {
|
|
120
119
|
*/
|
121
120
|
public override signedDistance(point: Vec3): number {
|
122
121
|
const sides = this.getEdges();
|
123
|
-
const distances = sides.map(side => side.distance(point));
|
122
|
+
const distances = sides.map((side) => side.distance(point));
|
124
123
|
const distance = Math.min(...distances);
|
125
124
|
|
126
125
|
// If the point is in this' interior, signedDistance must return a negative
|