@emasoft/svg-matrix 1.0.2 → 1.0.4
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 +402 -12
- package/package.json +17 -3
- package/samples/test.svg +39 -0
- package/src/index.js +26 -1
- package/src/matrix.js +263 -35
- package/src/svg-flatten.js +335 -0
- package/src/transforms2d.js +120 -2
- package/src/transforms3d.js +214 -4
- package/src/vector.js +174 -30
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG Transform Flattening Utility
|
|
3
|
+
*
|
|
4
|
+
* Parses SVG transform attributes, builds CTM (Current Transform Matrix) for each element,
|
|
5
|
+
* and can flatten all transforms by applying them directly to coordinates.
|
|
6
|
+
*
|
|
7
|
+
* @module svg-flatten
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import Decimal from 'decimal.js';
|
|
11
|
+
import { Matrix } from './matrix.js';
|
|
12
|
+
import * as Transforms2D from './transforms2d.js';
|
|
13
|
+
|
|
14
|
+
// Set high precision for all calculations
|
|
15
|
+
Decimal.set({ precision: 80 });
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse a single SVG transform function and return a 3x3 matrix.
|
|
19
|
+
* Supports: translate, scale, rotate, skewX, skewY, matrix
|
|
20
|
+
*
|
|
21
|
+
* @param {string} func - Transform function name
|
|
22
|
+
* @param {number[]} args - Numeric arguments
|
|
23
|
+
* @returns {Matrix} 3x3 transformation matrix
|
|
24
|
+
*/
|
|
25
|
+
export function parseTransformFunction(func, args) {
|
|
26
|
+
const D = x => new Decimal(x);
|
|
27
|
+
|
|
28
|
+
switch (func.toLowerCase()) {
|
|
29
|
+
case 'translate': {
|
|
30
|
+
const tx = args[0] || 0;
|
|
31
|
+
const ty = args[1] || 0;
|
|
32
|
+
return Transforms2D.translation(tx, ty);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
case 'scale': {
|
|
36
|
+
const sx = args[0] || 1;
|
|
37
|
+
const sy = args[1] !== undefined ? args[1] : sx;
|
|
38
|
+
return Transforms2D.scale(sx, sy);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
case 'rotate': {
|
|
42
|
+
// SVG rotate is in degrees, can have optional cx, cy
|
|
43
|
+
const angleDeg = args[0] || 0;
|
|
44
|
+
const angleRad = D(angleDeg).mul(D(Math.PI)).div(180);
|
|
45
|
+
|
|
46
|
+
if (args.length >= 3) {
|
|
47
|
+
// rotate(angle, cx, cy) - rotation around point
|
|
48
|
+
const cx = args[1];
|
|
49
|
+
const cy = args[2];
|
|
50
|
+
return Transforms2D.rotateAroundPoint(angleRad, cx, cy);
|
|
51
|
+
}
|
|
52
|
+
return Transforms2D.rotate(angleRad);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case 'skewx': {
|
|
56
|
+
const angleDeg = args[0] || 0;
|
|
57
|
+
const angleRad = D(angleDeg).mul(D(Math.PI)).div(180);
|
|
58
|
+
const tanVal = Decimal.tan(angleRad);
|
|
59
|
+
return Transforms2D.skew(tanVal, 0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
case 'skewy': {
|
|
63
|
+
const angleDeg = args[0] || 0;
|
|
64
|
+
const angleRad = D(angleDeg).mul(D(Math.PI)).div(180);
|
|
65
|
+
const tanVal = Decimal.tan(angleRad);
|
|
66
|
+
return Transforms2D.skew(0, tanVal);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
case 'matrix': {
|
|
70
|
+
// matrix(a, b, c, d, e, f) -> | a c e |
|
|
71
|
+
// | b d f |
|
|
72
|
+
// | 0 0 1 |
|
|
73
|
+
const [a, b, c, d, e, f] = args.map(x => D(x || 0));
|
|
74
|
+
return Matrix.from([
|
|
75
|
+
[a, c, e],
|
|
76
|
+
[b, d, f],
|
|
77
|
+
[D(0), D(0), D(1)]
|
|
78
|
+
]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
default:
|
|
82
|
+
console.warn(`Unknown transform function: ${func}`);
|
|
83
|
+
return Matrix.identity(3);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Parse an SVG transform attribute string into a combined matrix.
|
|
89
|
+
* Handles multiple transforms: "translate(10,20) rotate(45) scale(2)"
|
|
90
|
+
*
|
|
91
|
+
* @param {string} transformStr - SVG transform attribute value
|
|
92
|
+
* @returns {Matrix} Combined 3x3 transformation matrix
|
|
93
|
+
*/
|
|
94
|
+
export function parseTransformAttribute(transformStr) {
|
|
95
|
+
if (!transformStr || transformStr.trim() === '') {
|
|
96
|
+
return Matrix.identity(3);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Regex to match transform functions: name(args)
|
|
100
|
+
const transformRegex = /(\w+)\s*\(([^)]*)\)/g;
|
|
101
|
+
let match;
|
|
102
|
+
let result = Matrix.identity(3);
|
|
103
|
+
|
|
104
|
+
while ((match = transformRegex.exec(transformStr)) !== null) {
|
|
105
|
+
const func = match[1];
|
|
106
|
+
const argsStr = match[2];
|
|
107
|
+
|
|
108
|
+
// Parse arguments (comma or space separated)
|
|
109
|
+
const args = argsStr
|
|
110
|
+
.split(/[\s,]+/)
|
|
111
|
+
.filter(s => s.length > 0)
|
|
112
|
+
.map(s => parseFloat(s));
|
|
113
|
+
|
|
114
|
+
const matrix = parseTransformFunction(func, args);
|
|
115
|
+
// Transforms are applied left-to-right in SVG, so we multiply in order
|
|
116
|
+
result = result.mul(matrix);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Build the CTM (Current Transform Matrix) for an element by walking up its ancestry.
|
|
124
|
+
*
|
|
125
|
+
* @param {Object[]} transformStack - Array of transform strings from root to element
|
|
126
|
+
* @returns {Matrix} Combined CTM as 3x3 matrix
|
|
127
|
+
*/
|
|
128
|
+
export function buildCTM(transformStack) {
|
|
129
|
+
let ctm = Matrix.identity(3);
|
|
130
|
+
|
|
131
|
+
for (const transformStr of transformStack) {
|
|
132
|
+
if (transformStr) {
|
|
133
|
+
const matrix = parseTransformAttribute(transformStr);
|
|
134
|
+
ctm = ctm.mul(matrix);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return ctm;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Apply a CTM to a 2D point.
|
|
143
|
+
*
|
|
144
|
+
* @param {Matrix} ctm - 3x3 transformation matrix
|
|
145
|
+
* @param {number|string|Decimal} x - X coordinate
|
|
146
|
+
* @param {number|string|Decimal} y - Y coordinate
|
|
147
|
+
* @returns {{x: Decimal, y: Decimal}} Transformed coordinates
|
|
148
|
+
*/
|
|
149
|
+
export function applyToPoint(ctm, x, y) {
|
|
150
|
+
const [tx, ty] = Transforms2D.applyTransform(ctm, x, y);
|
|
151
|
+
return { x: tx, y: ty };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Convert a CTM back to SVG matrix() notation.
|
|
156
|
+
*
|
|
157
|
+
* @param {Matrix} ctm - 3x3 transformation matrix
|
|
158
|
+
* @param {number} [precision=6] - Decimal places for output
|
|
159
|
+
* @returns {string} SVG matrix transform string
|
|
160
|
+
*/
|
|
161
|
+
export function toSVGMatrix(ctm, precision = 6) {
|
|
162
|
+
const a = ctm.data[0][0].toFixed(precision);
|
|
163
|
+
const b = ctm.data[1][0].toFixed(precision);
|
|
164
|
+
const c = ctm.data[0][1].toFixed(precision);
|
|
165
|
+
const d = ctm.data[1][1].toFixed(precision);
|
|
166
|
+
const e = ctm.data[0][2].toFixed(precision);
|
|
167
|
+
const f = ctm.data[1][2].toFixed(precision);
|
|
168
|
+
|
|
169
|
+
return `matrix(${a}, ${b}, ${c}, ${d}, ${e}, ${f})`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Check if a matrix is effectively the identity matrix.
|
|
174
|
+
*
|
|
175
|
+
* @param {Matrix} m - 3x3 matrix to check
|
|
176
|
+
* @param {string} [tolerance='1e-10'] - Tolerance for comparison
|
|
177
|
+
* @returns {boolean} True if matrix is identity within tolerance
|
|
178
|
+
*/
|
|
179
|
+
export function isIdentity(m, tolerance = '1e-10') {
|
|
180
|
+
const identity = Matrix.identity(3);
|
|
181
|
+
return m.equals(identity, tolerance);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Transform path data coordinates using a CTM.
|
|
186
|
+
* Handles M, L, C, Q, S, T, A, Z commands (absolute only for now).
|
|
187
|
+
*
|
|
188
|
+
* @param {string} pathData - SVG path d attribute
|
|
189
|
+
* @param {Matrix} ctm - 3x3 transformation matrix
|
|
190
|
+
* @returns {string} Transformed path data
|
|
191
|
+
*/
|
|
192
|
+
export function transformPathData(pathData, ctm) {
|
|
193
|
+
// Simple regex-based path parser for common commands
|
|
194
|
+
const result = [];
|
|
195
|
+
const commandRegex = /([MLHVCSQTAZ])([^MLHVCSQTAZ]*)/gi;
|
|
196
|
+
let match;
|
|
197
|
+
|
|
198
|
+
while ((match = commandRegex.exec(pathData)) !== null) {
|
|
199
|
+
const cmd = match[1];
|
|
200
|
+
const argsStr = match[2].trim();
|
|
201
|
+
const args = argsStr
|
|
202
|
+
.split(/[\s,]+/)
|
|
203
|
+
.filter(s => s.length > 0)
|
|
204
|
+
.map(s => parseFloat(s));
|
|
205
|
+
|
|
206
|
+
const cmdUpper = cmd.toUpperCase();
|
|
207
|
+
|
|
208
|
+
switch (cmdUpper) {
|
|
209
|
+
case 'M':
|
|
210
|
+
case 'L':
|
|
211
|
+
case 'T': {
|
|
212
|
+
// Pairs of coordinates
|
|
213
|
+
const transformed = [];
|
|
214
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
215
|
+
const { x, y } = applyToPoint(ctm, args[i], args[i + 1]);
|
|
216
|
+
transformed.push(x.toFixed(6), y.toFixed(6));
|
|
217
|
+
}
|
|
218
|
+
result.push(cmd + ' ' + transformed.join(' '));
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
case 'H': {
|
|
223
|
+
// Horizontal line - becomes L after transform
|
|
224
|
+
const { x, y } = applyToPoint(ctm, args[0], 0);
|
|
225
|
+
result.push('L ' + x.toFixed(6) + ' ' + y.toFixed(6));
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
case 'V': {
|
|
230
|
+
// Vertical line - becomes L after transform
|
|
231
|
+
const { x, y } = applyToPoint(ctm, 0, args[0]);
|
|
232
|
+
result.push('L ' + x.toFixed(6) + ' ' + y.toFixed(6));
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
case 'C': {
|
|
237
|
+
// Cubic bezier: 3 pairs of coordinates
|
|
238
|
+
const transformed = [];
|
|
239
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
240
|
+
const { x, y } = applyToPoint(ctm, args[i], args[i + 1]);
|
|
241
|
+
transformed.push(x.toFixed(6), y.toFixed(6));
|
|
242
|
+
}
|
|
243
|
+
result.push('C ' + transformed.join(' '));
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
case 'S': {
|
|
248
|
+
// Smooth cubic: 2 pairs of coordinates
|
|
249
|
+
const transformed = [];
|
|
250
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
251
|
+
const { x, y } = applyToPoint(ctm, args[i], args[i + 1]);
|
|
252
|
+
transformed.push(x.toFixed(6), y.toFixed(6));
|
|
253
|
+
}
|
|
254
|
+
result.push('S ' + transformed.join(' '));
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
case 'Q': {
|
|
259
|
+
// Quadratic bezier: 2 pairs of coordinates
|
|
260
|
+
const transformed = [];
|
|
261
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
262
|
+
const { x, y } = applyToPoint(ctm, args[i], args[i + 1]);
|
|
263
|
+
transformed.push(x.toFixed(6), y.toFixed(6));
|
|
264
|
+
}
|
|
265
|
+
result.push('Q ' + transformed.join(' '));
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
case 'A': {
|
|
270
|
+
// Arc: rx ry x-axis-rotation large-arc-flag sweep-flag x y
|
|
271
|
+
// Transform end point, scale radii (approximate for non-uniform scale)
|
|
272
|
+
const transformed = [];
|
|
273
|
+
for (let i = 0; i < args.length; i += 7) {
|
|
274
|
+
const rx = args[i];
|
|
275
|
+
const ry = args[i + 1];
|
|
276
|
+
const rotation = args[i + 2];
|
|
277
|
+
const largeArc = args[i + 3];
|
|
278
|
+
const sweep = args[i + 4];
|
|
279
|
+
const x = args[i + 5];
|
|
280
|
+
const y = args[i + 6];
|
|
281
|
+
|
|
282
|
+
const { x: tx, y: ty } = applyToPoint(ctm, x, y);
|
|
283
|
+
|
|
284
|
+
// Scale radii approximately (doesn't handle rotation correctly for skew)
|
|
285
|
+
const scaleX = ctm.data[0][0].abs().plus(ctm.data[0][1].abs()).div(2);
|
|
286
|
+
const scaleY = ctm.data[1][0].abs().plus(ctm.data[1][1].abs()).div(2);
|
|
287
|
+
|
|
288
|
+
transformed.push(
|
|
289
|
+
(rx * scaleX.toNumber()).toFixed(6),
|
|
290
|
+
(ry * scaleY.toNumber()).toFixed(6),
|
|
291
|
+
rotation,
|
|
292
|
+
largeArc,
|
|
293
|
+
sweep,
|
|
294
|
+
tx.toFixed(6),
|
|
295
|
+
ty.toFixed(6)
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
result.push('A ' + transformed.join(' '));
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
case 'Z': {
|
|
303
|
+
result.push('Z');
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
default:
|
|
308
|
+
// Keep unknown commands as-is
|
|
309
|
+
result.push(cmd + ' ' + argsStr);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return result.join(' ');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Information about precision comparison between float and Decimal.
|
|
318
|
+
*/
|
|
319
|
+
export const PRECISION_INFO = {
|
|
320
|
+
floatError: 0.0143, // Typical error: 10 -> 9.9857
|
|
321
|
+
decimalPrecision: 80,
|
|
322
|
+
typicalRoundTripError: '2e-79',
|
|
323
|
+
improvementFactor: '1.43e+77'
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
export default {
|
|
327
|
+
parseTransformFunction,
|
|
328
|
+
parseTransformAttribute,
|
|
329
|
+
buildCTM,
|
|
330
|
+
applyToPoint,
|
|
331
|
+
toSVGMatrix,
|
|
332
|
+
isIdentity,
|
|
333
|
+
transformPathData,
|
|
334
|
+
PRECISION_INFO
|
|
335
|
+
};
|
package/src/transforms2d.js
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
import Decimal from 'decimal.js';
|
|
2
2
|
import { Matrix } from './matrix.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Helper to convert any numeric input to Decimal.
|
|
6
|
+
* @param {number|string|Decimal} x - The value to convert
|
|
7
|
+
* @returns {Decimal} The Decimal representation
|
|
8
|
+
*/
|
|
3
9
|
const D = x => (x instanceof Decimal ? x : new Decimal(x));
|
|
4
10
|
|
|
11
|
+
/**
|
|
12
|
+
* 2D Affine Transforms using 3x3 homogeneous matrices.
|
|
13
|
+
*
|
|
14
|
+
* All transforms return 3x3 Matrix objects that can be composed via multiplication.
|
|
15
|
+
* Transform composition is right-to-left: T.mul(R).mul(S) applies S first, then R, then T.
|
|
16
|
+
*
|
|
17
|
+
* @module Transforms2D
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create a 2D translation matrix.
|
|
22
|
+
* @param {number|string|Decimal} tx - Translation in X direction
|
|
23
|
+
* @param {number|string|Decimal} ty - Translation in Y direction
|
|
24
|
+
* @returns {Matrix} 3x3 translation matrix
|
|
25
|
+
*/
|
|
5
26
|
export function translation(tx, ty) {
|
|
6
27
|
return Matrix.from([
|
|
7
28
|
[new Decimal(1), new Decimal(0), D(tx)],
|
|
@@ -10,6 +31,12 @@ export function translation(tx, ty) {
|
|
|
10
31
|
]);
|
|
11
32
|
}
|
|
12
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Create a 2D scaling matrix.
|
|
36
|
+
* @param {number|string|Decimal} sx - Scale factor in X direction
|
|
37
|
+
* @param {number|string|Decimal} [sy=sx] - Scale factor in Y direction (defaults to sx for uniform scaling)
|
|
38
|
+
* @returns {Matrix} 3x3 scaling matrix
|
|
39
|
+
*/
|
|
13
40
|
export function scale(sx, sy = null) {
|
|
14
41
|
if (sy === null) sy = sx;
|
|
15
42
|
return Matrix.from([
|
|
@@ -19,6 +46,17 @@ export function scale(sx, sy = null) {
|
|
|
19
46
|
]);
|
|
20
47
|
}
|
|
21
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Create a 2D rotation matrix (counterclockwise around origin).
|
|
51
|
+
*
|
|
52
|
+
* Uses standard rotation matrix:
|
|
53
|
+
* | cos(θ) -sin(θ) 0 |
|
|
54
|
+
* | sin(θ) cos(θ) 0 |
|
|
55
|
+
* | 0 0 1 |
|
|
56
|
+
*
|
|
57
|
+
* @param {number|string|Decimal} theta - Rotation angle in radians
|
|
58
|
+
* @returns {Matrix} 3x3 rotation matrix
|
|
59
|
+
*/
|
|
22
60
|
export function rotate(theta) {
|
|
23
61
|
const t = D(theta);
|
|
24
62
|
const c = new Decimal(Math.cos(t.toNumber()));
|
|
@@ -30,10 +68,32 @@ export function rotate(theta) {
|
|
|
30
68
|
]);
|
|
31
69
|
}
|
|
32
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Create a 2D rotation matrix around a specific point.
|
|
73
|
+
* Equivalent to: translate(px, py) × rotate(theta) × translate(-px, -py)
|
|
74
|
+
*
|
|
75
|
+
* @param {number|string|Decimal} theta - Rotation angle in radians
|
|
76
|
+
* @param {number|string|Decimal} px - X coordinate of rotation center
|
|
77
|
+
* @param {number|string|Decimal} py - Y coordinate of rotation center
|
|
78
|
+
* @returns {Matrix} 3x3 rotation matrix around point (px, py)
|
|
79
|
+
*/
|
|
33
80
|
export function rotateAroundPoint(theta, px, py) {
|
|
34
|
-
|
|
81
|
+
const pxD = D(px);
|
|
82
|
+
const pyD = D(py);
|
|
83
|
+
return translation(pxD, pyD).mul(rotate(theta)).mul(translation(pxD.negated(), pyD.negated()));
|
|
35
84
|
}
|
|
36
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Create a 2D skew (shear) matrix.
|
|
88
|
+
*
|
|
89
|
+
* | 1 ax 0 |
|
|
90
|
+
* | ay 1 0 |
|
|
91
|
+
* | 0 0 1 |
|
|
92
|
+
*
|
|
93
|
+
* @param {number|string|Decimal} ax - Skew factor in X direction (affects X based on Y)
|
|
94
|
+
* @param {number|string|Decimal} ay - Skew factor in Y direction (affects Y based on X)
|
|
95
|
+
* @returns {Matrix} 3x3 skew matrix
|
|
96
|
+
*/
|
|
37
97
|
export function skew(ax, ay) {
|
|
38
98
|
return Matrix.from([
|
|
39
99
|
[new Decimal(1), D(ax), new Decimal(0)],
|
|
@@ -42,6 +102,17 @@ export function skew(ax, ay) {
|
|
|
42
102
|
]);
|
|
43
103
|
}
|
|
44
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Create a stretch matrix along a specified axis direction.
|
|
107
|
+
* Stretches by factor k along the unit vector (ux, uy).
|
|
108
|
+
*
|
|
109
|
+
* The axis should be normalized (ux² + uy² = 1), but this is not enforced.
|
|
110
|
+
*
|
|
111
|
+
* @param {number|string|Decimal} ux - X component of axis direction (unit vector)
|
|
112
|
+
* @param {number|string|Decimal} uy - Y component of axis direction (unit vector)
|
|
113
|
+
* @param {number|string|Decimal} k - Stretch factor along the axis
|
|
114
|
+
* @returns {Matrix} 3x3 stretch matrix
|
|
115
|
+
*/
|
|
45
116
|
export function stretchAlongAxis(ux, uy, k) {
|
|
46
117
|
const uxD = D(ux), uyD = D(uy), kD = D(k);
|
|
47
118
|
const one = new Decimal(1);
|
|
@@ -57,9 +128,56 @@ export function stretchAlongAxis(ux, uy, k) {
|
|
|
57
128
|
]);
|
|
58
129
|
}
|
|
59
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Apply a 2D transform matrix to a point.
|
|
133
|
+
* Uses homogeneous coordinates with perspective division.
|
|
134
|
+
*
|
|
135
|
+
* @param {Matrix} M - 3x3 transformation matrix
|
|
136
|
+
* @param {number|string|Decimal} x - X coordinate of point
|
|
137
|
+
* @param {number|string|Decimal} y - Y coordinate of point
|
|
138
|
+
* @returns {Decimal[]} Transformed point as [x', y'] array of Decimals
|
|
139
|
+
*/
|
|
60
140
|
export function applyTransform(M, x, y) {
|
|
61
141
|
const P = Matrix.from([[D(x)], [D(y)], [new Decimal(1)]]);
|
|
62
142
|
const R = M.mul(P);
|
|
63
143
|
const rx = R.data[0][0], ry = R.data[1][0], rw = R.data[2][0];
|
|
144
|
+
// Perspective division (for affine transforms, rw is always 1)
|
|
64
145
|
return [rx.div(rw), ry.div(rw)];
|
|
65
|
-
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Create a reflection matrix across the X axis (flips Y).
|
|
150
|
+
* @returns {Matrix} 3x3 reflection matrix
|
|
151
|
+
*/
|
|
152
|
+
export function reflectX() {
|
|
153
|
+
return Matrix.from([
|
|
154
|
+
[new Decimal(1), new Decimal(0), new Decimal(0)],
|
|
155
|
+
[new Decimal(0), new Decimal(-1), new Decimal(0)],
|
|
156
|
+
[new Decimal(0), new Decimal(0), new Decimal(1)]
|
|
157
|
+
]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Create a reflection matrix across the Y axis (flips X).
|
|
162
|
+
* @returns {Matrix} 3x3 reflection matrix
|
|
163
|
+
*/
|
|
164
|
+
export function reflectY() {
|
|
165
|
+
return Matrix.from([
|
|
166
|
+
[new Decimal(-1), new Decimal(0), new Decimal(0)],
|
|
167
|
+
[new Decimal(0), new Decimal(1), new Decimal(0)],
|
|
168
|
+
[new Decimal(0), new Decimal(0), new Decimal(1)]
|
|
169
|
+
]);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Create a reflection matrix across the origin (flips both X and Y).
|
|
174
|
+
* Equivalent to rotation by π radians.
|
|
175
|
+
* @returns {Matrix} 3x3 reflection matrix
|
|
176
|
+
*/
|
|
177
|
+
export function reflectOrigin() {
|
|
178
|
+
return Matrix.from([
|
|
179
|
+
[new Decimal(-1), new Decimal(0), new Decimal(0)],
|
|
180
|
+
[new Decimal(0), new Decimal(-1), new Decimal(0)],
|
|
181
|
+
[new Decimal(0), new Decimal(0), new Decimal(1)]
|
|
182
|
+
]);
|
|
183
|
+
}
|