feat: introduce local-first SVG workbench
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applySourcePatches } from "../../src/document/source-patcher";
|
||||
import { parseSvgSource } from "../../src/document/source-parser";
|
||||
import {
|
||||
composeTransformList,
|
||||
parseTransformList,
|
||||
} from "../../src/domain/affine";
|
||||
import { bakeElementTransform } from "../../src/domain/bake-transform";
|
||||
|
||||
function bake(source: string, id: string) {
|
||||
const parsed = parseSvgSource(source, 1);
|
||||
expect(parsed.semantic).not.toBeNull();
|
||||
const semantic = parsed.semantic!;
|
||||
const node = [...semantic.nodes.values()].find(
|
||||
(candidate) => candidate.id === id,
|
||||
)!;
|
||||
const matrix = composeTransformList(
|
||||
parseTransformList(node.attributes.transform ?? ""),
|
||||
);
|
||||
const result = bakeElementTransform(
|
||||
source,
|
||||
node,
|
||||
matrix,
|
||||
semantic.preferences,
|
||||
);
|
||||
return { result, source: applySourcePatches(source, result.patches) };
|
||||
}
|
||||
|
||||
describe("element transform baking", () => {
|
||||
it("retains lines, polygons, and safe rectangles as native elements", () => {
|
||||
const line = bake(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg"><line id="shape" x1="1" y1="2" x2="3" y2="4" transform="translate(10 20)"/></svg>',
|
||||
"shape",
|
||||
);
|
||||
expect(line.source).toContain(
|
||||
'<line id="shape" x1="11" y1="22" x2="13" y2="24"',
|
||||
);
|
||||
expect(line.source).not.toContain("transform=");
|
||||
expect(line.result.warnings).toEqual([]);
|
||||
|
||||
const polygon = bake(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg"><polygon id="shape" points="0,0 2,0 2,2" transform="scale(2 3)"/></svg>',
|
||||
"shape",
|
||||
);
|
||||
expect(polygon.source).toContain('points="0,0 4,0 4,6"');
|
||||
|
||||
const rectangle = bake(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg"><rect id="shape" x="2" y="3" width="4" height="5" rx="1" transform="translate(1 2) scale(2 3)"/></svg>',
|
||||
"shape",
|
||||
);
|
||||
expect(rectangle.result.convertedToPath).toBe(false);
|
||||
expect(rectangle.source).toContain(
|
||||
'<rect id="shape" x="5" y="11" width="8" height="15" rx="2"',
|
||||
);
|
||||
expect(rectangle.source).toContain('ry="3"');
|
||||
});
|
||||
|
||||
it("converts general rectangles to paths and preserves ordinary attributes", () => {
|
||||
const baked = bake(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg"><rect id="shape" class="important" x="0" y="0" width="10" height="5" rx="2" transform="rotate(30)"/></svg>',
|
||||
"shape",
|
||||
);
|
||||
expect(baked.result).toMatchObject({
|
||||
outputElement: "path",
|
||||
convertedToPath: true,
|
||||
});
|
||||
expect(baked.source).toContain('<path id="shape" class="important"');
|
||||
expect(baked.source).toContain(' d="M ');
|
||||
expect(baked.source).not.toMatch(/\s(?:x|y|width|height|rx|transform)=/u);
|
||||
});
|
||||
|
||||
it("retains uniform circles and converts axis-scaled circles to ellipses", () => {
|
||||
const circle = bake(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg"><circle id="shape" cx="4" cy="5" r="3" transform="rotate(40) scale(2)"/></svg>',
|
||||
"shape",
|
||||
);
|
||||
expect(circle.result.outputElement).toBe("circle");
|
||||
expect(circle.source).toContain('r="6"');
|
||||
|
||||
const ellipse = bake(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg"><circle id="shape" cx="4" cy="5" r="3" transform="scale(2 4)"/></svg>',
|
||||
"shape",
|
||||
);
|
||||
expect(ellipse.result.outputElement).toBe("ellipse");
|
||||
expect(ellipse.source).toContain('<ellipse id="shape"');
|
||||
expect(ellipse.source).toContain('rx="6"');
|
||||
expect(ellipse.source).toContain('ry="12"');
|
||||
expect(ellipse.source).not.toContain(' r="');
|
||||
});
|
||||
|
||||
it("bakes paths with high precision and reports stroke consequences", () => {
|
||||
const baked = bake(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg"><path id="shape" d="M0 0A10 4 20 0 1 20 0" stroke="red" transform="matrix(-2 .5 .25 3 4 8)"/></svg>',
|
||||
"shape",
|
||||
);
|
||||
expect(baked.source).toContain('<path id="shape" d="M 4 8 A ');
|
||||
expect(baked.source).not.toContain("transform=");
|
||||
expect(baked.result.warnings).toContainEqual(
|
||||
expect.stringContaining("stroke-width"),
|
||||
);
|
||||
});
|
||||
|
||||
it("refuses unsupported text baking instead of partially applying", () => {
|
||||
const source =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg"><text id="shape" transform="rotate(2)">Text</text></svg>';
|
||||
expect(() => bake(source, "shape")).toThrow(/not deterministic/u);
|
||||
});
|
||||
|
||||
it("refuses non-SVG numeric spellings instead of coercing geometry", () => {
|
||||
const source =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg"><rect id="shape" width="0x10" height="4" transform="translate(1)"/></svg>';
|
||||
expect(() => bake(source, "shape")).toThrow(/finite width/u);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user