import fc from "fast-check"; import { describe, expect, it } from "vitest"; import { IDENTITY, applyToPoint, composeTransformList, decomposeCanonical, diagnoseTransform, invert, matrixNearlyEqual, multiply, parseTransformList, recomposeCanonical, rotation, scaling, translation, } from "../../src/domain/affine"; describe("SVG affine transforms", () => { it("parses and composes source order according to SVG matrix semantics", () => { const matrix = composeTransformList( parseTransformList("translate(10 20) scale(2)"), ); expect(matrix).toEqual({ a: 2, b: 0, c: 0, d: 2, e: 10, f: 20 }); expect(applyToPoint(matrix, { x: 1, y: 1 })).toEqual({ x: 12, y: 22 }); }); it("inverts and canonically decomposes a reflected shear", () => { const matrix = composeTransformList( parseTransformList("translate(8 -2) rotate(23) scale(3 -2) skewX(14)"), ); expect(matrixNearlyEqual(multiply(matrix, invert(matrix)!), IDENTITY)).toBe( true, ); const decomposition = decomposeCanonical(matrix); expect(decomposition.reflected).toBe(true); expect(matrixNearlyEqual(recomposeCanonical(decomposition), matrix)).toBe( true, ); expect(decomposition.residual).toBeLessThan(1e-10); }); it("rejects malformed arities and diagnoses singular matrices", () => { expect(() => parseTransformList("rotate(10 20)")).toThrow( /expects 1 or 3/u, ); expect(diagnoseTransform("scale(1 0)").diagnostics).toContainEqual( expect.objectContaining({ code: "singular-transform", severity: "error", }), ); }); it("round-trips bounded points through generated nonsingular transform chains", () => { const coordinate = fc .integer({ min: -10_000, max: 10_000 }) .map((value) => value / 10); const nonzeroScale = fc .integer({ min: -100, max: 100 }) .filter((value) => value !== 0) .map((value) => value / 10); fc.assert( fc.property( coordinate, coordinate, fc.integer({ min: -720, max: 720 }), nonzeroScale, nonzeroScale, coordinate, coordinate, (tx, ty, angle, sx, sy, x, y) => { const matrix = multiply( translation(tx, ty), multiply(rotation(angle), scaling(sx, sy)), ); const inverse = invert(matrix); if (!inverse) return false; const recovered = applyToPoint( inverse, applyToPoint(matrix, { x, y }), ); return ( Math.abs(recovered.x - x) < 1e-8 && Math.abs(recovered.y - y) < 1e-8 ); }, ), { numRuns: 250 }, ); }); });