feat: introduce local-first SVG workbench

This commit is contained in:
2026-08-02 16:31:49 +02:00
commit 39d9802daa
97 changed files with 20702 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
import { describe, expect, it } from "vitest";
import { parseSvgSource } from "../../src/document/source-parser";
import { applyToPoint } from "../../src/domain/affine";
import { resolveTransformChain } from "../../src/domain/transform-chain";
function semantic(source: string) {
const result = parseSvgSource(source, 1);
expect(result.semantic).not.toBeNull();
return result.semantic!;
}
describe("ancestor transform chains", () => {
it("composes root, ancestor and local transforms in SVG order", () => {
const document = semantic(
'<svg xmlns="http://www.w3.org/2000/svg" transform="translate(2 3)"><g id="parent" transform="rotate(90)"><path id="child" transform="scale(2 3)" d="M0 0L1 1"/></g></svg>',
);
const child = [...document.nodes.values()].find(
(node) => node.id === "child",
)!;
const chain = resolveTransformChain(document, child.key);
expect(chain.entries).toHaveLength(3);
expect(applyToPoint(chain.matrix, { x: 1, y: 1 })).toMatchObject({
x: -1,
y: 5,
});
const restored = applyToPoint(
chain.inverse!,
applyToPoint(chain.matrix, { x: 7, y: -4 }),
);
expect(restored.x).toBeCloseTo(7);
expect(restored.y).toBeCloseTo(-4);
});
it("refuses deterministic coordinate editing through singular or CSS transforms", () => {
const document = semantic(
'<svg xmlns="http://www.w3.org/2000/svg"><g transform="scale(1 0)"><path id="p" style="transform: rotate(2deg)" d="M0 0L1 1"/></g></svg>',
);
const path = [...document.nodes.values()].find((node) => node.id === "p")!;
const chain = resolveTransformChain(document, path.key);
expect(chain.inverse).toBeNull();
expect(chain.diagnostics).toContainEqual(
expect.stringContaining("CSS transforms"),
);
});
});