diff --git a/CHANGELOG.md b/CHANGELOG.md index b65c535c2c6..e490b50d353 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- chore(TS): Parse transform attribute typing [#8878](https://github.com/fabricjs/fabric.js/pull/8878) - chore(TS): Fix typing for DOMParser [#8871](https://github.com/fabricjs/fabric.js/pull/8871) - fix(Path, polyline): fix for SVG import [#8879](https://github.com/fabricjs/fabric.js/pull/8879) - chore(TS) add types for loadSVGFromURl, parseSVGDocument, loadSVGFromString [#8869](https://github.com/fabricjs/fabric.js/pull/8869) diff --git a/src/parser/constants.ts b/src/parser/constants.ts index bae0e8b1855..cc30a53afbe 100644 --- a/src/parser/constants.ts +++ b/src/parser/constants.ts @@ -11,7 +11,7 @@ export const storage = { clipPaths, }; -export const reNum = '(?:[-+]?(?:\\d+|\\d*\\.\\d+)(?:[eE][-+]?\\d+)?)'; +export const reNum = String.raw`(?:[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?)`; export const svgNS = 'http://www.w3.org/2000/svg'; diff --git a/src/parser/parseTransformAttribute.ts b/src/parser/parseTransformAttribute.ts index a2791725763..a5bba2ceb4d 100644 --- a/src/parser/parseTransformAttribute.ts +++ b/src/parser/parseTransformAttribute.ts @@ -1,92 +1,28 @@ -//@ts-nocheck import { iMatrix } from '../constants'; -import { commaWsp, reNum } from './constants'; +import { reNum } from './constants'; import { multiplyTransformMatrices } from '../util/misc/matrix'; import { degreesToRadians } from '../util/misc/radiansDegreesConversion'; import { rotateMatrix } from './rotateMatrix'; import { scaleMatrix } from './scaleMatrix'; import { skewMatrix } from './skewMatrix'; import { translateMatrix } from './translateMatrix'; +import { TMat2D } from '../typedefs'; // == begin transform regexp -const number = reNum, - skewX = '(?:(skewX)\\s*\\(\\s*(' + number + ')\\s*\\))', - skewY = '(?:(skewY)\\s*\\(\\s*(' + number + ')\\s*\\))', - rotate = - '(?:(rotate)\\s*\\(\\s*(' + - number + - ')(?:' + - commaWsp + - '(' + - number + - ')' + - commaWsp + - '(' + - number + - '))?\\s*\\))', - scale = - '(?:(scale)\\s*\\(\\s*(' + - number + - ')(?:' + - commaWsp + - '(' + - number + - '))?\\s*\\))', - translate = - '(?:(translate)\\s*\\(\\s*(' + - number + - ')(?:' + - commaWsp + - '(' + - number + - '))?\\s*\\))', - matrix = - '(?:(matrix)\\s*\\(\\s*' + - '(' + - number + - ')' + - commaWsp + - '(' + - number + - ')' + - commaWsp + - '(' + - number + - ')' + - commaWsp + - '(' + - number + - ')' + - commaWsp + - '(' + - number + - ')' + - commaWsp + - '(' + - number + - ')' + - '\\s*\\))', - transform = - '(?:' + - matrix + - '|' + - translate + - '|' + - scale + - '|' + - rotate + - '|' + - skewX + - '|' + - skewY + - ')', - transforms = - '(?:' + transform + '(?:' + commaWsp + '*' + transform + ')*' + ')', - transformList = '^\\s*(?:' + transforms + '?)\\s*$', - // http://www.w3.org/TR/SVG/coords.html#TransformAttribute - reTransformList = new RegExp(transformList), - // == end transform regexp - reTransform = new RegExp(transform, 'g'); +const p = `(${reNum})`; +const skewX = String.raw`(skewX)\(${p}\)`; +const skewY = String.raw`(skewY)\(${p}\)`; +const rotate = String.raw`(rotate)\(${p}(?: ${p} ${p})?\)`; +const scale = String.raw`(scale)\(${p}(?: ${p})?\)`; +const translate = String.raw`(translate)\(${p}(?: ${p})?\)`; +const matrix = String.raw`(matrix)\(${p} ${p} ${p} ${p} ${p} ${p}\)`; +const transform = `(?:${matrix}|${translate}|${rotate}|${scale}|${skewX}|${skewY})`; +const transforms = `(?:${transform}*)`; +const transformList = String.raw`^\s*(?:${transforms}?)\s*$`; +// http://www.w3.org/TR/SVG/coords.html#TransformAttribute +const reTransformList = new RegExp(transformList); +// == end transform regexp +const reTransform = new RegExp(transform, 'g'); /** * Parses "transform" attribute, returning an array of values @@ -94,12 +30,21 @@ const number = reNum, * @function * @memberOf fabric * @param {String} attributeValue String containing attribute value - * @return {Array} Array of 6 elements representing transformation matrix + * @return {TTransformMatrix} Array of 6 elements representing transformation matrix */ -export function parseTransformAttribute(attributeValue) { +export function parseTransformAttribute(attributeValue: string): TMat2D { + // first we clean the string + attributeValue = attributeValue + .replace(new RegExp(`(${reNum})`, 'gi'), ' $1 ') + // replace annoying commas and arbitrary whitespace with single spaces + .replace(/,/gi, ' ') + .replace(/\s+/gi, ' ') + // remove spaces around front parentheses + .replace(/\s*([()])\s*/gi, '$1'); + // start with identity matrix - let matrix = iMatrix.concat(); - const matrices = []; + let matrix: TMat2D = [...iMatrix]; + const matrices: TMat2D[] = []; // return if no argument was given or // an argument does not match transform attribute regexp @@ -110,13 +55,14 @@ export function parseTransformAttribute(attributeValue) { return matrix; } - attributeValue.replace(reTransform, function (match) { - const m = new RegExp(transform).exec(match).filter(function (match) { - // match !== '' && match != null - return !!match; - }), - operation = m[1], - args = m.slice(2).map(parseFloat); + for (const match of attributeValue.matchAll(reTransform)) { + const transformMatch = new RegExp(transform).exec(match[0]); + if (!transformMatch) { + continue; + } + const matchedParams = transformMatch.filter((m) => !!m); + const operation = matchedParams[1]; + const args = matchedParams.slice(2).map(parseFloat); switch (operation) { case 'translate': @@ -136,15 +82,15 @@ export function parseTransformAttribute(attributeValue) { skewMatrix(matrix, args, 1); break; case 'matrix': - matrix = args; + matrix = args as TMat2D; break; } // snapshot current matrix into matrices array - matrices.push(matrix.concat()); + matrices.push([...matrix]); // reset - matrix = iMatrix.concat(); - }); + matrix = [...iMatrix]; + } let combinedMatrix = matrices[0]; while (matrices.length > 1) { diff --git a/src/parser/rotateMatrix.ts b/src/parser/rotateMatrix.ts index c0dd9cbab4e..e4d0facdef8 100644 --- a/src/parser/rotateMatrix.ts +++ b/src/parser/rotateMatrix.ts @@ -1,8 +1,20 @@ -//@ts-nocheck import { cos } from '../util/misc/cos'; import { sin } from '../util/misc/sin'; +import { TMat2D } from '../typedefs'; -export function rotateMatrix(matrix, args) { +/** + * A rotation matrix + * In the form of + * [cos(a) -sin(a) -xcos(a)+ysin(a)+x] + * [sin(a) cos(a) -xsin(a)-ycos(a)+y] + * [0 0 1 ] + */ +type TMatRotate = [a: number] | [a: number, x: number, y: number]; + +export function rotateMatrix( + matrix: TMat2D, + args: TMatRotate | number[] +): void { const cosValue = cos(args[0]), sinValue = sin(args[0]); let x = 0, diff --git a/src/parser/scaleMatrix.ts b/src/parser/scaleMatrix.ts index 995f4b48df1..4241fa024e0 100644 --- a/src/parser/scaleMatrix.ts +++ b/src/parser/scaleMatrix.ts @@ -1,6 +1,17 @@ -//@ts-nocheck +import { TMat2D } from '../typedefs'; -export function scaleMatrix(matrix, args) { +/** + * A scale matrix + * Takes form + * [x 0 0] + * [0 y 0] + * [0 0 1] + * For more info, see + * @link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform#scale + */ +type TMatScale = [x: number] | [x: number, y: number]; + +export function scaleMatrix(matrix: TMat2D, args: TMatScale | number[]) { const multiplierX = args[0], multiplierY = args.length === 2 ? args[1] : args[0]; diff --git a/src/parser/skewMatrix.ts b/src/parser/skewMatrix.ts index 92686df70d1..58846ab3696 100644 --- a/src/parser/skewMatrix.ts +++ b/src/parser/skewMatrix.ts @@ -1,6 +1,20 @@ -//@ts-nocheck import { degreesToRadians } from '../util/misc/radiansDegreesConversion'; +import { TMat2D } from '../typedefs'; -export function skewMatrix(matrix, args, pos) { +/** + * A matrix in the form + * [0 x 0] + * [y 0 0] + * [0 0 1] + * For more info, see + * @link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform#skewx + */ +type TMatSkew = [xy: number]; + +export function skewMatrix( + matrix: TMat2D, + args: TMatSkew | number[], + pos: 2 | 1 +): void { matrix[pos] = Math.tan(degreesToRadians(args[0])); } diff --git a/src/parser/translateMatrix.ts b/src/parser/translateMatrix.ts index d08a09806ba..2d1265be9a9 100644 --- a/src/parser/translateMatrix.ts +++ b/src/parser/translateMatrix.ts @@ -1,6 +1,23 @@ -//@ts-nocheck +import { TMat2D } from '../typedefs'; -export function translateMatrix(matrix, args) { +/** + * A translation matrix in the form of + * [ 1 0 x ] + * [ 0 1 y ] + * [ 0 0 1 ] + * See @link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform#translate for more details + */ +type TMatTranslate = [x: number] | [x: number, y: number]; + +/** + * Force the translation to be this + * @param matrix + * @param args + */ +export function translateMatrix( + matrix: TMat2D, + args: TMatTranslate | number[] +): void { matrix[4] = args[0]; if (args.length === 2) { matrix[5] = args[1]; diff --git a/src/typedefs.ts b/src/typedefs.ts index 1dbb6b4dd0b..f3358bc522c 100644 --- a/src/typedefs.ts +++ b/src/typedefs.ts @@ -64,7 +64,22 @@ export const enum SupportedSVGUnit { em = 'em', } -export type TMat2D = [number, number, number, number, number, number]; +/** + * A transform matrix. + * Basically a matrix in the form + * [ a c e ] + * [ b d f ] + * [ 0 0 1 ] + * For more details, see @link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform#matrix + */ +export type TMat2D = [ + a: number, + b: number, + c: number, + d: number, + e: number, + f: number +]; /** * An invalid keyword and an empty string will be handled as the `anonymous` keyword. diff --git a/src/util/path/index.ts b/src/util/path/index.ts index a47fe672ba7..daed44a87a2 100644 --- a/src/util/path/index.ts +++ b/src/util/path/index.ts @@ -22,7 +22,8 @@ import { TParsedArcCommand, } from './typedefs'; import { XY, Point } from '../../Point'; -import { numberRegExStr, rePathCommand } from './regex'; +import { rePathCommand } from './regex'; +import { reNum } from '../../parser/constants'; /** * Commands that may be repeated @@ -843,7 +844,7 @@ export const parsePath = (pathString: string): TComplexPathData => { // clean the string // add spaces around the numbers pathString = pathString - .replace(new RegExp(`(${numberRegExStr})`, 'gi'), ' $1 ') + .replace(new RegExp(`(${reNum})`, 'gi'), ' $1 ') // replace annoying commas and arbitrary whitespace with single spaces .replace(/,/gi, ' ') .replace(/\s+/gi, ' '); diff --git a/src/util/path/regex.ts b/src/util/path/regex.ts index 5a1de46209d..ffa16e01b0f 100644 --- a/src/util/path/regex.ts +++ b/src/util/path/regex.ts @@ -1,12 +1,10 @@ -// absolute value number -const absNumberRegExStr = String.raw`(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?`; -export const numberRegExStr = `[-+]?${absNumberRegExStr}`; +import { reNum } from '../../parser/constants'; /** * p for param * using "bad naming" here because it makes the regex much easier to read */ -const p = `(${numberRegExStr})`; +const p = `(${reNum})`; const reMoveToCommand = `(M) (?:${p} ${p} ?)+`;