diff --git a/packages/skia/src/renderer/__tests__/e2e/Matrix4.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/Matrix4.spec.tsx index b78cc3bb54..af03e1e0cf 100644 --- a/packages/skia/src/renderer/__tests__/e2e/Matrix4.spec.tsx +++ b/packages/skia/src/renderer/__tests__/e2e/Matrix4.spec.tsx @@ -12,6 +12,9 @@ import { toMatrix3, Matrix4, mapPoint3d, + invert4, + scale, + rotateZ, } from "../../../skia/types"; const ckPerspective = (d: number) => [ @@ -272,4 +275,94 @@ describe("Matrix4", () => { const result = mapPoint3d(translationMatrix, point); expect(result).toEqual(expectedResult); }); + + const almostEqual = ( + a: number[] | Matrix4 | readonly [number, number, number], + b: number[] | Matrix4 | readonly [number, number, number], + epsilon = 1e-10 + ) => { + expect(a.length).toBe(b.length); + a.forEach((val, idx) => { + expect(Math.abs(val - b[idx])).toBeLessThan(epsilon); + }); + }; + + const matrixEqual = (a: number[] | Matrix4, b: number[] | Matrix4) => { + expect(a.length).toBe(b.length); + a.forEach((val, idx) => { + // Object.is will distinguish -0 from 0, so we use === instead + const equal = val === b[idx] || (val === 0 && b[idx] === 0); + expect(equal).toBe(true); + }); + }; + + it("should return the identity matrix when inverting the identity matrix", () => { + const identityMatrix = Matrix4(); + const result = invert4(identityMatrix); + matrixEqual(result, identityMatrix); + }); + + it("should correctly invert a translation matrix", () => { + const translationMatrix = translate(100, -50, 25); + const inverse = invert4(translationMatrix); + // Inverse of translation(x,y,z) should be translation(-x,-y,-z) + const expectedInverse = translate(-100, 50, -25); + matrixEqual(inverse, expectedInverse); + }); + + it("should correctly invert a scale matrix", () => { + const scaleMatrix = scale(2, 4, 8); + const inverse = invert4(scaleMatrix); + // Inverse of scale(x,y,z) should be scale(1/x,1/y,1/z) + const expectedInverse = scale(1 / 2, 1 / 4, 1 / 8); + matrixEqual(inverse, expectedInverse); + }); + + it("should return identity matrix for non-invertible matrix", () => { + // A matrix of all zeros is not invertible + const nonInvertibleMatrix = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ] as Matrix4; + const result = invert4(nonInvertibleMatrix); + matrixEqual(result, Matrix4()); + }); + + it("multiplying a matrix by its inverse should give the identity matrix", () => { + // Create a complex transformation matrix + const complexMatrix = processTransform3d([ + { scale: 2 }, + { rotateZ: Math.PI / 4 }, + { translate: [100, 100, 50] as const }, + ]); + + const inverse = invert4(complexMatrix); + const result = multiply4(complexMatrix, inverse); + + // Due to floating point arithmetic, we use almostEqual instead of exact equality + almostEqual(result, Matrix4()); + }); + + it("should correctly transform points when using inverse matrix", () => { + const transformMatrix = translate(100, 100, 100); + const inverse = invert4(transformMatrix); + + const point = [200, 0, 300] as const; + const transformedPoint = mapPoint3d(inverse, point); + const expectedPoint = [100, -100, 200] as const; + + // Using almostEqual for floating point comparison + almostEqual(transformedPoint, expectedPoint); + }); + + it("should maintain inverse relationship for rotations", () => { + const rotationMatrix = rotateZ(Math.PI / 3); // 60 degrees rotation + const inverse = invert4(rotationMatrix); + const point = [100, 100, 0] as const; + + // Transform point forward then backward should give original point + const transformed = mapPoint3d(rotationMatrix, point); + const backTransformed = mapPoint3d(inverse, transformed); + + almostEqual(backTransformed, point); + }); }); diff --git a/packages/skia/src/skia/types/Matrix4.ts b/packages/skia/src/skia/types/Matrix4.ts index f104e346bf..3a128f587a 100644 --- a/packages/skia/src/skia/types/Matrix4.ts +++ b/packages/skia/src/skia/types/Matrix4.ts @@ -370,3 +370,104 @@ export const convertToAffineMatrix = (m4: Matrix4) => { // Returning the 6-element affine transformation matrix return [a, b, c, d, tx, ty]; }; + +/** + * Calculates the determinant of a 3x3 matrix + * @worklet + */ +const det3x3 = ( + a00: number, + a01: number, + a02: number, + a10: number, + a11: number, + a12: number, + a20: number, + a21: number, + a22: number +): number => { + "worklet"; + return ( + a00 * (a11 * a22 - a12 * a21) + + a01 * (a12 * a20 - a10 * a22) + + a02 * (a10 * a21 - a11 * a20) + ); +}; + +/** + * Inverts a 4x4 matrix + * @worklet + * @returns The inverted matrix, or the identity matrix if the input is not invertible + */ +export const invert4 = (m: Matrix4): Matrix4 => { + "worklet"; + + const a00 = m[0], + a01 = m[1], + a02 = m[2], + a03 = m[3]; + const a10 = m[4], + a11 = m[5], + a12 = m[6], + a13 = m[7]; + const a20 = m[8], + a21 = m[9], + a22 = m[10], + a23 = m[11]; + const a30 = m[12], + a31 = m[13], + a32 = m[14], + a33 = m[15]; + + // Calculate cofactors + const b00 = det3x3(a11, a12, a13, a21, a22, a23, a31, a32, a33); + const b01 = -det3x3(a10, a12, a13, a20, a22, a23, a30, a32, a33); + const b02 = det3x3(a10, a11, a13, a20, a21, a23, a30, a31, a33); + const b03 = -det3x3(a10, a11, a12, a20, a21, a22, a30, a31, a32); + + const b10 = -det3x3(a01, a02, a03, a21, a22, a23, a31, a32, a33); + const b11 = det3x3(a00, a02, a03, a20, a22, a23, a30, a32, a33); + const b12 = -det3x3(a00, a01, a03, a20, a21, a23, a30, a31, a33); + const b13 = det3x3(a00, a01, a02, a20, a21, a22, a30, a31, a32); + + const b20 = det3x3(a01, a02, a03, a11, a12, a13, a31, a32, a33); + const b21 = -det3x3(a00, a02, a03, a10, a12, a13, a30, a32, a33); + const b22 = det3x3(a00, a01, a03, a10, a11, a13, a30, a31, a33); + const b23 = -det3x3(a00, a01, a02, a10, a11, a12, a30, a31, a32); + + const b30 = -det3x3(a01, a02, a03, a11, a12, a13, a21, a22, a23); + const b31 = det3x3(a00, a02, a03, a10, a12, a13, a20, a22, a23); + const b32 = -det3x3(a00, a01, a03, a10, a11, a13, a20, a21, a23); + const b33 = det3x3(a00, a01, a02, a10, a11, a12, a20, a21, a22); + + // Calculate determinant + const det = a00 * b00 + a01 * b01 + a02 * b02 + a03 * b03; + + // Check if matrix is invertible + if (Math.abs(det) < 1e-8) { + // Return identity matrix if not invertible + return Matrix4(); + } + + const invDet = 1.0 / det; + + // Calculate inverse matrix + return [ + b00 * invDet, + b10 * invDet, + b20 * invDet, + b30 * invDet, + b01 * invDet, + b11 * invDet, + b21 * invDet, + b31 * invDet, + b02 * invDet, + b12 * invDet, + b22 * invDet, + b32 * invDet, + b03 * invDet, + b13 * invDet, + b23 * invDet, + b33 * invDet, + ] as Matrix4; +};