@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
@@ -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
|
+
});
|