Files

93 lines
2.7 KiB
TypeScript

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 },
);
});
});