@js-draw/math 1.2.2 → 1.3.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/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
|