@js-draw/math 1.21.3 → 1.23.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -1,9 +1,8 @@
|
|
1
|
-
|
2
1
|
import solveQuadratic from './solveQuadratic';
|
3
2
|
|
4
3
|
describe('solveQuadratic', () => {
|
5
4
|
it('should solve linear equations', () => {
|
6
|
-
expect(solveQuadratic(0, 1, 2)).toMatchObject([
|
5
|
+
expect(solveQuadratic(0, 1, 2)).toMatchObject([-2, -2]);
|
7
6
|
expect(solveQuadratic(0, 0, 2)[0]).toBeNaN();
|
8
7
|
});
|
9
8
|
|
@@ -11,21 +10,48 @@ describe('solveQuadratic', () => {
|
|
11
10
|
type TestCase = [[number, number, number], [number, number]];
|
12
11
|
|
13
12
|
const testCases: TestCase[] = [
|
14
|
-
[
|
15
|
-
|
13
|
+
[
|
14
|
+
[1, 0, 0],
|
15
|
+
[0, 0],
|
16
|
+
],
|
17
|
+
[
|
18
|
+
[2, 0, 0],
|
19
|
+
[0, 0],
|
20
|
+
],
|
16
21
|
|
17
|
-
[
|
18
|
-
|
19
|
-
|
22
|
+
[
|
23
|
+
[1, 0, -1],
|
24
|
+
[1, -1],
|
25
|
+
],
|
26
|
+
[
|
27
|
+
[1, 0, -4],
|
28
|
+
[2, -2],
|
29
|
+
],
|
30
|
+
[
|
31
|
+
[1, 0, 4],
|
32
|
+
[NaN, NaN],
|
33
|
+
],
|
20
34
|
|
21
|
-
[
|
22
|
-
|
35
|
+
[
|
36
|
+
[1, 1, 0],
|
37
|
+
[0, -1],
|
38
|
+
],
|
39
|
+
[
|
40
|
+
[1, 2, 0],
|
41
|
+
[0, -2],
|
42
|
+
],
|
23
43
|
|
24
|
-
[
|
25
|
-
|
44
|
+
[
|
45
|
+
[1, 2, 1],
|
46
|
+
[-1, -1],
|
47
|
+
],
|
48
|
+
[
|
49
|
+
[-9, 2, 1 / 3],
|
50
|
+
[1 / 3, -1 / 9],
|
51
|
+
],
|
26
52
|
];
|
27
53
|
|
28
|
-
for (const [
|
54
|
+
for (const [testCase, solution] of testCases) {
|
29
55
|
const foundSolutions = solveQuadratic(...testCase);
|
30
56
|
for (let i = 0; i < 2; i++) {
|
31
57
|
if (isNaN(solution[i]) && isNaN(foundSolutions[i])) {
|
@@ -36,4 +62,4 @@ describe('solveQuadratic', () => {
|
|
36
62
|
}
|
37
63
|
}
|
38
64
|
});
|
39
|
-
});
|
65
|
+
});
|
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
/**
|
3
2
|
* Solves an equation of the form ax² + bx + c = 0.
|
4
3
|
* The larger solution is returned first.
|
@@ -21,13 +20,13 @@ const solveQuadratic = (a: number, b: number, c: number): [number, number] => {
|
|
21
20
|
solution = -c / b;
|
22
21
|
}
|
23
22
|
|
24
|
-
return [
|
23
|
+
return [solution, solution];
|
25
24
|
}
|
26
25
|
|
27
26
|
const discriminant = b * b - 4 * a * c;
|
28
27
|
|
29
28
|
if (discriminant < 0) {
|
30
|
-
return [
|
29
|
+
return [NaN, NaN];
|
31
30
|
}
|
32
31
|
|
33
32
|
const rootDiscriminant = Math.sqrt(discriminant);
|
@@ -35,9 +34,9 @@ const solveQuadratic = (a: number, b: number, c: number): [number, number] => {
|
|
35
34
|
const solution2 = (-b - rootDiscriminant) / (2 * a);
|
36
35
|
|
37
36
|
if (solution1 > solution2) {
|
38
|
-
return [
|
37
|
+
return [solution1, solution2];
|
39
38
|
} else {
|
40
|
-
return [
|
39
|
+
return [solution2, solution1];
|
41
40
|
}
|
42
41
|
};
|
43
|
-
export default solveQuadratic;
|
42
|
+
export default solveQuadratic;
|
@@ -1,6 +1,5 @@
|
|
1
1
|
import { numberRegex } from './constants';
|
2
2
|
|
3
|
-
|
4
3
|
/**
|
5
4
|
* Returns the length of `numberAsString` after a decimal point.
|
6
5
|
*
|
@@ -16,7 +15,7 @@ export const getLenAfterDecimal = (numberAsString: string) => {
|
|
16
15
|
// like NaN or Infinity)
|
17
16
|
if (numberAsString.search(/[eE]/) !== -1 || /^[a-zA-Z]+$/.exec(numberAsString)) {
|
18
17
|
return -1;
|
19
|
-
|
18
|
+
// Or it has no decimal point
|
20
19
|
} else {
|
21
20
|
return 0;
|
22
21
|
}
|
package/src/rounding/lib.ts
CHANGED
@@ -1,2 +1 @@
|
|
1
|
-
|
2
|
-
export { toRoundedString } from './toRoundedString';
|
1
|
+
export { toRoundedString } from './toRoundedString';
|
@@ -1,6 +1,5 @@
|
|
1
1
|
import { toStringOfSamePrecision } from './toStringOfSamePrecision';
|
2
2
|
|
3
|
-
|
4
3
|
it('toStringOfSamePrecision', () => {
|
5
4
|
expect(toStringOfSamePrecision(1.23456, '1.12')).toBe('1.23');
|
6
5
|
expect(toStringOfSamePrecision(1.23456, '1.120')).toBe('1.235');
|
@@ -18,4 +17,4 @@ it('toStringOfSamePrecision', () => {
|
|
18
17
|
expect(toStringOfSamePrecision(-0.9999999999999432, '291.3')).toBe('-1');
|
19
18
|
expect(toStringOfSamePrecision(9998.9, '.1', '-11')).toBe('9998.9');
|
20
19
|
expect(toStringOfSamePrecision(-14.20000000000002, '.000001', '-11')).toBe('-14.2');
|
21
|
-
});
|
20
|
+
});
|
@@ -14,11 +14,9 @@ import Parameterized2DShape from './Parameterized2DShape';
|
|
14
14
|
* @internal
|
15
15
|
*/
|
16
16
|
export abstract class BezierJSWrapper extends Parameterized2DShape {
|
17
|
-
#bezierJs: Bezier|null = null;
|
17
|
+
#bezierJs: Bezier | null = null;
|
18
18
|
|
19
|
-
protected constructor(
|
20
|
-
bezierJsBezier?: Bezier
|
21
|
-
) {
|
19
|
+
protected constructor(bezierJsBezier?: Bezier) {
|
22
20
|
super();
|
23
21
|
|
24
22
|
if (bezierJsBezier) {
|
@@ -31,7 +29,7 @@ export abstract class BezierJSWrapper extends Parameterized2DShape {
|
|
31
29
|
|
32
30
|
protected getBezier() {
|
33
31
|
if (!this.#bezierJs) {
|
34
|
-
this.#bezierJs = new Bezier(this.getPoints().map(p => p.xy));
|
32
|
+
this.#bezierJs = new Bezier(this.getPoints().map((p) => p.xy));
|
35
33
|
}
|
36
34
|
return this.#bezierJs;
|
37
35
|
}
|
@@ -58,6 +56,7 @@ export abstract class BezierJSWrapper extends Parameterized2DShape {
|
|
58
56
|
return Vec2.ofXY(this.getBezier().get(t));
|
59
57
|
}
|
60
58
|
|
59
|
+
/** @returns the curve's directional derivative at `t`. */
|
61
60
|
public derivativeAt(t: number): Point2 {
|
62
61
|
return Vec2.ofXY(this.getBezier().derivative(t));
|
63
62
|
}
|
@@ -66,6 +65,7 @@ export abstract class BezierJSWrapper extends Parameterized2DShape {
|
|
66
65
|
return Vec2.ofXY((this.getBezier() as any).dderivative(t));
|
67
66
|
}
|
68
67
|
|
68
|
+
/** @returns the [normal vector](https://en.wikipedia.org/wiki/Normal_(geometry)) to this curve at `t`. */
|
69
69
|
public normal(t: number): Vec2 {
|
70
70
|
return Vec2.ofXY(this.getBezier().normal(t));
|
71
71
|
}
|
@@ -96,41 +96,49 @@ export abstract class BezierJSWrapper extends Parameterized2DShape {
|
|
96
96
|
const asLine = LineSegment2.ofSmallestContainingPoints(this.getPoints());
|
97
97
|
if (asLine) {
|
98
98
|
const intersection = asLine.intersectsLineSegment(line);
|
99
|
-
return intersection.map(p => this.nearestPointTo(p).parameterValue);
|
99
|
+
return intersection.map((p) => this.nearestPointTo(p).parameterValue);
|
100
100
|
}
|
101
101
|
|
102
102
|
const bezier = this.getBezier();
|
103
103
|
|
104
|
-
return bezier
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
104
|
+
return bezier
|
105
|
+
.intersects(line)
|
106
|
+
.map((t) => {
|
107
|
+
// We're using the .intersects(line) function, which is documented
|
108
|
+
// to always return numbers. However, to satisfy the type checker (and
|
109
|
+
// possibly improperly-defined types),
|
110
|
+
if (typeof t === 'string') {
|
111
|
+
t = parseFloat(t);
|
112
|
+
}
|
113
|
+
|
114
|
+
const point = Vec2.ofXY(this.at(t));
|
115
|
+
|
116
|
+
// Ensure that the intersection is on the line segment
|
117
|
+
if (point.distanceTo(line.p1) > line.length || point.distanceTo(line.p2) > line.length) {
|
118
|
+
return null;
|
119
|
+
}
|
120
|
+
|
121
|
+
return t;
|
122
|
+
})
|
123
|
+
.filter((entry) => entry !== null);
|
122
124
|
}
|
123
125
|
|
124
126
|
public override splitAt(t: number): [BezierJSWrapper] | [BezierJSWrapper, BezierJSWrapper] {
|
125
127
|
if (t <= 0 || t >= 1) {
|
126
|
-
return [
|
128
|
+
return [this];
|
127
129
|
}
|
128
130
|
|
129
131
|
const bezier = this.getBezier();
|
130
132
|
const split = bezier.split(t);
|
131
133
|
return [
|
132
|
-
new BezierJSWrapperImpl(
|
133
|
-
|
134
|
+
new BezierJSWrapperImpl(
|
135
|
+
split.left.points.map((point) => Vec2.ofXY(point)),
|
136
|
+
split.left,
|
137
|
+
),
|
138
|
+
new BezierJSWrapperImpl(
|
139
|
+
split.right.points.map((point) => Vec2.ofXY(point)),
|
140
|
+
split.right,
|
141
|
+
),
|
134
142
|
];
|
135
143
|
}
|
136
144
|
|
@@ -162,7 +170,7 @@ export abstract class BezierJSWrapper extends Parameterized2DShape {
|
|
162
170
|
|
163
171
|
// Start by testing a few points:
|
164
172
|
const pointsToTest = 4;
|
165
|
-
for (let i = 0; i < pointsToTest; i
|
173
|
+
for (let i = 0; i < pointsToTest; i++) {
|
166
174
|
const testT = i / (pointsToTest - 1);
|
167
175
|
const testMinSqrDist = sqrDistAt(testT);
|
168
176
|
|
@@ -181,8 +189,12 @@ export abstract class BezierJSWrapper extends Parameterized2DShape {
|
|
181
189
|
const bPrime = this.derivativeAt(t);
|
182
190
|
const bPrimePrime = this.secondDerivativeAt(t);
|
183
191
|
return (
|
184
|
-
2 * bPrime.x * bPrime.x
|
185
|
-
|
192
|
+
2 * bPrime.x * bPrime.x +
|
193
|
+
2 * b.x * bPrimePrime.x -
|
194
|
+
2 * point.x * bPrimePrime.x +
|
195
|
+
2 * bPrime.y * bPrime.y +
|
196
|
+
2 * b.y * bPrimePrime.y -
|
197
|
+
2 * point.y * bPrimePrime.y
|
186
198
|
);
|
187
199
|
};
|
188
200
|
// Because we're zeroing f'(t), we also need to be able to compute it:
|
@@ -191,8 +203,7 @@ export abstract class BezierJSWrapper extends Parameterized2DShape {
|
|
191
203
|
const b = this.at(t);
|
192
204
|
const bPrime = this.derivativeAt(t);
|
193
205
|
return (
|
194
|
-
2 * b.x * bPrime.x - 2 * point.x * bPrime.x
|
195
|
-
+ 2 * b.y * bPrime.y - 2 * point.y * bPrime.y
|
206
|
+
2 * b.x * bPrime.x - 2 * point.x * bPrime.x + 2 * b.y * bPrime.y - 2 * point.y * bPrime.y
|
196
207
|
);
|
197
208
|
};
|
198
209
|
|
@@ -226,7 +237,10 @@ export abstract class BezierJSWrapper extends Parameterized2DShape {
|
|
226
237
|
}
|
227
238
|
|
228
239
|
public intersectsBezier(other: BezierJSWrapper) {
|
229
|
-
const intersections = this.getBezier().intersects(other.getBezier()) as
|
240
|
+
const intersections = this.getBezier().intersects(other.getBezier()) as
|
241
|
+
| string[]
|
242
|
+
| null
|
243
|
+
| undefined;
|
230
244
|
if (!intersections || intersections.length === 0) {
|
231
245
|
return [];
|
232
246
|
}
|
@@ -239,7 +253,7 @@ export abstract class BezierJSWrapper extends Parameterized2DShape {
|
|
239
253
|
|
240
254
|
if (!match) {
|
241
255
|
throw new Error(
|
242
|
-
`Incorrect format returned by .intersects: ${intersections} should be array of "number/number"
|
256
|
+
`Incorrect format returned by .intersects: ${intersections} should be array of "number/number"!`,
|
243
257
|
);
|
244
258
|
}
|
245
259
|
|
@@ -253,7 +267,9 @@ export abstract class BezierJSWrapper extends Parameterized2DShape {
|
|
253
267
|
}
|
254
268
|
|
255
269
|
public override toString() {
|
256
|
-
return `Bézier(${this.getPoints()
|
270
|
+
return `Bézier(${this.getPoints()
|
271
|
+
.map((point) => point.toString())
|
272
|
+
.join(', ')})`;
|
257
273
|
}
|
258
274
|
}
|
259
275
|
|
@@ -262,7 +278,10 @@ export abstract class BezierJSWrapper extends Parameterized2DShape {
|
|
262
278
|
* around a `Bezier`.
|
263
279
|
*/
|
264
280
|
class BezierJSWrapperImpl extends BezierJSWrapper {
|
265
|
-
public constructor(
|
281
|
+
public constructor(
|
282
|
+
private controlPoints: readonly Point2[],
|
283
|
+
curve?: Bezier,
|
284
|
+
) {
|
266
285
|
super(curve);
|
267
286
|
}
|
268
287
|
|
@@ -271,4 +290,4 @@ class BezierJSWrapperImpl extends BezierJSWrapper {
|
|
271
290
|
}
|
272
291
|
}
|
273
292
|
|
274
|
-
export default BezierJSWrapper;
|
293
|
+
export default BezierJSWrapper;
|
@@ -23,13 +23,13 @@ class CubicBezier extends BezierJSWrapper {
|
|
23
23
|
}
|
24
24
|
|
25
25
|
public override getPoints() {
|
26
|
-
return [
|
26
|
+
return [this.p0, this.p1, this.p2, this.p3];
|
27
27
|
}
|
28
28
|
|
29
29
|
/** Returns an overestimate of this shape's bounding box. */
|
30
30
|
public override getLooseBoundingBox(): Rect2 {
|
31
|
-
return Rect2.bboxOf([
|
31
|
+
return Rect2.bboxOf([this.p0, this.p1, this.p2, this.p3]);
|
32
32
|
}
|
33
33
|
}
|
34
34
|
|
35
|
-
export default CubicBezier;
|
35
|
+
export default CubicBezier;
|
@@ -2,7 +2,6 @@ import LineSegment2 from './LineSegment2';
|
|
2
2
|
import { Vec2 } from '../Vec2';
|
3
3
|
import Mat33 from '../Mat33';
|
4
4
|
|
5
|
-
|
6
5
|
describe('Line2', () => {
|
7
6
|
it('x and y axes should intersect at (0, 0)', () => {
|
8
7
|
const xAxis = new LineSegment2(Vec2.of(-10, 0), Vec2.of(10, 0));
|
@@ -65,7 +64,7 @@ describe('Line2', () => {
|
|
65
64
|
// Points taken from issue observed directly in editor
|
66
65
|
const p1 = Vec2.of(769.6126045442547, 221.037877485765);
|
67
66
|
const p2 = Vec2.of(770.3873954557453, 224.962122514235);
|
68
|
-
const p3 = Vec2.of(
|
67
|
+
const p3 = Vec2.of(763.3590010920082, 223.66723995850086);
|
69
68
|
const p4 = Vec2.of(763.5494167642871, 223.66723995850086);
|
70
69
|
|
71
70
|
const line1 = new LineSegment2(p1, p2);
|
@@ -115,7 +114,7 @@ describe('Line2', () => {
|
|
115
114
|
// Halving
|
116
115
|
//
|
117
116
|
expect(lineSegment.at(0.5)).objEq(midpoint);
|
118
|
-
const [
|
117
|
+
const [firstHalf, secondHalf] = lineSegment.splitAt(0.5);
|
119
118
|
|
120
119
|
if (!secondHalf) {
|
121
120
|
throw new Error('Splitting a line segment in half should yield two line segments.');
|
@@ -136,24 +135,32 @@ describe('Line2', () => {
|
|
136
135
|
it('equivalence check should allow ignoring direction', () => {
|
137
136
|
expect(new LineSegment2(Vec2.zero, Vec2.unitX)).objEq(new LineSegment2(Vec2.zero, Vec2.unitX));
|
138
137
|
expect(new LineSegment2(Vec2.zero, Vec2.unitX)).objEq(new LineSegment2(Vec2.unitX, Vec2.zero));
|
139
|
-
expect(new LineSegment2(Vec2.zero, Vec2.unitX)).not.objEq(
|
138
|
+
expect(new LineSegment2(Vec2.zero, Vec2.unitX)).not.objEq(
|
139
|
+
new LineSegment2(Vec2.unitX, Vec2.zero),
|
140
|
+
{ ignoreDirection: false },
|
141
|
+
);
|
140
142
|
});
|
141
143
|
|
142
144
|
it('should support creating from a collection of points', () => {
|
143
145
|
expect(LineSegment2.ofSmallestContainingPoints([])).toBeNull();
|
144
146
|
expect(LineSegment2.ofSmallestContainingPoints([Vec2.of(1, 1)])).toBeNull();
|
145
|
-
expect(
|
146
|
-
[Vec2.of(1, 1), Vec2.of(1, 2), Vec2.of(3, 3)]
|
147
|
-
)
|
148
|
-
|
149
|
-
expect(LineSegment2.ofSmallestContainingPoints(
|
150
|
-
|
151
|
-
)
|
152
|
-
expect(
|
153
|
-
[Vec2.of(1, 1), Vec2.of(2, 2), Vec2.of(3, 3)]
|
154
|
-
)
|
155
|
-
expect(
|
156
|
-
|
157
|
-
|
147
|
+
expect(
|
148
|
+
LineSegment2.ofSmallestContainingPoints([Vec2.of(1, 1), Vec2.of(1, 2), Vec2.of(3, 3)]),
|
149
|
+
).toBeNull();
|
150
|
+
|
151
|
+
expect(LineSegment2.ofSmallestContainingPoints([Vec2.of(1, 1), Vec2.of(1, 2)])).objEq(
|
152
|
+
new LineSegment2(Vec2.of(1, 1), Vec2.of(1, 2)),
|
153
|
+
);
|
154
|
+
expect(
|
155
|
+
LineSegment2.ofSmallestContainingPoints([Vec2.of(1, 1), Vec2.of(2, 2), Vec2.of(3, 3)]),
|
156
|
+
).objEq(new LineSegment2(Vec2.of(1, 1), Vec2.of(3, 3)));
|
157
|
+
expect(
|
158
|
+
LineSegment2.ofSmallestContainingPoints([
|
159
|
+
Vec2.of(3, 3),
|
160
|
+
Vec2.of(2, 2),
|
161
|
+
Vec2.of(2.4, 2.4),
|
162
|
+
Vec2.of(3, 3),
|
163
|
+
]),
|
164
|
+
).objEq(new LineSegment2(Vec2.of(2, 2), Vec2.of(3, 3)));
|
158
165
|
});
|
159
166
|
});
|
@@ -42,7 +42,7 @@ export class LineSegment2 extends Parameterized2DShape {
|
|
42
42
|
/** Creates a new `LineSegment2` from its endpoints. */
|
43
43
|
public constructor(
|
44
44
|
private readonly point1: Point2,
|
45
|
-
private readonly point2: Point2
|
45
|
+
private readonly point2: Point2,
|
46
46
|
) {
|
47
47
|
super();
|
48
48
|
|
@@ -70,7 +70,7 @@ export class LineSegment2 extends Parameterized2DShape {
|
|
70
70
|
public static ofSmallestContainingPoints(points: readonly Point2[]) {
|
71
71
|
if (points.length <= 1) return null;
|
72
72
|
|
73
|
-
const sorted = [...points].sort((a, b) => a.x !== b.x ? a.x - b.x : a.y - b.y);
|
73
|
+
const sorted = [...points].sort((a, b) => (a.x !== b.x ? a.x - b.x : a.y - b.y));
|
74
74
|
const line = new LineSegment2(sorted[0], sorted[sorted.length - 1]);
|
75
75
|
|
76
76
|
for (const point of sorted) {
|
@@ -127,15 +127,12 @@ export class LineSegment2 extends Parameterized2DShape {
|
|
127
127
|
return this.direction;
|
128
128
|
}
|
129
129
|
|
130
|
-
public splitAt(t: number): [LineSegment2]|[LineSegment2,LineSegment2] {
|
130
|
+
public splitAt(t: number): [LineSegment2] | [LineSegment2, LineSegment2] {
|
131
131
|
if (t <= 0 || t >= 1) {
|
132
132
|
return [this];
|
133
133
|
}
|
134
134
|
|
135
|
-
return [
|
136
|
-
new LineSegment2(this.point1, this.at(t)),
|
137
|
-
new LineSegment2(this.at(t), this.point2),
|
138
|
-
];
|
135
|
+
return [new LineSegment2(this.point1, this.at(t)), new LineSegment2(this.at(t), this.point2)];
|
139
136
|
}
|
140
137
|
|
141
138
|
/**
|
@@ -146,7 +143,7 @@ export class LineSegment2 extends Parameterized2DShape {
|
|
146
143
|
* This will change in a future release.
|
147
144
|
* @deprecated
|
148
145
|
*/
|
149
|
-
public intersection(other: LineSegment2): IntersectionResult|null {
|
146
|
+
public intersection(other: LineSegment2): IntersectionResult | null {
|
150
147
|
// TODO(v2.0.0): Make this return a `t` value from `0` to `1`.
|
151
148
|
|
152
149
|
// We want x₁(t) = x₂(t) and y₁(t) = y₂(t)
|
@@ -191,21 +188,18 @@ export class LineSegment2 extends Parameterized2DShape {
|
|
191
188
|
|
192
189
|
const xIntersect = this.point1.x;
|
193
190
|
const yIntersect =
|
194
|
-
(this.point1.x - other.point1.x) * other.direction.y / other.direction.x + other.point1.y;
|
191
|
+
((this.point1.x - other.point1.x) * other.direction.y) / other.direction.x + other.point1.y;
|
195
192
|
resultPoint = Vec2.of(xIntersect, yIntersect);
|
196
193
|
resultT = (yIntersect - this.point1.y) / this.direction.y;
|
197
194
|
} else {
|
198
195
|
// From above,
|
199
196
|
// x = ((o₁ᵧ - o₂ᵧ)(d₁ₓd₂ₓ) + (d₂ᵧd₁ₓ)(o₂ₓ) - (d₁ᵧd₂ₓ)(o₁ₓ))/(d₂ᵧd₁ₓ - d₁ᵧd₂ₓ)
|
200
|
-
const numerator =
|
201
|
-
(this.point1.y - other.point1.y) * this.direction.x * other.direction.x
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
other.direction.y * this.direction.x
|
207
|
-
- this.direction.y * other.direction.x
|
208
|
-
);
|
197
|
+
const numerator =
|
198
|
+
(this.point1.y - other.point1.y) * this.direction.x * other.direction.x +
|
199
|
+
this.direction.x * other.direction.y * other.point1.x -
|
200
|
+
this.direction.y * other.direction.x * this.point1.x;
|
201
|
+
const denominator =
|
202
|
+
other.direction.y * this.direction.x - this.direction.y * other.direction.x;
|
209
203
|
|
210
204
|
// Avoid dividing by zero. It means there is no intersection
|
211
205
|
if (denominator === 0) {
|
@@ -225,10 +219,12 @@ export class LineSegment2 extends Parameterized2DShape {
|
|
225
219
|
const resultToP3 = resultPoint.distanceTo(other.point1);
|
226
220
|
const resultToP4 = resultPoint.distanceTo(other.point2);
|
227
221
|
|
228
|
-
if (
|
229
|
-
|
230
|
-
|
231
|
-
|
222
|
+
if (
|
223
|
+
resultToP1 > this.length ||
|
224
|
+
resultToP2 > this.length ||
|
225
|
+
resultToP3 > other.length ||
|
226
|
+
resultToP4 > other.length
|
227
|
+
) {
|
232
228
|
return null;
|
233
229
|
}
|
234
230
|
|
@@ -246,7 +242,7 @@ export class LineSegment2 extends Parameterized2DShape {
|
|
246
242
|
const intersection = this.intersection(lineSegment);
|
247
243
|
|
248
244
|
if (intersection) {
|
249
|
-
return [
|
245
|
+
return [intersection.t / this.length];
|
250
246
|
}
|
251
247
|
return [];
|
252
248
|
}
|
@@ -263,7 +259,7 @@ export class LineSegment2 extends Parameterized2DShape {
|
|
263
259
|
const intersection = this.intersection(lineSegment);
|
264
260
|
|
265
261
|
if (intersection) {
|
266
|
-
return [
|
262
|
+
return [intersection.point];
|
267
263
|
}
|
268
264
|
return [];
|
269
265
|
}
|
@@ -273,7 +269,7 @@ export class LineSegment2 extends Parameterized2DShape {
|
|
273
269
|
return this.nearestPointTo(target).point;
|
274
270
|
}
|
275
271
|
|
276
|
-
public override nearestPointTo(target: Vec3): { point: Vec3; parameterValue: number
|
272
|
+
public override nearestPointTo(target: Vec3): { point: Vec3; parameterValue: number } {
|
277
273
|
// Distance from P1 along this' direction.
|
278
274
|
const projectedDistFromP1 = target.minus(this.p1).dot(this.direction);
|
279
275
|
const projectedDistFromP2 = this.length - projectedDistFromP1;
|
@@ -304,7 +300,8 @@ export class LineSegment2 extends Parameterized2DShape {
|
|
304
300
|
/** Returns a copy of this line segment transformed by the given `affineTransfm`. */
|
305
301
|
public transformedBy(affineTransfm: Mat33): LineSegment2 {
|
306
302
|
return new LineSegment2(
|
307
|
-
affineTransfm.transformVec2(this.p1),
|
303
|
+
affineTransfm.transformVec2(this.p1),
|
304
|
+
affineTransfm.transformVec2(this.p2),
|
308
305
|
);
|
309
306
|
}
|
310
307
|
|
@@ -324,7 +321,7 @@ export class LineSegment2 extends Parameterized2DShape {
|
|
324
321
|
* - `tolerance`: The maximum difference between endpoints. (Default: 0)
|
325
322
|
* - `ignoreDirection`: Allow matching a version of `this` with opposite direction. (Default: `true`)
|
326
323
|
*/
|
327
|
-
public eq(other: LineSegment2, options?: { tolerance?: number
|
324
|
+
public eq(other: LineSegment2, options?: { tolerance?: number; ignoreDirection?: boolean }) {
|
328
325
|
if (!(other instanceof LineSegment2)) {
|
329
326
|
return false;
|
330
327
|
}
|
@@ -333,8 +330,8 @@ export class LineSegment2 extends Parameterized2DShape {
|
|
333
330
|
const ignoreDirection = options?.ignoreDirection ?? true;
|
334
331
|
|
335
332
|
return (
|
336
|
-
(other.p1.eq(this.p1, tolerance) && other.p2.eq(this.p2, tolerance))
|
337
|
-
|
333
|
+
(other.p1.eq(this.p1, tolerance) && other.p2.eq(this.p2, tolerance)) ||
|
334
|
+
(ignoreDirection && other.p1.eq(this.p2, tolerance) && other.p2.eq(this.p1, tolerance))
|
338
335
|
);
|
339
336
|
}
|
340
337
|
}
|
@@ -20,13 +20,15 @@ export abstract class Parameterized2DShape extends Abstract2DShape {
|
|
20
20
|
/**
|
21
21
|
* Divides this shape into two separate shapes at parameter value $t$.
|
22
22
|
*/
|
23
|
-
abstract splitAt(
|
23
|
+
abstract splitAt(
|
24
|
+
t: number,
|
25
|
+
): [Parameterized2DShape] | [Parameterized2DShape, Parameterized2DShape];
|
24
26
|
|
25
27
|
/**
|
26
28
|
* Returns the nearest point on `this` to `point` and the `parameterValue` at which
|
27
29
|
* that point occurs.
|
28
30
|
*/
|
29
|
-
abstract nearestPointTo(point: Point2): { point: Point2
|
31
|
+
abstract nearestPointTo(point: Point2): { point: Point2; parameterValue: number };
|
30
32
|
|
31
33
|
/**
|
32
34
|
* Returns the **parameter values** at which `lineSegment` intersects this shape.
|
@@ -35,9 +37,8 @@ export abstract class Parameterized2DShape extends Abstract2DShape {
|
|
35
37
|
*/
|
36
38
|
public abstract argIntersectsLineSegment(lineSegment: LineSegment2): number[];
|
37
39
|
|
38
|
-
|
39
40
|
public override intersectsLineSegment(line: LineSegment2): Point2[] {
|
40
|
-
return this.argIntersectsLineSegment(line).map(t => this.at(t));
|
41
|
+
return this.argIntersectsLineSegment(line).map((t) => this.at(t));
|
41
42
|
}
|
42
43
|
}
|
43
44
|
|
@@ -27,7 +27,7 @@ describe('Path.fromString', () => {
|
|
27
27
|
},
|
28
28
|
{
|
29
29
|
kind: PathCommandType.MoveTo,
|
30
|
-
point: Vec2.of(3,3),
|
30
|
+
point: Vec2.of(3, 3),
|
31
31
|
},
|
32
32
|
]);
|
33
33
|
expect(path3.startPoint).toMatchObject(Vec2.of(1, 1));
|
@@ -71,7 +71,7 @@ describe('Path.fromString', () => {
|
|
71
71
|
const path1 = Path.fromString('m3,3 l1,2 l1,1 z');
|
72
72
|
const path2 = Path.fromString('m3,3 l1,2 l1,1 Z');
|
73
73
|
|
74
|
-
expect(path1.startPoint).toMatchObject(Vec2.of(3,3));
|
74
|
+
expect(path1.startPoint).toMatchObject(Vec2.of(3, 3));
|
75
75
|
expect(path2.startPoint).toMatchObject(path1.startPoint);
|
76
76
|
expect(path1.parts).toMatchObject(path2.parts);
|
77
77
|
expect(path1.parts).toMatchObject([
|
@@ -86,7 +86,7 @@ describe('Path.fromString', () => {
|
|
86
86
|
{
|
87
87
|
kind: PathCommandType.LineTo,
|
88
88
|
point: path1.startPoint,
|
89
|
-
}
|
89
|
+
},
|
90
90
|
]);
|
91
91
|
});
|
92
92
|
|
@@ -128,7 +128,7 @@ describe('Path.fromString', () => {
|
|
128
128
|
controlPoint1: Vec2.of(1, 1),
|
129
129
|
controlPoint2: Vec2.of(0.1, 0.1),
|
130
130
|
endPoint: Vec2.zero,
|
131
|
-
}
|
131
|
+
},
|
132
132
|
]);
|
133
133
|
});
|
134
134
|
|
@@ -220,4 +220,4 @@ describe('Path.fromString', () => {
|
|
220
220
|
]);
|
221
221
|
expect(path.startPoint).toMatchObject(Vec2.of(5, 10));
|
222
222
|
});
|
223
|
-
});
|
223
|
+
});
|