@js-draw/math 1.2.2 → 1.3.1
Sign up to get free protection for your applications and to get access to all the features.
- package/dist/cjs/Color4.d.ts +2 -2
- package/dist/cjs/Mat33.d.ts +31 -1
- package/dist/cjs/Mat33.js +232 -24
- package/dist/cjs/Vec3.d.ts +7 -0
- package/dist/cjs/Vec3.js +9 -0
- package/dist/cjs/shapes/Path.d.ts +7 -0
- package/dist/cjs/shapes/Path.js +33 -0
- package/dist/cjs/shapes/Rect2.d.ts +1 -1
- package/dist/cjs/shapes/Rect2.js +19 -11
- package/dist/mjs/Color4.d.ts +2 -2
- package/dist/mjs/Color4.mjs +1 -2
- package/dist/mjs/Mat33.d.ts +31 -1
- package/dist/mjs/Mat33.mjs +232 -24
- package/dist/mjs/Vec3.d.ts +7 -0
- package/dist/mjs/Vec3.mjs +9 -0
- package/dist/mjs/shapes/Path.d.ts +7 -0
- package/dist/mjs/shapes/Path.mjs +33 -0
- package/dist/mjs/shapes/Rect2.d.ts +1 -1
- package/dist/mjs/shapes/Rect2.mjs +19 -11
- package/package.json +3 -3
- package/src/Color4.ts +2 -2
- package/src/Mat33.fromCSSMatrix.test.ts +90 -0
- package/src/Mat33.test.ts +6 -42
- package/src/Mat33.ts +143 -32
- package/src/Vec3.ts +10 -0
- package/src/rounding.ts +1 -0
- package/src/shapes/Path.ts +37 -0
- package/src/shapes/Rect2.test.ts +34 -0
- package/src/shapes/Rect2.ts +25 -12
- /package/dist/cjs/{Mat33.test.d.ts → Mat33.fromCSSMatrix.test.d.ts} +0 -0
- /package/dist/mjs/{Mat33.test.d.ts → Mat33.fromCSSMatrix.test.d.ts} +0 -0
package/src/Mat33.ts
CHANGED
@@ -335,9 +335,37 @@ export class Mat33 {
|
|
335
335
|
return Math.hypot(this.a1, this.a2);
|
336
336
|
}
|
337
337
|
|
338
|
+
/** Returns the `idx`-th column (`idx` is 0-indexed). */
|
339
|
+
public getColumn(idx: number) {
|
340
|
+
return Vec3.of(
|
341
|
+
this.rows[0].at(idx),
|
342
|
+
this.rows[1].at(idx),
|
343
|
+
this.rows[2].at(idx),
|
344
|
+
);
|
345
|
+
}
|
346
|
+
|
347
|
+
/** Returns the magnitude of the entry with the largest entry */
|
348
|
+
public maximumEntryMagnitude() {
|
349
|
+
let greatestSoFar = Math.abs(this.a1);
|
350
|
+
for (const entry of this.toArray()) {
|
351
|
+
greatestSoFar = Math.max(greatestSoFar, Math.abs(entry));
|
352
|
+
}
|
353
|
+
|
354
|
+
return greatestSoFar;
|
355
|
+
}
|
356
|
+
|
338
357
|
/**
|
339
358
|
* Constructs a 3x3 translation matrix (for translating `Vec2`s) using
|
340
359
|
* **transformVec2**.
|
360
|
+
*
|
361
|
+
* Creates a matrix in the form
|
362
|
+
* $$
|
363
|
+
* \begin{pmatrix}
|
364
|
+
* 1 & 0 & {\tt amount.x}\\
|
365
|
+
* 0 & 1 & {\tt amount.y}\\
|
366
|
+
* 0 & 0 & 1
|
367
|
+
* \end{pmatrix}
|
368
|
+
* $$
|
341
369
|
*/
|
342
370
|
public static translation(amount: Vec2): Mat33 {
|
343
371
|
// When transforming Vec2s by a 3x3 matrix, we give the input
|
@@ -392,7 +420,11 @@ export class Mat33 {
|
|
392
420
|
return result.rightMul(Mat33.translation(center.times(-1)));
|
393
421
|
}
|
394
422
|
|
395
|
-
/**
|
423
|
+
/**
|
424
|
+
* **Note**: Assumes `this.c1 = this.c2 = 0` and `this.c3 = 1`.
|
425
|
+
*
|
426
|
+
* @see {@link fromCSSMatrix} and {@link toSafeCSSTransformList}
|
427
|
+
*/
|
396
428
|
public toCSSMatrix(): string {
|
397
429
|
return `matrix(${this.a1},${this.b1},${this.a2},${this.b2},${this.a3},${this.b3})`;
|
398
430
|
}
|
@@ -412,39 +444,118 @@ export class Mat33 {
|
|
412
444
|
return Mat33.identity;
|
413
445
|
}
|
414
446
|
|
415
|
-
const
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
447
|
+
const parseArguments = (argumentString: string) => {
|
448
|
+
return argumentString.split(/[, \t\n]+/g).map(argString => {
|
449
|
+
let isPercentage = false;
|
450
|
+
if (argString.endsWith('%')) {
|
451
|
+
isPercentage = true;
|
452
|
+
argString = argString.substring(0, argString.length - 1);
|
453
|
+
}
|
454
|
+
|
455
|
+
// Remove trailing px units.
|
456
|
+
argString = argString.replace(/px$/ig, '');
|
457
|
+
|
458
|
+
const numberExp = /^[-]?\d*(?:\.\d*)?(?:[eE][-+]?\d+)?$/i;
|
459
|
+
|
460
|
+
if (!numberExp.exec(argString)) {
|
461
|
+
throw new Error(
|
462
|
+
`All arguments to transform functions must be numeric (state: ${
|
463
|
+
JSON.stringify({
|
464
|
+
currentArgument: argString,
|
465
|
+
allArguments: argumentString,
|
466
|
+
})
|
467
|
+
})`
|
468
|
+
);
|
469
|
+
}
|
470
|
+
|
471
|
+
let argNumber = parseFloat(argString);
|
472
|
+
|
473
|
+
if (isPercentage) {
|
474
|
+
argNumber /= 100;
|
475
|
+
}
|
476
|
+
|
477
|
+
return argNumber;
|
478
|
+
});
|
479
|
+
};
|
480
|
+
|
481
|
+
|
482
|
+
const keywordToAction = {
|
483
|
+
matrix: (matrixData: number[]) => {
|
484
|
+
if (matrixData.length !== 6) {
|
485
|
+
throw new Error(`Invalid matrix argument: ${matrixData}. Must have length 6`);
|
486
|
+
}
|
487
|
+
|
488
|
+
const a = matrixData[0];
|
489
|
+
const b = matrixData[1];
|
490
|
+
const c = matrixData[2];
|
491
|
+
const d = matrixData[3];
|
492
|
+
const e = matrixData[4];
|
493
|
+
const f = matrixData[5];
|
494
|
+
|
495
|
+
const transform = new Mat33(
|
496
|
+
a, c, e,
|
497
|
+
b, d, f,
|
498
|
+
0, 0, 1
|
499
|
+
);
|
500
|
+
return transform;
|
501
|
+
},
|
502
|
+
|
503
|
+
scale: (scaleArgs: number[]) => {
|
504
|
+
let scaleX, scaleY;
|
505
|
+
if (scaleArgs.length === 1) {
|
506
|
+
scaleX = scaleArgs[0];
|
507
|
+
scaleY = scaleArgs[0];
|
508
|
+
} else if (scaleArgs.length === 2) {
|
509
|
+
scaleX = scaleArgs[0];
|
510
|
+
scaleY = scaleArgs[1];
|
511
|
+
} else {
|
512
|
+
throw new Error(`The scale() function only supports two arguments. Given: ${scaleArgs}`);
|
513
|
+
}
|
514
|
+
|
515
|
+
return Mat33.scaling2D(Vec2.of(scaleX, scaleY));
|
516
|
+
},
|
517
|
+
|
518
|
+
translate: (translateArgs: number[]) => {
|
519
|
+
let translateX = 0;
|
520
|
+
let translateY = 0;
|
521
|
+
|
522
|
+
if (translateArgs.length === 1) {
|
523
|
+
// If no y translation is given, assume 0.
|
524
|
+
translateX = translateArgs[0];
|
525
|
+
} else if (translateArgs.length === 2) {
|
526
|
+
translateX = translateArgs[0];
|
527
|
+
translateY = translateArgs[1];
|
528
|
+
} else {
|
529
|
+
throw new Error(`The translate() function requires either 1 or 2 arguments. Given ${translateArgs}`);
|
530
|
+
}
|
531
|
+
|
532
|
+
return Mat33.translation(Vec2.of(translateX, translateY));
|
533
|
+
},
|
534
|
+
};
|
535
|
+
|
536
|
+
// A command (\w+)
|
537
|
+
// followed by a set of arguments ([ \t\n0-9eE.,\-%]+)
|
538
|
+
const partRegex = /\s*(\w+)\s*\(([^)]*)\)/ig;
|
539
|
+
let match;
|
540
|
+
let matrix: Mat33|null = null;
|
541
|
+
|
542
|
+
while ((match = partRegex.exec(cssString)) !== null) {
|
543
|
+
const action = match[1].toLowerCase();
|
544
|
+
if (!(action in keywordToAction)) {
|
545
|
+
throw new Error(`Unsupported CSS transform action: ${action}`);
|
546
|
+
}
|
547
|
+
|
548
|
+
const args = parseArguments(match[2]);
|
549
|
+
const currentMatrix = keywordToAction[action as keyof typeof keywordToAction](args);
|
550
|
+
|
551
|
+
if (!matrix) {
|
552
|
+
matrix = currentMatrix;
|
553
|
+
} else {
|
554
|
+
matrix = matrix.rightMul(currentMatrix);
|
555
|
+
}
|
432
556
|
}
|
433
557
|
|
434
|
-
|
435
|
-
const a = matrixData[0];
|
436
|
-
const b = matrixData[1];
|
437
|
-
const c = matrixData[2];
|
438
|
-
const d = matrixData[3];
|
439
|
-
const e = matrixData[4];
|
440
|
-
const f = matrixData[5];
|
441
|
-
|
442
|
-
const transform = new Mat33(
|
443
|
-
a, c, e,
|
444
|
-
b, d, f,
|
445
|
-
0, 0, 1
|
446
|
-
);
|
447
|
-
return transform;
|
558
|
+
return matrix ?? Mat33.identity;
|
448
559
|
}
|
449
560
|
}
|
450
561
|
export default Mat33;
|
package/src/Vec3.ts
CHANGED
@@ -63,6 +63,16 @@ export class Vec3 {
|
|
63
63
|
return this.dot(this);
|
64
64
|
}
|
65
65
|
|
66
|
+
/**
|
67
|
+
* Returns the entry of this with the greatest magnitude.
|
68
|
+
*
|
69
|
+
* In other words, returns $\max \{ |x| : x \in {\bf v} \}$, where ${\bf v}$ is the set of
|
70
|
+
* all entries of this vector.
|
71
|
+
*/
|
72
|
+
public maximumEntryMagnitude(): number {
|
73
|
+
return Math.max(Math.abs(this.x), Math.max(Math.abs(this.y), Math.abs(this.z)));
|
74
|
+
}
|
75
|
+
|
66
76
|
/**
|
67
77
|
* Return this' angle in the XY plane (treats this as a Vec2).
|
68
78
|
*
|
package/src/rounding.ts
CHANGED
package/src/shapes/Path.ts
CHANGED
@@ -97,6 +97,8 @@ export class Path {
|
|
97
97
|
const geometry: GeometryArrayType = [];
|
98
98
|
|
99
99
|
for (const part of this.parts) {
|
100
|
+
let exhaustivenessCheck: never;
|
101
|
+
|
100
102
|
switch (part.kind) {
|
101
103
|
case PathCommandType.CubicBezierTo:
|
102
104
|
geometry.push(
|
@@ -124,6 +126,9 @@ export class Path {
|
|
124
126
|
geometry.push(new PointShape2D(part.point));
|
125
127
|
startPoint = part.point;
|
126
128
|
break;
|
129
|
+
default:
|
130
|
+
exhaustivenessCheck = part;
|
131
|
+
return exhaustivenessCheck;
|
127
132
|
}
|
128
133
|
}
|
129
134
|
|
@@ -131,6 +136,38 @@ export class Path {
|
|
131
136
|
return this.cachedGeometry;
|
132
137
|
}
|
133
138
|
|
139
|
+
/**
|
140
|
+
* Iterates through the start/end points of each component in this path.
|
141
|
+
*
|
142
|
+
* If a start point is equivalent to the end point of the previous segment,
|
143
|
+
* the point is **not** emitted twice.
|
144
|
+
*/
|
145
|
+
public *startEndPoints() {
|
146
|
+
yield this.startPoint;
|
147
|
+
|
148
|
+
for (const part of this.parts) {
|
149
|
+
let exhaustivenessCheck: never;
|
150
|
+
|
151
|
+
switch (part.kind) {
|
152
|
+
case PathCommandType.CubicBezierTo:
|
153
|
+
yield part.endPoint;
|
154
|
+
break;
|
155
|
+
case PathCommandType.QuadraticBezierTo:
|
156
|
+
yield part.endPoint;
|
157
|
+
break;
|
158
|
+
case PathCommandType.LineTo:
|
159
|
+
yield part.point;
|
160
|
+
break;
|
161
|
+
case PathCommandType.MoveTo:
|
162
|
+
yield part.point;
|
163
|
+
break;
|
164
|
+
default:
|
165
|
+
exhaustivenessCheck = part;
|
166
|
+
return exhaustivenessCheck;
|
167
|
+
}
|
168
|
+
}
|
169
|
+
}
|
170
|
+
|
134
171
|
private cachedPolylineApproximation: LineSegment2[]|null = null;
|
135
172
|
|
136
173
|
// Approximates this path with a group of line segments.
|
package/src/shapes/Rect2.test.ts
CHANGED
@@ -111,6 +111,27 @@ describe('Rect2', () => {
|
|
111
111
|
expect(new Rect2(-100, -1, 200, 2).intersects(new Rect2(-5, 50, 10, 30))).toBe(false);
|
112
112
|
});
|
113
113
|
|
114
|
+
it('should correctly compute the intersection of one rectangle and several others', () => {
|
115
|
+
const mainRect = new Rect2(334,156,333,179);
|
116
|
+
const shouldIntersect = [
|
117
|
+
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),
|
125
|
+
];
|
126
|
+
|
127
|
+
for (const rect of shouldIntersect) {
|
128
|
+
expect(mainRect.intersects(rect)).toBe(true);
|
129
|
+
}
|
130
|
+
for (const rect of shouldNotIntersect) {
|
131
|
+
expect(mainRect.intersects(rect)).toBe(false);
|
132
|
+
}
|
133
|
+
});
|
134
|
+
|
114
135
|
it('intersecting rectangles should have their intersections correctly computed', () => {
|
115
136
|
expect(new Rect2(-1, -1, 2, 2).intersection(Rect2.empty)).objEq(Rect2.empty);
|
116
137
|
expect(new Rect2(-1, -1, 2, 2).intersection(new Rect2(0, 0, 3, 3))).objEq(
|
@@ -130,6 +151,19 @@ describe('Rect2', () => {
|
|
130
151
|
expect(transformedBBox.containsRect(rect)).toBe(true);
|
131
152
|
});
|
132
153
|
|
154
|
+
it('.grownBy should expand a rectangle by the given margin', () => {
|
155
|
+
expect(Rect2.empty.grownBy(0)).toBe(Rect2.empty);
|
156
|
+
|
157
|
+
// Should add padding to all sides.
|
158
|
+
expect(new Rect2(1, 2, 3, 4).grownBy(1)).objEq(new Rect2(0, 1, 5, 6));
|
159
|
+
|
160
|
+
// Shrinking should not result in negative widths/heights and
|
161
|
+
// should adjust x/y appropriately
|
162
|
+
expect(new Rect2(1, 2, 1, 2).grownBy(-1)).objEq(new Rect2(1.5, 3, 0, 0));
|
163
|
+
expect(new Rect2(1, 2, 4, 4).grownBy(-1)).objEq(new Rect2(2, 3, 2, 2));
|
164
|
+
expect(new Rect2(1, 2, 2, 8).grownBy(-2)).objEq(new Rect2(2, 4, 0, 4));
|
165
|
+
});
|
166
|
+
|
133
167
|
describe('should correctly expand to include a given point', () => {
|
134
168
|
it('Growing an empty rectange to include (1, 0)', () => {
|
135
169
|
const originalRect = Rect2.empty;
|
package/src/shapes/Rect2.ts
CHANGED
@@ -21,7 +21,6 @@ export class Rect2 extends Abstract2DShape {
|
|
21
21
|
// topLeft assumes up is -y
|
22
22
|
public readonly topLeft: Point2;
|
23
23
|
public readonly size: Vec2;
|
24
|
-
public readonly bottomRight: Point2;
|
25
24
|
public readonly area: number;
|
26
25
|
|
27
26
|
public constructor(
|
@@ -45,7 +44,6 @@ export class Rect2 extends Abstract2DShape {
|
|
45
44
|
// Precompute/store vector forms.
|
46
45
|
this.topLeft = Vec2.of(this.x, this.y);
|
47
46
|
this.size = Vec2.of(this.w, this.h);
|
48
|
-
this.bottomRight = this.topLeft.plus(this.size);
|
49
47
|
this.area = this.w * this.h;
|
50
48
|
}
|
51
49
|
|
@@ -65,8 +63,8 @@ export class Rect2 extends Abstract2DShape {
|
|
65
63
|
|
66
64
|
public containsRect(other: Rect2): boolean {
|
67
65
|
return this.x <= other.x && this.y <= other.y
|
68
|
-
&& this.
|
69
|
-
&& this.
|
66
|
+
&& this.x + this.w >= other.x + other.w
|
67
|
+
&& this.y + this.h >= other.y + other.h;
|
70
68
|
}
|
71
69
|
|
72
70
|
public intersects(other: Rect2): boolean {
|
@@ -159,6 +157,17 @@ export class Rect2 extends Abstract2DShape {
|
|
159
157
|
return this;
|
160
158
|
}
|
161
159
|
|
160
|
+
// Prevent width/height from being negative
|
161
|
+
if (margin < 0) {
|
162
|
+
const xMargin = -Math.min(-margin, this.w / 2);
|
163
|
+
const yMargin = -Math.min(-margin, this.h / 2);
|
164
|
+
|
165
|
+
return new Rect2(
|
166
|
+
this.x - xMargin, this.y - yMargin,
|
167
|
+
this.w + xMargin * 2, this.h + yMargin * 2,
|
168
|
+
);
|
169
|
+
}
|
170
|
+
|
162
171
|
return new Rect2(
|
163
172
|
this.x - margin, this.y - margin, this.w + margin * 2, this.h + margin * 2
|
164
173
|
);
|
@@ -194,6 +203,10 @@ export class Rect2 extends Abstract2DShape {
|
|
194
203
|
return Math.max(this.w, this.h);
|
195
204
|
}
|
196
205
|
|
206
|
+
public get bottomRight() {
|
207
|
+
return this.topLeft.plus(this.size);
|
208
|
+
}
|
209
|
+
|
197
210
|
public get topRight() {
|
198
211
|
return this.bottomRight.plus(Vec2.of(0, -this.h));
|
199
212
|
}
|
@@ -315,17 +328,17 @@ export class Rect2 extends Abstract2DShape {
|
|
315
328
|
}
|
316
329
|
|
317
330
|
const firstRect = rects[0];
|
318
|
-
let minX: number = firstRect.
|
319
|
-
let minY: number = firstRect.
|
320
|
-
let maxX: number = firstRect.
|
321
|
-
let maxY: number = firstRect.
|
331
|
+
let minX: number = firstRect.x;
|
332
|
+
let minY: number = firstRect.y;
|
333
|
+
let maxX: number = firstRect.x + firstRect.w;
|
334
|
+
let maxY: number = firstRect.y + firstRect.h;
|
322
335
|
|
323
336
|
for (let i = 1; i < rects.length; i++) {
|
324
337
|
const rect = rects[i];
|
325
|
-
minX = Math.min(minX, rect.
|
326
|
-
minY = Math.min(minY, rect.
|
327
|
-
maxX = Math.max(maxX, rect.
|
328
|
-
maxY = Math.max(maxY, rect.
|
338
|
+
minX = Math.min(minX, rect.x);
|
339
|
+
minY = Math.min(minY, rect.y);
|
340
|
+
maxX = Math.max(maxX, rect.x + rect.w);
|
341
|
+
maxY = Math.max(maxY, rect.y + rect.h);
|
329
342
|
}
|
330
343
|
|
331
344
|
return new Rect2(
|
File without changes
|
File without changes
|