import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import test from "node:test"; import { applyEditedPathToNode, applyEditedPathToNodes, editablePathForNode, isEditablePathNode, normalizedPathDataToSvg } from "../src/ui/pages/CanvasWorkspace/canvas/editablePath.ts"; import { createBooleanShapeGroupNode } from "../src/ui/pages/CanvasWorkspace/canvas/shapeNode.ts"; const shapeNode = { id: "shape-1", type: "frame", title: "Rectangle", x: 10, y: 20, width: 100, height: 60, content: JSON.stringify({ kind: "rectangle", cornerRadius: 0 }), tone: "shape", status: "draft", layerRole: "shape", fillColor: "transparent", strokeColor: "#111827", strokeWidth: 2 }; test("converts shape-tool geometry into commands supported by the path editor", () => { const path = editablePathForNode(shapeNode); assert.match(path, /^M /); assert.match(path, / L /); assert.match(path, / Z$/); assert.doesNotMatch(path, /[AHV]/); }); test("persists an edited shape path and expands its canvas bounds without moving world vertices", () => { const edited = applyEditedPathToNode(shapeNode, [1, -10, 10, 2, 100, 10, 2, 100, 60, 2, -10, 60, 11]); const options = JSON.parse(edited.content); assert.equal(options.kind, "path"); assert.ok(Array.isArray(options.pathData)); assert.equal(edited.x, -3); assert.equal(edited.width, 116); assert.match(editablePathForNode(edited), /^M 3 10/); }); test("discards stale per-boundary stroke geometry after editing a boolean result path", () => { const booleanResult = { ...shapeNode, content: JSON.stringify({ kind: "path", pathData: [1, 0, 0, 2, 1, 0, 2, 1, 1, 2, 0, 1, 11], strokeSegments: [{ pathData: [1, 0, 0, 2, 1, 0], strokeColor: "#7C3AED", strokeWidth: 4, strokeStyle: "solid", opacity: 1 }] }) }; const edited = applyEditedPathToNode(booleanResult, [1, 0, 0, 2, 100, 0, 2, 50, 60, 11]); assert.equal(JSON.parse(edited.content).strokeSegments, undefined); }); test("keeps unmarked Brush strokes outside the vertex editor", () => { const brushSvg = ''; const brushNode = { ...shapeNode, id: "brush-1", type: "image", title: "Brush stroke", width: 100, height: 50, content: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(brushSvg)}`, layerRole: "pen-stroke", strokeColor: "#000000", strokeWidth: 3 }; assert.equal(isEditablePathNode(brushNode), false); assert.equal(editablePathForNode(brushNode), null); assert.equal(applyEditedPathToNode(brushNode, [1, 5, 5, 2, 95, 45]), brushNode); }); test("scales normalized cubic data back into SVG coordinates", () => { assert.equal(normalizedPathDataToSvg([1, 0, 0.5, 5, 0.25, 0, 0.75, 1, 1, 0.5, 11], 200, 100), "M 0 50 C 50 0 150 100 200 50 Z"); }); test("opens a boolean result as an editable path and flattens it on commit", () => { const secondShape = { ...shapeNode, id: "shape-2", x: 60 }; const booleanNode = createBooleanShapeGroupNode({ id: "boolean-1", title: "UNION", operation: "union", nodes: [shapeNode, secondShape] }); const nodes = [ { ...shapeNode, parentId: booleanNode.id }, { ...secondShape, parentId: booleanNode.id }, booleanNode ]; assert.equal(isEditablePathNode(booleanNode), true); assert.match(editablePathForNode(booleanNode), /^M /); const editedNodes = applyEditedPathToNodes(nodes, booleanNode.id, [1, 0, 0, 2, booleanNode.width, 0, 2, booleanNode.width, booleanNode.height, 2, 0, booleanNode.height, 11]); const edited = editedNodes.find((node) => node.id === booleanNode.id); assert.equal(editedNodes.length, 1); assert.equal(edited.layerRole, "shape"); assert.equal(JSON.parse(edited.content).kind, "path"); }); test("registers SVGPathEditor on the project's Leafer 2.2 Path runtime", async () => { await import("@leafer-in/editor"); const core = await import("@leafer-ui/core"); await import("leafer-x-path-editor"); const path = new core.Path({ path: "M 0 0 L 10 10", editable: true }); assert.equal(path.editInner, "SVGPathEditor"); }); test("keeps the Pen tool immediately after Shape in the workspace toolbar", async () => { const source = await readFile(new URL("../src/ui/pages/CanvasWorkspace/canvas/WorkspaceToolbar.tsx", import.meta.url), "utf8"); const workspaceSource = await readFile(new URL("../src/ui/pages/CanvasWorkspace/index.tsx", import.meta.url), "utf8"); const pathEditorSource = await readFile(new URL("../src/ui/pages/CanvasWorkspace/canvas/CanvasPathEditor.tsx", import.meta.url), "utf8"); const styles = await readFile(new URL("../src/ui/styles.css", import.meta.url), "utf8"); const nodePointerSource = workspaceSource.slice(workspaceSource.indexOf("const handleNodePointerDown"), workspaceSource.indexOf("const startPathEditing")); assert.ok(source.indexOf("/); assert.doesNotMatch(pathCommitSource, /setPathEditingNodeId/); assert.match(workspaceSource, /const startPathEditing[\s\S]*?setActiveTool\("path"\);[\s\S]*?setPathEditingNodeId\(node\.id\);/); assert.match(workspaceSource, /onClose=\{handlePathEditorClose\}/); assert.match(nodePointerSource, /if \(activeTool === "path"\) \{\s+if \(isEditablePathNode\(node\) && editablePathForNode\(node\)\) \{/); assert.match(nodePointerSource, /commitPathPenDrawing\(currentPathPenDraft\);/); assert.match(nodePointerSource, /event\.preventDefault\(\);\s+startPathEditing\(node\);\s+return;/); assert.doesNotMatch(nodePointerSource, /!pathPenDraftRef\.current && isEditablePathNode/); assert.match(workspaceSource, /if \(tool !== "path" && pathPenDraftRef\.current\) finishPathPenDrawing\(\);/); assert.match(workspaceSource, /settings=\{activeTool === "path" \? pathPenControlSettings : penSettings\}/); assert.match(workspaceSource, /updateShapeStyle\(pathPenControlNode\.id, pathPenSettingsToShapeStyle\(settings, viewportRef\.current\.k\)\);/); assert.match(pathEditorSource, /const pathRef = useRef\(null\);/); assert.match(pathEditorSource, /pathRef\.current\?\.set\(pathNodeStyle\(node\)\);/); assert.doesNotMatch(workspaceSource, /startPenDrawing\(event, true\)/); assert.match(workspaceSource, /key === "p"\) return void run\(\(\) => chooseCanvasTool\("path"\)\)/); assert.match(workspaceSource, /key === "b"\) return void run\(\(\) => chooseCanvasTool\("pen"\)\)/); assert.match(styles, /\.canvas-stage\.is-path-pen-tool \.canvas-world \*/); assert.match(styles, /\.canvas-stage\.is-path-pen-tool \.canvas-path-editor-overlay \*/); assert.match(styles, /cursor:\s*url\("\/cursors\/pen-tool\.png\?v=4"\) 4 4, default !important;/); assert.match(styles, /\.canvas-stage\.is-path-pen-tool \.pen-tool-controls[\s\S]*?cursor:\s*default !important;/); const controlsRule = styles.match(/\.pen-tool-controls \{[\s\S]*?\n\}/g)?.at(-1) ?? ""; assert.match(controlsRule, /z-index:\s*42;/); });