@emasoft/svg-matrix 1.0.19 → 1.0.21
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/README.md +256 -759
- package/bin/svg-matrix.js +171 -2
- package/bin/svglinter.cjs +1162 -0
- package/package.json +9 -3
- package/src/animation-optimization.js +394 -0
- package/src/animation-references.js +440 -0
- package/src/arc-length.js +940 -0
- package/src/bezier-analysis.js +1626 -0
- package/src/bezier-intersections.js +1369 -0
- package/src/clip-path-resolver.js +110 -2
- package/src/convert-path-data.js +583 -0
- package/src/css-specificity.js +443 -0
- package/src/douglas-peucker.js +356 -0
- package/src/flatten-pipeline.js +109 -4
- package/src/geometry-to-path.js +126 -16
- package/src/gjk-collision.js +840 -0
- package/src/index.js +175 -2
- package/src/off-canvas-detection.js +1222 -0
- package/src/path-analysis.js +1241 -0
- package/src/path-data-plugins.js +928 -0
- package/src/path-optimization.js +825 -0
- package/src/path-simplification.js +1140 -0
- package/src/polygon-clip.js +376 -99
- package/src/svg-boolean-ops.js +898 -0
- package/src/svg-collections.js +910 -0
- package/src/svg-parser.js +175 -16
- package/src/svg-rendering-context.js +627 -0
- package/src/svg-toolbox.js +7495 -0
- package/src/svg-validation-data.js +944 -0
- package/src/transform-decomposition.js +810 -0
- package/src/transform-optimization.js +936 -0
- package/src/use-symbol-resolver.js +75 -7
|
@@ -0,0 +1,936 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform Optimization with Arbitrary Precision and Mathematical Verification
|
|
3
|
+
*
|
|
4
|
+
* Optimizes sequences of 2D affine transformations by merging compatible transforms,
|
|
5
|
+
* removing redundancies, and converting matrices to simpler forms when possible.
|
|
6
|
+
*
|
|
7
|
+
* Guarantees:
|
|
8
|
+
* 1. ARBITRARY PRECISION - All calculations use Decimal.js (80 digits)
|
|
9
|
+
* 2. MATHEMATICAL VERIFICATION - Every optimization verifies that the result is equivalent
|
|
10
|
+
*
|
|
11
|
+
* ## Optimization Strategies
|
|
12
|
+
*
|
|
13
|
+
* 1. **Merge Adjacent Transforms**: Combine consecutive transforms of the same type
|
|
14
|
+
* - translate + translate → single translate
|
|
15
|
+
* - rotate + rotate (same center) → single rotate
|
|
16
|
+
* - scale + scale → single scale
|
|
17
|
+
*
|
|
18
|
+
* 2. **Remove Identity Transforms**: Remove transforms that have no effect
|
|
19
|
+
* - translate(0, 0)
|
|
20
|
+
* - rotate(0) or rotate(2π)
|
|
21
|
+
* - scale(1, 1)
|
|
22
|
+
*
|
|
23
|
+
* 3. **Matrix Simplification**: Convert general matrices to simpler forms
|
|
24
|
+
* - Pure translation matrix → translate()
|
|
25
|
+
* - Pure rotation matrix → rotate()
|
|
26
|
+
* - Pure scale matrix → scale()
|
|
27
|
+
*
|
|
28
|
+
* 4. **Shorthand Notation**: Combine translate-rotate-translate sequences
|
|
29
|
+
* - translate + rotate + translate⁻¹ → rotate(angle, cx, cy)
|
|
30
|
+
*
|
|
31
|
+
* @module transform-optimization
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import Decimal from 'decimal.js';
|
|
35
|
+
import { Matrix } from './matrix.js';
|
|
36
|
+
|
|
37
|
+
// Set high precision for all calculations
|
|
38
|
+
Decimal.set({ precision: 80 });
|
|
39
|
+
|
|
40
|
+
// Helper to convert to Decimal
|
|
41
|
+
const D = x => (x instanceof Decimal ? x : new Decimal(x));
|
|
42
|
+
|
|
43
|
+
// Near-zero threshold for comparisons
|
|
44
|
+
const EPSILON = new Decimal('1e-40');
|
|
45
|
+
|
|
46
|
+
// Verification tolerance (larger than EPSILON for practical use)
|
|
47
|
+
const VERIFICATION_TOLERANCE = new Decimal('1e-30');
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Matrix Utilities (imported patterns)
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Create a 3x3 identity matrix with Decimal values.
|
|
55
|
+
* @returns {Matrix} 3x3 identity matrix
|
|
56
|
+
*/
|
|
57
|
+
export function identityMatrix() {
|
|
58
|
+
return Matrix.identity(3);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create a 2D translation matrix.
|
|
63
|
+
* @param {number|string|Decimal} tx - X translation
|
|
64
|
+
* @param {number|string|Decimal} ty - Y translation
|
|
65
|
+
* @returns {Matrix} 3x3 translation matrix
|
|
66
|
+
*/
|
|
67
|
+
export function translationMatrix(tx, ty) {
|
|
68
|
+
return Matrix.from([
|
|
69
|
+
[1, 0, D(tx)],
|
|
70
|
+
[0, 1, D(ty)],
|
|
71
|
+
[0, 0, 1]
|
|
72
|
+
]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create a 2D rotation matrix.
|
|
77
|
+
* @param {number|string|Decimal} angle - Rotation angle in radians
|
|
78
|
+
* @returns {Matrix} 3x3 rotation matrix
|
|
79
|
+
*/
|
|
80
|
+
export function rotationMatrix(angle) {
|
|
81
|
+
const theta = D(angle);
|
|
82
|
+
const cos = Decimal.cos(theta);
|
|
83
|
+
const sin = Decimal.sin(theta);
|
|
84
|
+
return Matrix.from([
|
|
85
|
+
[cos, sin.neg(), 0],
|
|
86
|
+
[sin, cos, 0],
|
|
87
|
+
[0, 0, 1]
|
|
88
|
+
]);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create a 2D rotation matrix around a specific point.
|
|
93
|
+
* @param {number|string|Decimal} angle - Rotation angle in radians
|
|
94
|
+
* @param {number|string|Decimal} cx - X coordinate of rotation center
|
|
95
|
+
* @param {number|string|Decimal} cy - Y coordinate of rotation center
|
|
96
|
+
* @returns {Matrix} 3x3 rotation matrix around point (cx, cy)
|
|
97
|
+
*/
|
|
98
|
+
export function rotationMatrixAroundPoint(angle, cx, cy) {
|
|
99
|
+
const cxD = D(cx);
|
|
100
|
+
const cyD = D(cy);
|
|
101
|
+
const T1 = translationMatrix(cxD.neg(), cyD.neg());
|
|
102
|
+
const R = rotationMatrix(angle);
|
|
103
|
+
const T2 = translationMatrix(cxD, cyD);
|
|
104
|
+
return T2.mul(R).mul(T1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Create a 2D scale matrix.
|
|
109
|
+
* @param {number|string|Decimal} sx - X scale factor
|
|
110
|
+
* @param {number|string|Decimal} sy - Y scale factor
|
|
111
|
+
* @returns {Matrix} 3x3 scale matrix
|
|
112
|
+
*/
|
|
113
|
+
export function scaleMatrix(sx, sy) {
|
|
114
|
+
return Matrix.from([
|
|
115
|
+
[D(sx), 0, 0],
|
|
116
|
+
[0, D(sy), 0],
|
|
117
|
+
[0, 0, 1]
|
|
118
|
+
]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Calculate the maximum absolute difference between two matrices.
|
|
123
|
+
* @param {Matrix} m1 - First matrix
|
|
124
|
+
* @param {Matrix} m2 - Second matrix
|
|
125
|
+
* @returns {Decimal} Maximum absolute difference
|
|
126
|
+
*/
|
|
127
|
+
export function matrixMaxDifference(m1, m2) {
|
|
128
|
+
let maxDiff = D(0);
|
|
129
|
+
|
|
130
|
+
for (let i = 0; i < m1.rows; i++) {
|
|
131
|
+
for (let j = 0; j < m1.cols; j++) {
|
|
132
|
+
const diff = m1.data[i][j].minus(m2.data[i][j]).abs();
|
|
133
|
+
if (diff.greaterThan(maxDiff)) {
|
|
134
|
+
maxDiff = diff;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return maxDiff;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if two matrices are equal within tolerance.
|
|
144
|
+
* @param {Matrix} m1 - First matrix
|
|
145
|
+
* @param {Matrix} m2 - Second matrix
|
|
146
|
+
* @param {Decimal} [tolerance=VERIFICATION_TOLERANCE] - Maximum allowed difference
|
|
147
|
+
* @returns {boolean} True if matrices are equal within tolerance
|
|
148
|
+
*/
|
|
149
|
+
export function matricesEqual(m1, m2, tolerance = VERIFICATION_TOLERANCE) {
|
|
150
|
+
return matrixMaxDifference(m1, m2).lessThan(D(tolerance));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ============================================================================
|
|
154
|
+
// Transform Merging Functions
|
|
155
|
+
// ============================================================================
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Merge two translation transforms into a single translation.
|
|
159
|
+
*
|
|
160
|
+
* Mathematical formula:
|
|
161
|
+
* translate(a, b) × translate(c, d) = translate(a + c, b + d)
|
|
162
|
+
*
|
|
163
|
+
* VERIFICATION: The result matrix must equal the product of the two input matrices.
|
|
164
|
+
*
|
|
165
|
+
* @param {{tx: number|string|Decimal, ty: number|string|Decimal}} t1 - First translation
|
|
166
|
+
* @param {{tx: number|string|Decimal, ty: number|string|Decimal}} t2 - Second translation
|
|
167
|
+
* @returns {{
|
|
168
|
+
* tx: Decimal,
|
|
169
|
+
* ty: Decimal,
|
|
170
|
+
* verified: boolean,
|
|
171
|
+
* maxError: Decimal
|
|
172
|
+
* }} Merged translation with verification result
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* // Merge translate(5, 10) and translate(3, -2)
|
|
176
|
+
* const result = mergeTranslations({tx: 5, ty: 10}, {tx: 3, ty: -2});
|
|
177
|
+
* // Result: {tx: 8, ty: 8, verified: true}
|
|
178
|
+
*/
|
|
179
|
+
export function mergeTranslations(t1, t2) {
|
|
180
|
+
// Calculate merged translation: sum of components
|
|
181
|
+
const tx = D(t1.tx).plus(D(t2.tx));
|
|
182
|
+
const ty = D(t1.ty).plus(D(t2.ty));
|
|
183
|
+
|
|
184
|
+
// VERIFICATION: Matrix multiplication must give same result
|
|
185
|
+
const M1 = translationMatrix(t1.tx, t1.ty);
|
|
186
|
+
const M2 = translationMatrix(t2.tx, t2.ty);
|
|
187
|
+
const product = M1.mul(M2);
|
|
188
|
+
const merged = translationMatrix(tx, ty);
|
|
189
|
+
|
|
190
|
+
const maxError = matrixMaxDifference(product, merged);
|
|
191
|
+
const verified = maxError.lessThan(VERIFICATION_TOLERANCE);
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
tx,
|
|
195
|
+
ty,
|
|
196
|
+
verified,
|
|
197
|
+
maxError
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Merge two rotation transforms around the origin into a single rotation.
|
|
203
|
+
*
|
|
204
|
+
* Mathematical formula:
|
|
205
|
+
* rotate(a) × rotate(b) = rotate(a + b)
|
|
206
|
+
*
|
|
207
|
+
* Note: This only works for rotations around the SAME point (origin).
|
|
208
|
+
* For rotations around different points, this function should not be used.
|
|
209
|
+
*
|
|
210
|
+
* VERIFICATION: The result matrix must equal the product of the two input matrices.
|
|
211
|
+
*
|
|
212
|
+
* @param {{angle: number|string|Decimal}} r1 - First rotation
|
|
213
|
+
* @param {{angle: number|string|Decimal}} r2 - Second rotation
|
|
214
|
+
* @returns {{
|
|
215
|
+
* angle: Decimal,
|
|
216
|
+
* verified: boolean,
|
|
217
|
+
* maxError: Decimal
|
|
218
|
+
* }} Merged rotation with verification result
|
|
219
|
+
*
|
|
220
|
+
* @example
|
|
221
|
+
* // Merge rotate(π/4) and rotate(π/4)
|
|
222
|
+
* const result = mergeRotations({angle: Math.PI/4}, {angle: Math.PI/4});
|
|
223
|
+
* // Result: {angle: π/2, verified: true}
|
|
224
|
+
*/
|
|
225
|
+
export function mergeRotations(r1, r2) {
|
|
226
|
+
// Calculate merged rotation: sum of angles
|
|
227
|
+
const angle = D(r1.angle).plus(D(r2.angle));
|
|
228
|
+
|
|
229
|
+
// Normalize angle to [-π, π]
|
|
230
|
+
const PI = Decimal.acos(-1);
|
|
231
|
+
const TWO_PI = PI.mul(2);
|
|
232
|
+
let normalizedAngle = angle.mod(TWO_PI);
|
|
233
|
+
if (normalizedAngle.greaterThan(PI)) {
|
|
234
|
+
normalizedAngle = normalizedAngle.minus(TWO_PI);
|
|
235
|
+
} else if (normalizedAngle.lessThan(PI.neg())) {
|
|
236
|
+
normalizedAngle = normalizedAngle.plus(TWO_PI);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// VERIFICATION: Matrix multiplication must give same result
|
|
240
|
+
const M1 = rotationMatrix(r1.angle);
|
|
241
|
+
const M2 = rotationMatrix(r2.angle);
|
|
242
|
+
const product = M1.mul(M2);
|
|
243
|
+
const merged = rotationMatrix(normalizedAngle);
|
|
244
|
+
|
|
245
|
+
const maxError = matrixMaxDifference(product, merged);
|
|
246
|
+
const verified = maxError.lessThan(VERIFICATION_TOLERANCE);
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
angle: normalizedAngle,
|
|
250
|
+
verified,
|
|
251
|
+
maxError
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Merge two scale transforms into a single scale.
|
|
257
|
+
*
|
|
258
|
+
* Mathematical formula:
|
|
259
|
+
* scale(a, b) × scale(c, d) = scale(a × c, b × d)
|
|
260
|
+
*
|
|
261
|
+
* VERIFICATION: The result matrix must equal the product of the two input matrices.
|
|
262
|
+
*
|
|
263
|
+
* @param {{sx: number|string|Decimal, sy: number|string|Decimal}} s1 - First scale
|
|
264
|
+
* @param {{sx: number|string|Decimal, sy: number|string|Decimal}} s2 - Second scale
|
|
265
|
+
* @returns {{
|
|
266
|
+
* sx: Decimal,
|
|
267
|
+
* sy: Decimal,
|
|
268
|
+
* verified: boolean,
|
|
269
|
+
* maxError: Decimal
|
|
270
|
+
* }} Merged scale with verification result
|
|
271
|
+
*
|
|
272
|
+
* @example
|
|
273
|
+
* // Merge scale(2, 3) and scale(1.5, 0.5)
|
|
274
|
+
* const result = mergeScales({sx: 2, sy: 3}, {sx: 1.5, sy: 0.5});
|
|
275
|
+
* // Result: {sx: 3, sy: 1.5, verified: true}
|
|
276
|
+
*/
|
|
277
|
+
export function mergeScales(s1, s2) {
|
|
278
|
+
// Calculate merged scale: product of components
|
|
279
|
+
const sx = D(s1.sx).mul(D(s2.sx));
|
|
280
|
+
const sy = D(s1.sy).mul(D(s2.sy));
|
|
281
|
+
|
|
282
|
+
// VERIFICATION: Matrix multiplication must give same result
|
|
283
|
+
const M1 = scaleMatrix(s1.sx, s1.sy);
|
|
284
|
+
const M2 = scaleMatrix(s2.sx, s2.sy);
|
|
285
|
+
const product = M1.mul(M2);
|
|
286
|
+
const merged = scaleMatrix(sx, sy);
|
|
287
|
+
|
|
288
|
+
const maxError = matrixMaxDifference(product, merged);
|
|
289
|
+
const verified = maxError.lessThan(VERIFICATION_TOLERANCE);
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
sx,
|
|
293
|
+
sy,
|
|
294
|
+
verified,
|
|
295
|
+
maxError
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ============================================================================
|
|
300
|
+
// Matrix to Transform Conversion Functions
|
|
301
|
+
// ============================================================================
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Convert a matrix to a translate transform if it represents a pure translation.
|
|
305
|
+
*
|
|
306
|
+
* A pure translation matrix has the form:
|
|
307
|
+
* [1 0 tx]
|
|
308
|
+
* [0 1 ty]
|
|
309
|
+
* [0 0 1]
|
|
310
|
+
*
|
|
311
|
+
* VERIFICATION: The matrices must be equal.
|
|
312
|
+
*
|
|
313
|
+
* @param {Matrix} matrix - 3x3 transformation matrix
|
|
314
|
+
* @returns {{
|
|
315
|
+
* isTranslation: boolean,
|
|
316
|
+
* tx: Decimal|null,
|
|
317
|
+
* ty: Decimal|null,
|
|
318
|
+
* verified: boolean,
|
|
319
|
+
* maxError: Decimal
|
|
320
|
+
* }} Translation parameters if matrix is pure translation
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* // Check if a matrix is a pure translation
|
|
324
|
+
* const M = translationMatrix(5, 10);
|
|
325
|
+
* const result = matrixToTranslate(M);
|
|
326
|
+
* // Result: {isTranslation: true, tx: 5, ty: 10, verified: true}
|
|
327
|
+
*/
|
|
328
|
+
export function matrixToTranslate(matrix) {
|
|
329
|
+
const data = matrix.data;
|
|
330
|
+
|
|
331
|
+
// Check if linear part is identity
|
|
332
|
+
const isIdentityLinear =
|
|
333
|
+
data[0][0].minus(1).abs().lessThan(EPSILON) &&
|
|
334
|
+
data[0][1].abs().lessThan(EPSILON) &&
|
|
335
|
+
data[1][0].abs().lessThan(EPSILON) &&
|
|
336
|
+
data[1][1].minus(1).abs().lessThan(EPSILON);
|
|
337
|
+
|
|
338
|
+
if (!isIdentityLinear) {
|
|
339
|
+
return {
|
|
340
|
+
isTranslation: false,
|
|
341
|
+
tx: null,
|
|
342
|
+
ty: null,
|
|
343
|
+
verified: false,
|
|
344
|
+
maxError: D(0)
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Extract translation
|
|
349
|
+
const tx = data[0][2];
|
|
350
|
+
const ty = data[1][2];
|
|
351
|
+
|
|
352
|
+
// VERIFICATION: Matrices must be equal
|
|
353
|
+
const reconstructed = translationMatrix(tx, ty);
|
|
354
|
+
const maxError = matrixMaxDifference(matrix, reconstructed);
|
|
355
|
+
const verified = maxError.lessThan(VERIFICATION_TOLERANCE);
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
isTranslation: true,
|
|
359
|
+
tx,
|
|
360
|
+
ty,
|
|
361
|
+
verified,
|
|
362
|
+
maxError
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Convert a matrix to a rotate transform if it represents a pure rotation around origin.
|
|
368
|
+
*
|
|
369
|
+
* A pure rotation matrix has the form:
|
|
370
|
+
* [cos(θ) -sin(θ) 0]
|
|
371
|
+
* [sin(θ) cos(θ) 0]
|
|
372
|
+
* [ 0 0 1]
|
|
373
|
+
*
|
|
374
|
+
* Properties:
|
|
375
|
+
* - Orthogonal columns: a·c + b·d = 0
|
|
376
|
+
* - Unit column lengths: a² + b² = 1, c² + d² = 1
|
|
377
|
+
* - Determinant = 1 (no reflection)
|
|
378
|
+
* - No translation: tx = ty = 0
|
|
379
|
+
*
|
|
380
|
+
* VERIFICATION: The matrices must be equal.
|
|
381
|
+
*
|
|
382
|
+
* @param {Matrix} matrix - 3x3 transformation matrix
|
|
383
|
+
* @returns {{
|
|
384
|
+
* isRotation: boolean,
|
|
385
|
+
* angle: Decimal|null,
|
|
386
|
+
* verified: boolean,
|
|
387
|
+
* maxError: Decimal
|
|
388
|
+
* }} Rotation angle if matrix is pure rotation
|
|
389
|
+
*
|
|
390
|
+
* @example
|
|
391
|
+
* // Check if a matrix is a pure rotation
|
|
392
|
+
* const M = rotationMatrix(Math.PI / 4);
|
|
393
|
+
* const result = matrixToRotate(M);
|
|
394
|
+
* // Result: {isRotation: true, angle: π/4, verified: true}
|
|
395
|
+
*/
|
|
396
|
+
export function matrixToRotate(matrix) {
|
|
397
|
+
const data = matrix.data;
|
|
398
|
+
|
|
399
|
+
// Extract components
|
|
400
|
+
const a = data[0][0];
|
|
401
|
+
const b = data[1][0];
|
|
402
|
+
const c = data[0][1];
|
|
403
|
+
const d = data[1][1];
|
|
404
|
+
const tx = data[0][2];
|
|
405
|
+
const ty = data[1][2];
|
|
406
|
+
|
|
407
|
+
// Check no translation
|
|
408
|
+
if (!tx.abs().lessThan(EPSILON) || !ty.abs().lessThan(EPSILON)) {
|
|
409
|
+
return {
|
|
410
|
+
isRotation: false,
|
|
411
|
+
angle: null,
|
|
412
|
+
verified: false,
|
|
413
|
+
maxError: D(0)
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Check orthogonality: a*c + b*d = 0
|
|
418
|
+
const orthogonal = a.mul(c).plus(b.mul(d)).abs().lessThan(EPSILON);
|
|
419
|
+
|
|
420
|
+
// Check unit columns: a² + b² = 1, c² + d² = 1
|
|
421
|
+
const col1Norm = a.mul(a).plus(b.mul(b));
|
|
422
|
+
const col2Norm = c.mul(c).plus(d.mul(d));
|
|
423
|
+
const unitNorm = col1Norm.minus(1).abs().lessThan(EPSILON) &&
|
|
424
|
+
col2Norm.minus(1).abs().lessThan(EPSILON);
|
|
425
|
+
|
|
426
|
+
// Check determinant = 1 (no reflection)
|
|
427
|
+
const det = a.mul(d).minus(b.mul(c));
|
|
428
|
+
const detOne = det.minus(1).abs().lessThan(EPSILON);
|
|
429
|
+
|
|
430
|
+
if (!orthogonal || !unitNorm || !detOne) {
|
|
431
|
+
return {
|
|
432
|
+
isRotation: false,
|
|
433
|
+
angle: null,
|
|
434
|
+
verified: false,
|
|
435
|
+
maxError: D(0)
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Calculate rotation angle
|
|
440
|
+
const angle = Decimal.atan2(b, a);
|
|
441
|
+
|
|
442
|
+
// VERIFICATION: Matrices must be equal
|
|
443
|
+
const reconstructed = rotationMatrix(angle);
|
|
444
|
+
const maxError = matrixMaxDifference(matrix, reconstructed);
|
|
445
|
+
const verified = maxError.lessThan(VERIFICATION_TOLERANCE);
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
isRotation: true,
|
|
449
|
+
angle,
|
|
450
|
+
verified,
|
|
451
|
+
maxError
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Convert a matrix to a scale transform if it represents a pure scale.
|
|
457
|
+
*
|
|
458
|
+
* A pure scale matrix has the form:
|
|
459
|
+
* [sx 0 0]
|
|
460
|
+
* [0 sy 0]
|
|
461
|
+
* [0 0 1]
|
|
462
|
+
*
|
|
463
|
+
* Properties:
|
|
464
|
+
* - Diagonal matrix in linear part: b = c = 0
|
|
465
|
+
* - No translation: tx = ty = 0
|
|
466
|
+
*
|
|
467
|
+
* VERIFICATION: The matrices must be equal.
|
|
468
|
+
*
|
|
469
|
+
* @param {Matrix} matrix - 3x3 transformation matrix
|
|
470
|
+
* @returns {{
|
|
471
|
+
* isScale: boolean,
|
|
472
|
+
* sx: Decimal|null,
|
|
473
|
+
* sy: Decimal|null,
|
|
474
|
+
* isUniform: boolean,
|
|
475
|
+
* verified: boolean,
|
|
476
|
+
* maxError: Decimal
|
|
477
|
+
* }} Scale factors if matrix is pure scale
|
|
478
|
+
*
|
|
479
|
+
* @example
|
|
480
|
+
* // Check if a matrix is a pure scale
|
|
481
|
+
* const M = scaleMatrix(2, 2);
|
|
482
|
+
* const result = matrixToScale(M);
|
|
483
|
+
* // Result: {isScale: true, sx: 2, sy: 2, isUniform: true, verified: true}
|
|
484
|
+
*/
|
|
485
|
+
export function matrixToScale(matrix) {
|
|
486
|
+
const data = matrix.data;
|
|
487
|
+
|
|
488
|
+
// Extract components
|
|
489
|
+
const a = data[0][0];
|
|
490
|
+
const b = data[1][0];
|
|
491
|
+
const c = data[0][1];
|
|
492
|
+
const d = data[1][1];
|
|
493
|
+
const tx = data[0][2];
|
|
494
|
+
const ty = data[1][2];
|
|
495
|
+
|
|
496
|
+
// Check no translation
|
|
497
|
+
if (!tx.abs().lessThan(EPSILON) || !ty.abs().lessThan(EPSILON)) {
|
|
498
|
+
return {
|
|
499
|
+
isScale: false,
|
|
500
|
+
sx: null,
|
|
501
|
+
sy: null,
|
|
502
|
+
isUniform: false,
|
|
503
|
+
verified: false,
|
|
504
|
+
maxError: D(0)
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Check diagonal: b = 0, c = 0
|
|
509
|
+
if (!b.abs().lessThan(EPSILON) || !c.abs().lessThan(EPSILON)) {
|
|
510
|
+
return {
|
|
511
|
+
isScale: false,
|
|
512
|
+
sx: null,
|
|
513
|
+
sy: null,
|
|
514
|
+
isUniform: false,
|
|
515
|
+
verified: false,
|
|
516
|
+
maxError: D(0)
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Extract scale factors
|
|
521
|
+
const sx = a;
|
|
522
|
+
const sy = d;
|
|
523
|
+
|
|
524
|
+
// Check if uniform
|
|
525
|
+
const isUniform = sx.minus(sy).abs().lessThan(EPSILON);
|
|
526
|
+
|
|
527
|
+
// VERIFICATION: Matrices must be equal
|
|
528
|
+
const reconstructed = scaleMatrix(sx, sy);
|
|
529
|
+
const maxError = matrixMaxDifference(matrix, reconstructed);
|
|
530
|
+
const verified = maxError.lessThan(VERIFICATION_TOLERANCE);
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
isScale: true,
|
|
534
|
+
sx,
|
|
535
|
+
sy,
|
|
536
|
+
isUniform,
|
|
537
|
+
verified,
|
|
538
|
+
maxError
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// ============================================================================
|
|
543
|
+
// Transform List Optimization Functions
|
|
544
|
+
// ============================================================================
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Remove identity transforms from a transform list.
|
|
548
|
+
*
|
|
549
|
+
* Identity transforms are those that have no effect:
|
|
550
|
+
* - translate(0, 0)
|
|
551
|
+
* - rotate(0) or rotate(2πn) for integer n
|
|
552
|
+
* - scale(1, 1)
|
|
553
|
+
*
|
|
554
|
+
* This function does NOT perform verification (it only removes transforms).
|
|
555
|
+
*
|
|
556
|
+
* @param {Array<{type: string, params: Object}>} transforms - Array of transform objects
|
|
557
|
+
* @returns {{
|
|
558
|
+
* transforms: Array<{type: string, params: Object}>,
|
|
559
|
+
* removedCount: number
|
|
560
|
+
* }} Filtered transform list
|
|
561
|
+
*
|
|
562
|
+
* @example
|
|
563
|
+
* // Remove identity transforms
|
|
564
|
+
* const transforms = [
|
|
565
|
+
* {type: 'translate', params: {tx: 5, ty: 10}},
|
|
566
|
+
* {type: 'rotate', params: {angle: 0}},
|
|
567
|
+
* {type: 'scale', params: {sx: 1, sy: 1}},
|
|
568
|
+
* {type: 'translate', params: {tx: 0, ty: 0}}
|
|
569
|
+
* ];
|
|
570
|
+
* const result = removeIdentityTransforms(transforms);
|
|
571
|
+
* // Result: {transforms: [{type: 'translate', params: {tx: 5, ty: 10}}], removedCount: 3}
|
|
572
|
+
*/
|
|
573
|
+
export function removeIdentityTransforms(transforms) {
|
|
574
|
+
const PI = Decimal.acos(-1);
|
|
575
|
+
const TWO_PI = PI.mul(2);
|
|
576
|
+
|
|
577
|
+
const filtered = transforms.filter(t => {
|
|
578
|
+
switch (t.type) {
|
|
579
|
+
case 'translate': {
|
|
580
|
+
const tx = D(t.params.tx);
|
|
581
|
+
const ty = D(t.params.ty);
|
|
582
|
+
return !tx.abs().lessThan(EPSILON) || !ty.abs().lessThan(EPSILON);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
case 'rotate': {
|
|
586
|
+
const angle = D(t.params.angle);
|
|
587
|
+
// Normalize angle to [0, 2π)
|
|
588
|
+
const normalized = angle.mod(TWO_PI);
|
|
589
|
+
return !normalized.abs().lessThan(EPSILON);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
case 'scale': {
|
|
593
|
+
const sx = D(t.params.sx);
|
|
594
|
+
const sy = D(t.params.sy);
|
|
595
|
+
return !sx.minus(1).abs().lessThan(EPSILON) || !sy.minus(1).abs().lessThan(EPSILON);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
case 'matrix': {
|
|
599
|
+
// Check if matrix is identity
|
|
600
|
+
const m = t.params.matrix;
|
|
601
|
+
const identity = identityMatrix();
|
|
602
|
+
return !matricesEqual(m, identity, EPSILON);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
default:
|
|
606
|
+
// Keep unknown transform types
|
|
607
|
+
return true;
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
return {
|
|
612
|
+
transforms: filtered,
|
|
613
|
+
removedCount: transforms.length - filtered.length
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Convert translate-rotate-translate sequence to rotate around point shorthand.
|
|
619
|
+
*
|
|
620
|
+
* Detects the pattern:
|
|
621
|
+
* translate(tx, ty) × rotate(angle) × translate(-tx, -ty)
|
|
622
|
+
*
|
|
623
|
+
* And converts it to:
|
|
624
|
+
* rotate(angle, tx, ty)
|
|
625
|
+
*
|
|
626
|
+
* This is a common optimization for rotating around a point other than the origin.
|
|
627
|
+
*
|
|
628
|
+
* VERIFICATION: The matrices must be equal.
|
|
629
|
+
*
|
|
630
|
+
* @param {number|string|Decimal} translateX - First translation X
|
|
631
|
+
* @param {number|string|Decimal} translateY - First translation Y
|
|
632
|
+
* @param {number|string|Decimal} angle - Rotation angle in radians
|
|
633
|
+
* @param {number|string|Decimal} centerX - Expected rotation center X
|
|
634
|
+
* @param {number|string|Decimal} centerY - Expected rotation center Y
|
|
635
|
+
* @returns {{
|
|
636
|
+
* angle: Decimal,
|
|
637
|
+
* cx: Decimal,
|
|
638
|
+
* cy: Decimal,
|
|
639
|
+
* verified: boolean,
|
|
640
|
+
* maxError: Decimal
|
|
641
|
+
* }} Shorthand rotation parameters with verification
|
|
642
|
+
*
|
|
643
|
+
* @example
|
|
644
|
+
* // Convert translate-rotate-translate to rotate around point
|
|
645
|
+
* const result = shortRotate(100, 50, Math.PI/4, 100, 50);
|
|
646
|
+
* // Result: {angle: π/4, cx: 100, cy: 50, verified: true}
|
|
647
|
+
*/
|
|
648
|
+
export function shortRotate(translateX, translateY, angle, centerX, centerY) {
|
|
649
|
+
const txD = D(translateX);
|
|
650
|
+
const tyD = D(translateY);
|
|
651
|
+
const angleD = D(angle);
|
|
652
|
+
const cxD = D(centerX);
|
|
653
|
+
const cyD = D(centerY);
|
|
654
|
+
|
|
655
|
+
// Build the sequence: T(tx, ty) × R(angle) × T(-tx, -ty)
|
|
656
|
+
const T1 = translationMatrix(txD, tyD);
|
|
657
|
+
const R = rotationMatrix(angleD);
|
|
658
|
+
const T2 = translationMatrix(txD.neg(), tyD.neg());
|
|
659
|
+
const sequence = T1.mul(R).mul(T2);
|
|
660
|
+
|
|
661
|
+
// Build the shorthand: R(angle, cx, cy)
|
|
662
|
+
const shorthand = rotationMatrixAroundPoint(angleD, cxD, cyD);
|
|
663
|
+
|
|
664
|
+
// VERIFICATION: Matrices must be equal
|
|
665
|
+
const maxError = matrixMaxDifference(sequence, shorthand);
|
|
666
|
+
const verified = maxError.lessThan(VERIFICATION_TOLERANCE);
|
|
667
|
+
|
|
668
|
+
return {
|
|
669
|
+
angle: angleD,
|
|
670
|
+
cx: cxD,
|
|
671
|
+
cy: cyD,
|
|
672
|
+
verified,
|
|
673
|
+
maxError
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Optimize a list of transforms by applying all optimization strategies.
|
|
679
|
+
*
|
|
680
|
+
* Optimization strategies applied:
|
|
681
|
+
* 1. Remove identity transforms
|
|
682
|
+
* 2. Merge consecutive transforms of the same type
|
|
683
|
+
* 3. Detect and convert translate-rotate-translate to rotate around point
|
|
684
|
+
* 4. Convert matrices to simpler forms when possible
|
|
685
|
+
*
|
|
686
|
+
* VERIFICATION: The combined matrix of the optimized list must equal the
|
|
687
|
+
* combined matrix of the original list.
|
|
688
|
+
*
|
|
689
|
+
* @param {Array<{type: string, params: Object}>} transforms - Array of transform objects
|
|
690
|
+
* @returns {{
|
|
691
|
+
* transforms: Array<{type: string, params: Object}>,
|
|
692
|
+
* optimizationCount: number,
|
|
693
|
+
* verified: boolean,
|
|
694
|
+
* maxError: Decimal
|
|
695
|
+
* }} Optimized transform list with verification
|
|
696
|
+
*
|
|
697
|
+
* @example
|
|
698
|
+
* // Optimize a transform list
|
|
699
|
+
* const transforms = [
|
|
700
|
+
* {type: 'translate', params: {tx: 5, ty: 10}},
|
|
701
|
+
* {type: 'translate', params: {tx: 3, ty: -2}},
|
|
702
|
+
* {type: 'rotate', params: {angle: 0}},
|
|
703
|
+
* {type: 'scale', params: {sx: 2, sy: 2}},
|
|
704
|
+
* {type: 'scale', params: {sx: 0.5, sy: 0.5}}
|
|
705
|
+
* ];
|
|
706
|
+
* const result = optimizeTransformList(transforms);
|
|
707
|
+
* // Result: optimized list with merged translations and scales, identity rotation removed
|
|
708
|
+
*/
|
|
709
|
+
export function optimizeTransformList(transforms) {
|
|
710
|
+
// Calculate original combined matrix for verification
|
|
711
|
+
let originalMatrix = identityMatrix();
|
|
712
|
+
for (const t of transforms) {
|
|
713
|
+
let m;
|
|
714
|
+
switch (t.type) {
|
|
715
|
+
case 'translate':
|
|
716
|
+
m = translationMatrix(t.params.tx, t.params.ty);
|
|
717
|
+
break;
|
|
718
|
+
case 'rotate':
|
|
719
|
+
if (t.params.cx !== undefined && t.params.cy !== undefined) {
|
|
720
|
+
m = rotationMatrixAroundPoint(t.params.angle, t.params.cx, t.params.cy);
|
|
721
|
+
} else {
|
|
722
|
+
m = rotationMatrix(t.params.angle);
|
|
723
|
+
}
|
|
724
|
+
break;
|
|
725
|
+
case 'scale':
|
|
726
|
+
m = scaleMatrix(t.params.sx, t.params.sy);
|
|
727
|
+
break;
|
|
728
|
+
case 'matrix':
|
|
729
|
+
m = t.params.matrix;
|
|
730
|
+
break;
|
|
731
|
+
default:
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
originalMatrix = originalMatrix.mul(m);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Step 1: Remove identity transforms
|
|
738
|
+
const { transforms: step1, removedCount } = removeIdentityTransforms(transforms);
|
|
739
|
+
let optimized = step1.slice();
|
|
740
|
+
|
|
741
|
+
// Step 2: Merge consecutive transforms of the same type
|
|
742
|
+
let i = 0;
|
|
743
|
+
while (i < optimized.length - 1) {
|
|
744
|
+
const current = optimized[i];
|
|
745
|
+
const next = optimized[i + 1];
|
|
746
|
+
|
|
747
|
+
// Try to merge
|
|
748
|
+
let merged = null;
|
|
749
|
+
|
|
750
|
+
if (current.type === 'translate' && next.type === 'translate') {
|
|
751
|
+
const result = mergeTranslations(current.params, next.params);
|
|
752
|
+
if (result.verified) {
|
|
753
|
+
merged = {
|
|
754
|
+
type: 'translate',
|
|
755
|
+
params: { tx: result.tx, ty: result.ty }
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
} else if (current.type === 'rotate' && next.type === 'rotate') {
|
|
759
|
+
// Only merge if both are around origin
|
|
760
|
+
if (!current.params.cx && !current.params.cy && !next.params.cx && !next.params.cy) {
|
|
761
|
+
const result = mergeRotations(current.params, next.params);
|
|
762
|
+
if (result.verified) {
|
|
763
|
+
merged = {
|
|
764
|
+
type: 'rotate',
|
|
765
|
+
params: { angle: result.angle }
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
} else if (current.type === 'scale' && next.type === 'scale') {
|
|
770
|
+
const result = mergeScales(current.params, next.params);
|
|
771
|
+
if (result.verified) {
|
|
772
|
+
merged = {
|
|
773
|
+
type: 'scale',
|
|
774
|
+
params: { sx: result.sx, sy: result.sy }
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (merged) {
|
|
780
|
+
// Replace current and next with merged
|
|
781
|
+
optimized.splice(i, 2, merged);
|
|
782
|
+
// Don't increment i, check if we can merge again
|
|
783
|
+
} else {
|
|
784
|
+
i++;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Step 3: Detect translate-rotate-translate patterns
|
|
789
|
+
i = 0;
|
|
790
|
+
while (i < optimized.length - 2) {
|
|
791
|
+
const t1 = optimized[i];
|
|
792
|
+
const t2 = optimized[i + 1];
|
|
793
|
+
const t3 = optimized[i + 2];
|
|
794
|
+
|
|
795
|
+
if (t1.type === 'translate' && t2.type === 'rotate' && t3.type === 'translate') {
|
|
796
|
+
// Check if t3 is inverse of t1
|
|
797
|
+
const tx1 = D(t1.params.tx);
|
|
798
|
+
const ty1 = D(t1.params.ty);
|
|
799
|
+
const tx3 = D(t3.params.tx);
|
|
800
|
+
const ty3 = D(t3.params.ty);
|
|
801
|
+
|
|
802
|
+
if (tx1.plus(tx3).abs().lessThan(EPSILON) && ty1.plus(ty3).abs().lessThan(EPSILON)) {
|
|
803
|
+
// This is a rotate around point pattern
|
|
804
|
+
const result = shortRotate(tx1, ty1, t2.params.angle, tx1, ty1);
|
|
805
|
+
if (result.verified) {
|
|
806
|
+
const merged = {
|
|
807
|
+
type: 'rotate',
|
|
808
|
+
params: { angle: result.angle, cx: result.cx, cy: result.cy }
|
|
809
|
+
};
|
|
810
|
+
optimized.splice(i, 3, merged);
|
|
811
|
+
// Don't increment i, might be able to merge more
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
i++;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Step 4: Convert matrices to simpler forms
|
|
821
|
+
for (let j = 0; j < optimized.length; j++) {
|
|
822
|
+
const t = optimized[j];
|
|
823
|
+
if (t.type === 'matrix') {
|
|
824
|
+
const m = t.params.matrix;
|
|
825
|
+
|
|
826
|
+
// Try to convert to simpler forms
|
|
827
|
+
const translateResult = matrixToTranslate(m);
|
|
828
|
+
if (translateResult.isTranslation && translateResult.verified) {
|
|
829
|
+
optimized[j] = {
|
|
830
|
+
type: 'translate',
|
|
831
|
+
params: { tx: translateResult.tx, ty: translateResult.ty }
|
|
832
|
+
};
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const rotateResult = matrixToRotate(m);
|
|
837
|
+
if (rotateResult.isRotation && rotateResult.verified) {
|
|
838
|
+
optimized[j] = {
|
|
839
|
+
type: 'rotate',
|
|
840
|
+
params: { angle: rotateResult.angle }
|
|
841
|
+
};
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const scaleResult = matrixToScale(m);
|
|
846
|
+
if (scaleResult.isScale && scaleResult.verified) {
|
|
847
|
+
optimized[j] = {
|
|
848
|
+
type: 'scale',
|
|
849
|
+
params: { sx: scaleResult.sx, sy: scaleResult.sy }
|
|
850
|
+
};
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Final removal of any new identity transforms created by optimization
|
|
857
|
+
const { transforms: final } = removeIdentityTransforms(optimized);
|
|
858
|
+
|
|
859
|
+
// Calculate optimized combined matrix for verification
|
|
860
|
+
let optimizedMatrix = identityMatrix();
|
|
861
|
+
for (const t of final) {
|
|
862
|
+
let m;
|
|
863
|
+
switch (t.type) {
|
|
864
|
+
case 'translate':
|
|
865
|
+
m = translationMatrix(t.params.tx, t.params.ty);
|
|
866
|
+
break;
|
|
867
|
+
case 'rotate':
|
|
868
|
+
if (t.params.cx !== undefined && t.params.cy !== undefined) {
|
|
869
|
+
m = rotationMatrixAroundPoint(t.params.angle, t.params.cx, t.params.cy);
|
|
870
|
+
} else {
|
|
871
|
+
m = rotationMatrix(t.params.angle);
|
|
872
|
+
}
|
|
873
|
+
break;
|
|
874
|
+
case 'scale':
|
|
875
|
+
m = scaleMatrix(t.params.sx, t.params.sy);
|
|
876
|
+
break;
|
|
877
|
+
case 'matrix':
|
|
878
|
+
m = t.params.matrix;
|
|
879
|
+
break;
|
|
880
|
+
default:
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
optimizedMatrix = optimizedMatrix.mul(m);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// VERIFICATION: Combined matrices must be equal
|
|
887
|
+
const maxError = matrixMaxDifference(originalMatrix, optimizedMatrix);
|
|
888
|
+
const verified = maxError.lessThan(VERIFICATION_TOLERANCE);
|
|
889
|
+
|
|
890
|
+
return {
|
|
891
|
+
transforms: final,
|
|
892
|
+
optimizationCount: transforms.length - final.length,
|
|
893
|
+
verified,
|
|
894
|
+
maxError
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// ============================================================================
|
|
899
|
+
// Exports
|
|
900
|
+
// ============================================================================
|
|
901
|
+
|
|
902
|
+
export {
|
|
903
|
+
EPSILON,
|
|
904
|
+
VERIFICATION_TOLERANCE,
|
|
905
|
+
D
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
export default {
|
|
909
|
+
// Matrix utilities
|
|
910
|
+
identityMatrix,
|
|
911
|
+
translationMatrix,
|
|
912
|
+
rotationMatrix,
|
|
913
|
+
rotationMatrixAroundPoint,
|
|
914
|
+
scaleMatrix,
|
|
915
|
+
matrixMaxDifference,
|
|
916
|
+
matricesEqual,
|
|
917
|
+
|
|
918
|
+
// Transform merging
|
|
919
|
+
mergeTranslations,
|
|
920
|
+
mergeRotations,
|
|
921
|
+
mergeScales,
|
|
922
|
+
|
|
923
|
+
// Matrix to transform conversion
|
|
924
|
+
matrixToTranslate,
|
|
925
|
+
matrixToRotate,
|
|
926
|
+
matrixToScale,
|
|
927
|
+
|
|
928
|
+
// Transform list optimization
|
|
929
|
+
removeIdentityTransforms,
|
|
930
|
+
shortRotate,
|
|
931
|
+
optimizeTransformList,
|
|
932
|
+
|
|
933
|
+
// Constants
|
|
934
|
+
EPSILON,
|
|
935
|
+
VERIFICATION_TOLERANCE
|
|
936
|
+
};
|