From 0332b062c573b4791308825c0cf9378700c5e3f3 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Tue, 18 Aug 2026 23:09:45 +0300 Subject: [PATCH] refactor(canvas): adopt immutable CanvasKit paths (#554) * refactor(canvas): adopt immutable CanvasKit paths - Upgrade CanvasKit to 0.41.1 across app, Core, CLI, and Vue peers - Build mutable geometry with PathBuilder and detach immutable paths for drawing - Replace in-place strokes, transforms, and boolean operations with immutable results - Update renderer mocks and path construction tests for the new API * test(canvas): model immutable path ownership - Return detached path instances from CanvasKit builder mocks - Assert shadow ring and combined path disposal - Preserve recorded vector operations across ownership transfer * fix(build): align CanvasKit workspace versions --- CHANGELOG.md | 4 + bun.lock | 94 +++++------ package.json | 2 +- packages/cli/package.json | 2 +- packages/core/package.json | 2 +- packages/core/src/canvas/boolean.ts | 59 ++++--- packages/core/src/canvas/fills.ts | 13 +- packages/core/src/canvas/flatten.ts | 10 +- packages/core/src/canvas/labels/draw.ts | 14 +- packages/core/src/canvas/node-edit-overlay.ts | 9 +- .../src/canvas/overlays/auto-layout-hover.ts | 7 +- .../core/src/canvas/overlays/measurement.ts | 7 +- .../core/src/canvas/overlays/selection.ts | 22 +-- packages/core/src/canvas/pen-overlay.ts | 28 ++-- packages/core/src/canvas/scene.ts | 6 +- packages/core/src/canvas/shadows.ts | 150 +++++++++--------- packages/core/src/canvas/shapes.ts | 38 +++-- packages/core/src/canvas/strokes.ts | 37 +++-- packages/core/src/canvas/text/derived.ts | 19 ++- packages/core/src/canvas/text/outlines.ts | 8 +- packages/core/src/vector/centerline.ts | 12 +- packages/core/src/vector/index.ts | 20 +-- packages/vue/package.json | 2 +- tests/engine/render/canvas/boolean.test.ts | 5 +- .../render/canvas/corner-smoothing.test.ts | 17 +- tests/engine/render/canvas/effects/helpers.ts | 34 ++-- .../render/canvas/effects/path-shapes.test.ts | 17 +- tests/engine/render/canvas/text.test.ts | 13 +- tests/engine/vector/path-rendering.test.ts | 34 +++- 29 files changed, 380 insertions(+), 305 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e763b7b35..446a138e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Changed + +- Upgrade CanvasKit to 0.41 and migrate renderer geometry to immutable paths built through `PathBuilder`. + ### Added - Open multiple selected design files in separate tabs. diff --git a/bun.lock b/bun.lock index 234ac84a4..3404d83af 100644 --- a/bun.lock +++ b/bun.lock @@ -49,7 +49,7 @@ "@xmldom/xmldom": "^0.9.11", "ai": "^7.0.66", "aws4fetch": "^1.0.20", - "canvaskit-wasm": "^0.40.0", + "canvaskit-wasm": "^0.41.1", "culori": "^4.0.2", "dedent": "^1.7.2", "es-toolkit": "^1.51.0", @@ -152,7 +152,7 @@ "@open-pencil/mcp": "workspace:*", "@open-pencil/scene-graph": "workspace:*", "agentfmt": "^0.1.3", - "canvaskit-wasm": "^0.40.0", + "canvaskit-wasm": "^0.41.1", "citty": "^0.1.6", }, "devDependencies": { @@ -172,7 +172,7 @@ "@tauri-apps/api": "^2.11.1", "@xmldom/xmldom": "^0.9.11", "acorn": "^8.18.0", - "canvaskit-wasm": "^0.40.0", + "canvaskit-wasm": "^0.41.1", "culori": "^4.0.2", "destr": "^2.0.5", "diff": "^8.0.4", @@ -339,7 +339,7 @@ }, "peerDependencies": { "@open-pencil/core": "workspace:*", - "canvaskit-wasm": "^0.40.0", + "canvaskit-wasm": ">=0.41.1", "vue": "^3.5.41", }, "optionalPeers": [ @@ -366,9 +366,9 @@ "@ai-sdk/deepseek": ["@ai-sdk/deepseek@3.0.28", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ySiafZL/1KQq8NF49eGWmVRYP4ezBjxAwAlMK+hhTV+9ee37LcCxlSIbURe4C4KFq8kYaTFKsTGI2poLFLOu0w=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.52", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.27", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SXUM8jzzuTUJRq+EOgPd5to6DSx0EKslVn+IVZHbUEX6k/3vCPNrvjckbK26HnNxHU/STxm+zTSJteqrO+7Z0w=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.53", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.27", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4ZgwJjv/lGA7ywlD/3oYg6XMkIy1VvVKiSRYB83PyLDApwMGXSF+oT7D5lPYJFhNE5Vr3W2H+SwCFuTsbfgn9w=="], - "@ai-sdk/google": ["@ai-sdk/google@4.0.44", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bmRTDg06jQD+eX8nf214pET9+Oe8O1+lUIRGbWsGXj9IN2UJkpl1O1x7cvtiboyTtKSLvSRdVtItUfSl8sQ2GA=="], + "@ai-sdk/google": ["@ai-sdk/google@4.0.45", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-1oLHhc2twSQzm7O8j/HaRWLfBh+tPjQP55d8yXmrZvvopoH6/P7WaDznVJqjouGSof429lR+tYdINkjvUl+8Qg=="], "@ai-sdk/openai": ["@ai-sdk/openai@4.0.42", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZxDca6jJalYuXrIGVrw6dnkpz1Io9AWy+/b/wVWIbjigHCbd+zWLpPi8NnK0OFU+U3YCpP+KWfUvEnG5pFhltA=="], @@ -378,7 +378,7 @@ "@ai-sdk/valibot": ["@ai-sdk/valibot@3.0.27", "", { "dependencies": { "@ai-sdk/provider-utils": "5.0.27" }, "peerDependencies": { "@valibot/to-json-schema": "^1.3.0", "valibot": "^1.1.0" } }, "sha512-c77tkUqvaivemWbJjjVDCCbSfeRGd/Eg0t0GHQdlsS5GDBog9D7D9Ig/BiTvx0jDnKdGp0qQFs4k4NCP/JILUg=="], - "@ai-sdk/vue": ["@ai-sdk/vue@4.0.66", "", { "dependencies": { "@ai-sdk/provider-utils": "5.0.27", "ai": "7.0.66", "swrv": "^1.2.0" }, "peerDependencies": { "vue": "^3.3.4" } }, "sha512-FS+Ir4GUzyAV1fFVwUe1HrrEIsB8XKZO71IrV5ONhXv86PH2DUpFWKOxngssVcAWHtZeEwL8u/7L2KHtNChKpg=="], + "@ai-sdk/vue": ["@ai-sdk/vue@4.0.67", "", { "dependencies": { "@ai-sdk/provider-utils": "5.0.27", "ai": "7.0.67", "swrv": "^1.2.0" }, "peerDependencies": { "vue": "^3.3.4" } }, "sha512-81vFZPlH8zxeloo8rOJsh/JI4hxT+4P9VyXzJLO5C3vezbH5suniW+4Ch09gTtblN6Zz2Sqe8PRAc9r2IQEOEA=="], "@andrewbranch/untar.js": ["@andrewbranch/untar.js@1.0.4", "", {}, "sha512-pVXSwPsLuw8IGLo2Di0EaOfsk+ntVvpkk942J/sHYIkwvtKUakEcPh7HBgZ6tuimgzKSEHgCvO4XgQ05DEbwDw=="], @@ -584,7 +584,7 @@ "@chainsafe/as-chacha20poly1305": ["@chainsafe/as-chacha20poly1305@0.1.0", "", {}, "sha512-BpNcL8/lji/GM3+vZ/bgRWqJ1q5kwvTFmGPk7pxm/QQZDbaMI98waOHjEymTjq2JmdD/INdNBFOVSyJofXg7ew=="], - "@chainsafe/as-sha256": ["@chainsafe/as-sha256@1.2.4", "", {}, "sha512-3GXDysZOKD6cTYbm48lEdXdUbS7cafjXQZfgHOspTByhoGR/JM3KBXyF3vE6bf63ImjNPyoEZwnQcpYPQ6k3bQ=="], + "@chainsafe/as-sha256": ["@chainsafe/as-sha256@1.2.5", "", {}, "sha512-8LzmWxBC/2O5BgbP+aRom/FaDJMrM04VZMlVIivYTc6yNQKYrONmIv27m3q/1VBgxrfrNNqmibitb2hE2tQzIw=="], "@chainsafe/is-ip": ["@chainsafe/is-ip@2.1.0", "", {}, "sha512-KIjt+6IfysQ4GCv66xihEitBjvhU/bixbbbFxdJ1sqCp4uJ0wuZiYBPhksZoy4lfaF0k9cwNzY5upEW/VWdw3w=="], @@ -804,7 +804,7 @@ "@hono/node-server": ["@hono/node-server@1.19.17", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ=="], - "@iconify-json/lucide": ["@iconify-json/lucide@1.2.123", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-0CozmpKXEOEEhltrfT2zt+/t1hez67Y+hM2VJWoIcBKdBrb83VYuKf+b5A0QU9HfQm+RNn2GjFWlTOwi+Ss6IA=="], + "@iconify-json/lucide": ["@iconify-json/lucide@1.2.124", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-mO6JyBVNER7uXEkW2bXiN9Rrm6w91IXxwnNmIMbu2KZX0ZPcVZH1xDUTw94pryxESUIpBhinkOUIZIav+TA9ng=="], "@iconify-json/simple-icons": ["@iconify-json/simple-icons@1.2.93", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-/XhANjfGYOuqvSR3TmUnkQkINvQ4GVjVuukvymRbxtVFBvIq/yiXJqCDycKcQPT401OYT9H2vIY6ihAlz1QIAw=="], @@ -906,7 +906,7 @@ "@libp2p/identify": ["@libp2p/identify@3.0.36", "", { "dependencies": { "@libp2p/crypto": "^5.1.6", "@libp2p/interface": "^2.10.4", "@libp2p/interface-internal": "^2.3.17", "@libp2p/peer-id": "^5.1.7", "@libp2p/peer-record": "^8.0.33", "@libp2p/utils": "^6.7.0", "@multiformats/multiaddr": "^12.4.4", "@multiformats/multiaddr-matcher": "^1.7.2", "it-drain": "^3.0.9", "it-parallel": "^3.0.11", "it-protobuf-stream": "^2.0.2", "main-event": "^1.0.1", "protons-runtime": "^5.5.0", "uint8arraylist": "^2.4.8", "uint8arrays": "^5.1.0" } }, "sha512-swpgKzZ8SihHeguIEf3LxYlEcD7C9cSB7DE1XGTuCxNXkXksv2ieQTSiJ2xURMRw0nwe0wEGS5vzmshJh3kzrA=="], - "@libp2p/interface": ["@libp2p/interface@2.10.4", "", { "dependencies": { "@multiformats/multiaddr": "^12.4.4", "it-pushable": "^3.2.3", "it-stream-types": "^2.0.2", "main-event": "^1.0.1", "multiformats": "^13.3.6", "progress-events": "^1.0.1", "uint8arraylist": "^2.4.8" } }, "sha512-FX3uujZgjH9bb7mDSNR54j3JzJnF/ngnQH20GQ1wPk5irIeHDvmzRlUj3bJ3hHQmdB2MxLZNT6e39O1es10LFA=="], + "@libp2p/interface": ["@libp2p/interface@2.11.0", "", { "dependencies": { "@multiformats/dns": "^1.0.6", "@multiformats/multiaddr": "^12.4.4", "it-pushable": "^3.2.3", "it-stream-types": "^2.0.2", "main-event": "^1.0.1", "multiformats": "^13.3.6", "progress-events": "^1.0.1", "uint8arraylist": "^2.4.8" } }, "sha512-0MUFKoXWHTQW3oWIgSHApmYMUKWO/Y02+7Hpyp+n3z+geD4Xo2Rku2gYWmxcq+Pyjkz6Q9YjDWz3Yb2SoV2E8Q=="], "@libp2p/interface-internal": ["@libp2p/interface-internal@2.3.19", "", { "dependencies": { "@libp2p/interface": "^2.11.0", "@libp2p/peer-collections": "^6.0.35", "@multiformats/multiaddr": "^12.4.4", "progress-events": "^1.0.1" } }, "sha512-v335EB0i5CaNF+0SqT01CTBp0VyjJizpy46KprcshFFjX16UQ8+/QzoTZqmot9WiAmAzwR0b87oKmlAE9cpxzQ=="], @@ -1002,8 +1002,6 @@ "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@3.0.0", "", { "peerDependencies": { "ai": "^7.0.0", "zod": "^3.25.76 || ^4.1.8" } }, "sha512-m9XTSWoODH2RM5OsZpaGiN7QRR8cdP5paBWq699Tu3JVmGPBKT8xF8XwV0ZBVVsjikD/JgWfak4VSsTR4wAVbg=="], - "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], - "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.137.0", "", { "os": "android", "cpu": "arm" }, "sha512-KDs+0VPdEmasOkpuJHW9V5WCF+cvYdMQv2Jd+aJXt+cxIx12NToRQRbXaRwUEDsZw+/jMk81Ve8ZFbjUkJTOwA=="], "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.137.0", "", { "os": "android", "cpu": "arm64" }, "sha512-WhALNzfy3x/RfC6bsqX+csavuUY0yHHE7XfgPE5M542uhoBZUUoGTPG+nkMbGoG4+gcfss5s7urMyn5QBHu0sw=="], @@ -1330,25 +1328,25 @@ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@storybook/addon-a11y": ["@storybook/addon-a11y@10.5.8", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^10.5.8" } }, "sha512-yunl+sKa29NaUC3I4yjESwk6ED8waEgmmvIJzNAJvdhb7thFzZYYEld68RzKmOVcGBkD3snK5eZ/Xf0l4VWliQ=="], + "@storybook/addon-a11y": ["@storybook/addon-a11y@10.5.9", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^10.5.9" } }, "sha512-MvXkoJIVRcZrqhWi+wynYAmFkvTWm5aHdJfYohhgPeEJVDRF91zejtOsN7zPRE/lQvBDY+IiomKxeRN3UhWNVw=="], - "@storybook/addon-docs": ["@storybook/addon-docs@10.5.8", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/csf-plugin": "10.5.8", "@storybook/icons": "^2.0.2", "@storybook/react-dom-shim": "10.5.8", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.5.8" }, "optionalPeers": ["@types/react"] }, "sha512-NlHiMKW/UvW/uL8HXFDCEVwoH3qZeGYZ/qlWax4d7H471b/T54MBq2KcB4ZrdA785FfIH3numAJdBb5jwn00Mg=="], + "@storybook/addon-docs": ["@storybook/addon-docs@10.5.9", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/csf-plugin": "10.5.9", "@storybook/icons": "^2.0.2", "@storybook/react-dom-shim": "10.5.9", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.5.9" }, "optionalPeers": ["@types/react"] }, "sha512-8sFsMkZYrrdqCLdV+hnwTwDF7RaBsBPRwl4wfc8ve9Q/7Yhi5REe/Xjvd8x1yn6fBPPw9tnID9dx6Agdnr81fw=="], - "@storybook/addon-themes": ["@storybook/addon-themes@10.5.8", "", { "dependencies": { "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.5.8" } }, "sha512-MYTLkN/5t02R0DJikVJUIRWnoyDqDwJprXnagUEFRT5aiO75glG7rY3uikB8n+7vXL/N9fTkgjIxER0SShkvYg=="], + "@storybook/addon-themes": ["@storybook/addon-themes@10.5.9", "", { "dependencies": { "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.5.9" } }, "sha512-/OZ6/4dS9N/N3vDzaQxMBotIPrrn80tLJFd23QDvSYeTyAEomH6x3JnXUwyhvlVhTQcYkhrUCOLdNPYW/wn67w=="], - "@storybook/builder-vite": ["@storybook/builder-vite@10.5.8", "", { "dependencies": { "@storybook/csf-plugin": "10.5.8", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.5.8", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-UeRnn7yT55WmBlHNOQzLrvN7vsHEvVgIukhKDO+4cMbGXN87wZkbxhx6NstpuXRH8OxGqwKS0SZNVp+SC1ftLQ=="], + "@storybook/builder-vite": ["@storybook/builder-vite@10.5.9", "", { "dependencies": { "@storybook/csf-plugin": "10.5.9", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.5.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-Zg4JbGQiHFPGlFJ9HM+XPgzKmU/RFPCymhohVRJhBBYfmgaQgz0flWWzscseCDpl638MNd8/r/H+nwuoBgSYDg=="], - "@storybook/csf-plugin": ["@storybook/csf-plugin@10.5.8", "", { "dependencies": { "unplugin": "^2.3.5" }, "peerDependencies": { "esbuild": "*", "rollup": "*", "storybook": "^10.5.8", "vite": "*", "webpack": "*" }, "optionalPeers": ["esbuild", "rollup", "vite", "webpack"] }, "sha512-/FHiMyOWWEXfwK/lM0WxmkP9GLzbSJJuzGtfeuNWSOVDnvAMbjavitxfHb5wSbWKIQo0XYC1EJ2Y7x91XNYP4w=="], + "@storybook/csf-plugin": ["@storybook/csf-plugin@10.5.9", "", { "dependencies": { "unplugin": "^2.3.5" }, "peerDependencies": { "esbuild": "*", "rollup": "*", "storybook": "^10.5.9", "vite": "*", "webpack": "*" }, "optionalPeers": ["esbuild", "rollup", "vite", "webpack"] }, "sha512-4H5QIHQVtQYCuL43GCRLGjNQhZpQg9gL03ja0DV80kO2Dn9LEt6ol87bSnSjn4VDgcAXtgTzXFvRLknfVgAAqg=="], "@storybook/global": ["@storybook/global@5.0.0", "", {}, "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ=="], "@storybook/icons": ["@storybook/icons@2.1.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Fxh9vYpX9bQqFeHRiY8h2ApeRGDzRSMLwJwNZ/AIRqnyOKHxRKL+yFe+ctEkVJmuptRE9u1Hrn8ZZNHyfDKKNg=="], - "@storybook/react-dom-shim": ["@storybook/react-dom-shim@10.5.8", "", { "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.5.8" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-N8D13/Xny+V3kfe1KBgsAHS0nKWXLLdgOOXS9poKdYzVwVCN+CGEGBxWX0zMMtdCptqa6/57em9coPlZMoO+bg=="], + "@storybook/react-dom-shim": ["@storybook/react-dom-shim@10.5.9", "", { "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.5.9" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7qZD6CSa64p1m/zX9tG4ALcEHlEk0Bx+5++4RL3MFlRCgCFfAomXsCHTzF0RyF8cI4vl+hS7Y0ryjQchVs6UQA=="], - "@storybook/vue3": ["@storybook/vue3@10.5.8", "", { "dependencies": { "@storybook/global": "^5.0.0", "type-fest": "^5.6.0", "vue-component-type-helpers": "^3.2.9" }, "peerDependencies": { "storybook": "^10.5.8", "vue": "^3.0.0" } }, "sha512-R8XZDb4iHjRcfbamYJcss/gTacWXX0K9mVXMBSi+vQNxHFguhLua5yT50UX2WYSW7rXtEZIgnzTldMoBnaLIwQ=="], + "@storybook/vue3": ["@storybook/vue3@10.5.9", "", { "dependencies": { "@storybook/global": "^5.0.0", "type-fest": "^5.6.0", "vue-component-type-helpers": "^3.2.9" }, "peerDependencies": { "storybook": "^10.5.9", "vue": "^3.0.0" } }, "sha512-4VXkJmVRTntxIcIIBEOmWzWUy9ecwCap2Cnt13ioffCGwT6Yzk0fOW4niXSJFX7fO2b13Dtn747Nep7kH9TFqg=="], - "@storybook/vue3-vite": ["@storybook/vue3-vite@10.5.8", "", { "dependencies": { "@storybook/builder-vite": "10.5.8", "@storybook/vue3": "10.5.8", "magic-string": "^0.30.0", "typescript": "^5.9.3", "vue-component-meta": "^3.2.7", "vue-docgen-api": "^4.75.1" }, "peerDependencies": { "storybook": "^10.5.8", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-TdChq6KWgrT/ohK9idiV9K5JTQKY7IKBeFvtprdCnsjwrdk26VQwT7LZRrj7V8DsBKKrrbACUOdvJkYaOZOWrA=="], + "@storybook/vue3-vite": ["@storybook/vue3-vite@10.5.9", "", { "dependencies": { "@storybook/builder-vite": "10.5.9", "@storybook/vue3": "10.5.9", "magic-string": "^0.30.0", "typescript": "^5.9.3", "vue-component-meta": "^3.2.7", "vue-docgen-api": "^4.75.1" }, "peerDependencies": { "storybook": "^10.5.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-RzuKgQSkTnXhy9UsguYZOmDT36uyejmwYEEitqXs8rKlR2fW7XZqP4Ur0FfT0IYQUX6RE3f0oBJWtDvd5WG5aQ=="], "@stream-markdown/code": ["@stream-markdown/code@1.0.4", "", { "dependencies": { "@stream-markdown/core": "1.0.4", "shiki": "^4.0.2" } }, "sha512-hxyNhHbPayz/FWjcH8HVqWmzOEUALEByGj7rpWY3mOqHcdKpETGBAs1FH7RJ/SFMLwO8wr/Ps8dxxGfzPjTtoA=="], @@ -1406,11 +1404,11 @@ "@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="], - "@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.7", "", {}, "sha512-bp+v10y65sp2H7WpWfIMyxTNfl8ZVfxFTLRjPIFRryi6FV/J33z4IS53WO4pTk36KlvJ4iLiQz+oaydDC1xbcA=="], + "@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.8", "", {}, "sha512-BfEvehNpOT75r5Ksc5xW6NZuXujTfb7nlSEyVu4XHG3gdxNg1KqXruWbDewXOUaUYIo4oRbSfkjIajz4MAT8tA=="], "@tanstack/vue-table": ["@tanstack/vue-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "vue": ">=3.2" } }, "sha512-rusRyd77c5tDPloPskctMyPLFEQUeBzxdQ+2Eow4F7gDPlPOB1UnnhzfpdvqZ8ZyX2rRNGmqNnQWm87OI2OQPw=="], - "@tanstack/vue-virtual": ["@tanstack/vue-virtual@3.13.35", "", { "dependencies": { "@tanstack/virtual-core": "3.17.7" }, "peerDependencies": { "vue": "^2.7.0 || ^3.0.0" } }, "sha512-lOfSPvgPdlaH6Qy+CyIc3XpycitaSQ9GECndGpTuDiu+uDA1am+90yWXwzDSd/20ZM196ggWJLS+Qb6WjVd/OA=="], + "@tanstack/vue-virtual": ["@tanstack/vue-virtual@3.13.36", "", { "dependencies": { "@tanstack/virtual-core": "3.17.8" }, "peerDependencies": { "vue": "^2.7.0 || ^3.0.0" } }, "sha512-gKpExv4RbB9luVG+SucTXoqPZv/gzu/Yvz6BNO+8kpNxJ2x+I/ulryzl5W9BRciahZGp5Tls3Dp5XP1ztVGbMw=="], "@tauri-apps/api": ["@tauri-apps/api@2.11.1", "", {}, "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA=="], @@ -1456,7 +1454,7 @@ "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="], - "@testing-library/user-event": ["@testing-library/user-event@14.6.4", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-QCGwP6QrjypBLwyj5cuyfVamkaIEy/XGY+1VDehbtbQqOggYmTFpFOdWR5mPz14vX8vXLMVjDHlRNBcClyO9ew=="], + "@testing-library/user-event": ["@testing-library/user-event@14.6.5", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-FhqjldLTpteueBaKflhNFlMT3+PM0O5fiBUivht6b9CZ1eesJyy7+g3Jr7XwJzt/Hip3ZG5hWwK1MX1FuDiE4w=="], "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="], @@ -1562,7 +1560,7 @@ "@types/lodash.debounce": ["@types/lodash.debounce@4.0.9", "", { "dependencies": { "@types/lodash": "*" } }, "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ=="], - "@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="], + "@types/markdown-it": ["@types/markdown-it@14.2.0", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ=="], "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], @@ -1662,7 +1660,7 @@ "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], - "@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="], + "@vitest/utils": ["@vitest/utils@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ=="], "@volar/language-core": ["@volar/language-core@2.4.28", "", { "dependencies": { "@volar/source-map": "2.4.28" } }, "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ=="], @@ -1820,7 +1818,7 @@ "@yuku-toolchain/types": ["@yuku-toolchain/types@0.8.7", "", {}, "sha512-2Z53dNxAJL6UvFoIrDZvYf3zlO8s4VJK4O2hhaB4mXVwwpX/7ajtss3cmfqKvamlNLWyt9FSWs4eoYdlbxpnHA=="], - "@zip.js/zip.js": ["@zip.js/zip.js@2.8.51", "", {}, "sha512-xCEMRBwBh6JPMQKy5D/RE0Ra9NJvrvjcCHNXo+NzMYCdZy3R0oAX9zJKRb1c393HNMbLYfoD2MRSPgB8L6Jskw=="], + "@zip.js/zip.js": ["@zip.js/zip.js@2.8.52", "", {}, "sha512-ZVXpmXRROrjz/0Glm9homDpSUAiavgpdnR+0tGlTtvtIsqs/d7f7qqbkOxrRKfUaNML68YXb4reIA3vZjul3uQ=="], "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], @@ -1834,7 +1832,7 @@ "agentfmt": ["agentfmt@0.1.3", "", { "dependencies": { "picocolors": "^1.1.1" } }, "sha512-BVSZr8ovigxjYewuBx42Ep4RJj00HURaQYUQ+id41sgb7r0PMUSEWsnD/+h96xj6t6v2WVyPuGPH0K6RtjftKQ=="], - "ai": ["ai@7.0.66", "", { "dependencies": { "@ai-sdk/gateway": "4.0.52", "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wBUyoCYF3GVr+62nelBgR8YbpTSsMZrzFyOOjiwijylNSM2TFCW35C+Pml2vc59/WLMpyhS/LWZ55M+B9DAcSg=="], + "ai": ["ai@7.0.67", "", { "dependencies": { "@ai-sdk/gateway": "4.0.53", "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-83Y/OFx7rylrrgzCsaDPlGeL3QC8qrFQ51LBs6MXm2JVxQLLkrvKmPa1/hOzgyCHM8dsB/2rE1a6OGPNRpmTLQ=="], "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], @@ -1988,7 +1986,7 @@ "caniuse-lite": ["caniuse-lite@1.0.30001809", "", {}, "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ=="], - "canvaskit-wasm": ["canvaskit-wasm@0.40.0", "", { "dependencies": { "@webgpu/types": "0.1.21" } }, "sha512-Od2o+ZmoEw9PBdN/yCGvzfu0WVqlufBPEWNG452wY7E9aT8RBE+ChpZF526doOlg7zumO4iCS+RAeht4P0Gbpw=="], + "canvaskit-wasm": ["canvaskit-wasm@0.41.1", "", { "dependencies": { "@webgpu/types": "0.1.21" } }, "sha512-gO2rNMIE0lOQ8MaarM4qNeq2zZAEknAObKP2XAKidMSAY8P5sURGdFkrx1TSdHuBFqTf5w35JqjuUzjibqkD5g=="], "canvg": ["canvg@3.0.11", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@types/raf": "^3.4.0", "core-js": "^3.8.3", "raf": "^3.4.1", "regenerator-runtime": "^0.13.7", "rgbcolor": "^1.0.1", "stackblur-canvas": "^2.0.0", "svg-pathdata": "^6.0.3" } }, "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA=="], @@ -2304,7 +2302,7 @@ "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], - "electron-to-chromium": ["electron-to-chromium@1.5.408", "", {}, "sha512-SLoprcYpJ/OH2v2ps0+N5biv9H4/KBT3+YmmDew64TwK5y9j2wv7pMOFY7IorVkyMtEyLSCRlXKLsNlakeAlPw=="], + "electron-to-chromium": ["electron-to-chromium@1.5.409", "", {}, "sha512-ChI4N44d0B4A6C8prnNjMOaGgE59fUyEVYcRYm2XEXIjMbbvF5i9UL1cblDbpGqiU0uS8FE8UcKxqZqTXdmzbQ=="], "elkjs": ["elkjs@0.11.1", "", {}, "sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg=="], @@ -2582,7 +2580,7 @@ "highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="], - "hono": ["hono@4.13.2", "", {}, "sha512-JydRilDRkYBQMt9qR9U92mXxmbGqsqSn/IKOrh4e7/gEbn+0zSr8igTu0obwJoNGN4sez28DIql7FBHWydoJpA=="], + "hono": ["hono@4.13.3", "", {}, "sha512-r8AO2mYHoLxSHkgafNeC/BXyb2vWRxD3jem4Ts+ptav8oTG5FIRifAjuJEmZI4bSvvc2ns0GxmIYiZnHqN3mMw=="], "hookable": ["hookable@6.1.1", "", {}, "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ=="], @@ -3630,7 +3628,7 @@ "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], - "storybook": ["storybook@10.5.8", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0 || ^0.28.0", "jsonc-parser": "^3.3.1", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.21.1" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15 || ^0.2.0" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-rR4oFMSiWBSqI0lvsJPtcQUPj8+hzj3TkLu+Mw61Wo6YxPSb5FsLSHai0jZnuaIdKIlmu25KCfwlSQl4e1uvnA=="], + "storybook": ["storybook@10.5.9", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0 || ^0.28.0", "jsonc-parser": "^3.3.1", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.21.1" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15 || ^0.2.0" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-UfdMKSjEhIKr8LbqYyIE5r7vT/drL/PxN75YaouJ+UG0FssEy6cf49OdTF3kstAqVMHskc+zEqyRoiQHZXHwgA=="], "stream-buffers": ["stream-buffers@3.0.3", "", {}, "sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw=="], @@ -3886,7 +3884,7 @@ "utrie": ["utrie@1.0.2", "", { "dependencies": { "base64-arraybuffer": "^1.0.2" } }, "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw=="], - "uuid": ["uuid@14.0.1", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew=="], + "uuid": ["uuid@14.0.2", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ=="], "valibot": ["valibot@1.4.2", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg=="], @@ -4128,22 +4126,6 @@ "@libp2p/crypto/uint8arrays": ["uint8arrays@6.1.1", "", { "dependencies": { "multiformats": "^14.0.0" } }, "sha512-iz7JN0XCSZYA111lhFG2Ui9EhFvTNekqSRHw3lvMHq+dzwWy1OQftxFQREEh4rffU0oSoXdQHsk2TiHKVm4fsA=="], - "@libp2p/interface-internal/@libp2p/interface": ["@libp2p/interface@2.11.0", "", { "dependencies": { "@multiformats/dns": "^1.0.6", "@multiformats/multiaddr": "^12.4.4", "it-pushable": "^3.2.3", "it-stream-types": "^2.0.2", "main-event": "^1.0.1", "multiformats": "^13.3.6", "progress-events": "^1.0.1", "uint8arraylist": "^2.4.8" } }, "sha512-0MUFKoXWHTQW3oWIgSHApmYMUKWO/Y02+7Hpyp+n3z+geD4Xo2Rku2gYWmxcq+Pyjkz6Q9YjDWz3Yb2SoV2E8Q=="], - - "@libp2p/logger/@libp2p/interface": ["@libp2p/interface@2.11.0", "", { "dependencies": { "@multiformats/dns": "^1.0.6", "@multiformats/multiaddr": "^12.4.4", "it-pushable": "^3.2.3", "it-stream-types": "^2.0.2", "main-event": "^1.0.1", "multiformats": "^13.3.6", "progress-events": "^1.0.1", "uint8arraylist": "^2.4.8" } }, "sha512-0MUFKoXWHTQW3oWIgSHApmYMUKWO/Y02+7Hpyp+n3z+geD4Xo2Rku2gYWmxcq+Pyjkz6Q9YjDWz3Yb2SoV2E8Q=="], - - "@libp2p/multistream-select/@libp2p/interface": ["@libp2p/interface@2.11.0", "", { "dependencies": { "@multiformats/dns": "^1.0.6", "@multiformats/multiaddr": "^12.4.4", "it-pushable": "^3.2.3", "it-stream-types": "^2.0.2", "main-event": "^1.0.1", "multiformats": "^13.3.6", "progress-events": "^1.0.1", "uint8arraylist": "^2.4.8" } }, "sha512-0MUFKoXWHTQW3oWIgSHApmYMUKWO/Y02+7Hpyp+n3z+geD4Xo2Rku2gYWmxcq+Pyjkz6Q9YjDWz3Yb2SoV2E8Q=="], - - "@libp2p/peer-collections/@libp2p/interface": ["@libp2p/interface@2.11.0", "", { "dependencies": { "@multiformats/dns": "^1.0.6", "@multiformats/multiaddr": "^12.4.4", "it-pushable": "^3.2.3", "it-stream-types": "^2.0.2", "main-event": "^1.0.1", "multiformats": "^13.3.6", "progress-events": "^1.0.1", "uint8arraylist": "^2.4.8" } }, "sha512-0MUFKoXWHTQW3oWIgSHApmYMUKWO/Y02+7Hpyp+n3z+geD4Xo2Rku2gYWmxcq+Pyjkz6Q9YjDWz3Yb2SoV2E8Q=="], - - "@libp2p/peer-id/@libp2p/interface": ["@libp2p/interface@2.11.0", "", { "dependencies": { "@multiformats/dns": "^1.0.6", "@multiformats/multiaddr": "^12.4.4", "it-pushable": "^3.2.3", "it-stream-types": "^2.0.2", "main-event": "^1.0.1", "multiformats": "^13.3.6", "progress-events": "^1.0.1", "uint8arraylist": "^2.4.8" } }, "sha512-0MUFKoXWHTQW3oWIgSHApmYMUKWO/Y02+7Hpyp+n3z+geD4Xo2Rku2gYWmxcq+Pyjkz6Q9YjDWz3Yb2SoV2E8Q=="], - - "@libp2p/peer-record/@libp2p/interface": ["@libp2p/interface@2.11.0", "", { "dependencies": { "@multiformats/dns": "^1.0.6", "@multiformats/multiaddr": "^12.4.4", "it-pushable": "^3.2.3", "it-stream-types": "^2.0.2", "main-event": "^1.0.1", "multiformats": "^13.3.6", "progress-events": "^1.0.1", "uint8arraylist": "^2.4.8" } }, "sha512-0MUFKoXWHTQW3oWIgSHApmYMUKWO/Y02+7Hpyp+n3z+geD4Xo2Rku2gYWmxcq+Pyjkz6Q9YjDWz3Yb2SoV2E8Q=="], - - "@libp2p/peer-store/@libp2p/interface": ["@libp2p/interface@2.11.0", "", { "dependencies": { "@multiformats/dns": "^1.0.6", "@multiformats/multiaddr": "^12.4.4", "it-pushable": "^3.2.3", "it-stream-types": "^2.0.2", "main-event": "^1.0.1", "multiformats": "^13.3.6", "progress-events": "^1.0.1", "uint8arraylist": "^2.4.8" } }, "sha512-0MUFKoXWHTQW3oWIgSHApmYMUKWO/Y02+7Hpyp+n3z+geD4Xo2Rku2gYWmxcq+Pyjkz6Q9YjDWz3Yb2SoV2E8Q=="], - - "@libp2p/utils/@libp2p/interface": ["@libp2p/interface@2.11.0", "", { "dependencies": { "@multiformats/dns": "^1.0.6", "@multiformats/multiaddr": "^12.4.4", "it-pushable": "^3.2.3", "it-stream-types": "^2.0.2", "main-event": "^1.0.1", "multiformats": "^13.3.6", "progress-events": "^1.0.1", "uint8arraylist": "^2.4.8" } }, "sha512-0MUFKoXWHTQW3oWIgSHApmYMUKWO/Y02+7Hpyp+n3z+geD4Xo2Rku2gYWmxcq+Pyjkz6Q9YjDWz3Yb2SoV2E8Q=="], - "@markmend/core/marked": ["marked@18.0.10", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-FJeH4bRpYoXiggcgriCGItKCSv3xkngJc4QCZ/rkQCogU3VYaLxYJoZl8Nw/b4+x7iij/pd+09mZ6A1dXzpL0A=="], "@multiformats/dns/@libp2p/interface": ["@libp2p/interface@3.2.5", "", { "dependencies": { "@multiformats/dns": "^1.0.6", "@multiformats/multiaddr": "^13.0.3", "main-event": "^1.0.1", "multiformats": "^14.0.0", "progress-events": "^1.1.0", "uint8arraylist": "^3.0.2" } }, "sha512-RdTchTdakBBc4ShKKsvoME/bJYsrCoVDpdYQVdd4TQ30TCb8QwWKGClqAtw7gfLNuaUKm41xz06kASW6AZpbDA=="], @@ -4162,9 +4144,9 @@ "@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], @@ -4186,7 +4168,7 @@ "@vitest/snapshot/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], - "@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="], + "@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@4.1.11", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw=="], "@vitest/utils/tinyrainbow": ["tinyrainbow@3.1.1", "", {}, "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw=="], @@ -4204,6 +4186,8 @@ "@waku/enr/@noble/secp256k1": ["@noble/secp256k1@1.7.2", "", {}, "sha512-/qzwYl5eFLH8OWIecQWM31qld2g1NfjgylK+TNhqtaUKP37Nm+Y+z30Fjhw0Ct8p9yCQEm2N3W/AckdIb3SMcQ=="], + "@waku/sds/@libp2p/interface": ["@libp2p/interface@2.10.4", "", { "dependencies": { "@multiformats/multiaddr": "^12.4.4", "it-pushable": "^3.2.3", "it-stream-types": "^2.0.2", "main-event": "^1.0.1", "multiformats": "^13.3.6", "progress-events": "^1.0.1", "uint8arraylist": "^2.4.8" } }, "sha512-FX3uujZgjH9bb7mDSNR54j3JzJnF/ngnQH20GQ1wPk5irIeHDvmzRlUj3bJ3hHQmdB2MxLZNT6e39O1es10LFA=="], + "@waku/utils/chai": ["chai@4.5.0", "", { "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", "deep-eql": "^4.1.3", "get-func-name": "^2.0.2", "loupe": "^2.3.6", "pathval": "^1.1.1", "type-detect": "^4.1.0" } }, "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw=="], "@wdio/cli/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -4292,7 +4276,7 @@ "execa/@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], - "expect-webdriverio/@vitest/snapshot": ["@vitest/snapshot@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw=="], + "expect-webdriverio/@vitest/snapshot": ["@vitest/snapshot@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog=="], "extract-zip/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], @@ -4554,6 +4538,8 @@ "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="], + "@testing-library/dom/pretty-format/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "@testing-library/dom/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], @@ -4562,8 +4548,6 @@ "@waku/enr/@libp2p/peer-id/@libp2p/crypto": ["@libp2p/crypto@5.1.22", "", { "dependencies": { "@libp2p/interface": "^3.2.5", "@noble/curves": "^2.0.1", "@noble/hashes": "^2.0.1", "multiformats": "^14.0.0", "protons-runtime": "^7.0.0", "uint8arraylist": "^3.0.2", "uint8arrays": "^6.1.1" } }, "sha512-yVk4BefSiEz044kjsiLSEoJTbhRoyamqx4kFI9LnX8/asr4wnqDWKTXcn6mSx/RVNEzmDO70l3vOvcMCl4aLCQ=="], - "@waku/enr/@libp2p/peer-id/@libp2p/interface": ["@libp2p/interface@2.11.0", "", { "dependencies": { "@multiformats/dns": "^1.0.6", "@multiformats/multiaddr": "^12.4.4", "it-pushable": "^3.2.3", "it-stream-types": "^2.0.2", "main-event": "^1.0.1", "multiformats": "^13.3.6", "progress-events": "^1.0.1", "uint8arraylist": "^2.4.8" } }, "sha512-0MUFKoXWHTQW3oWIgSHApmYMUKWO/Y02+7Hpyp+n3z+geD4Xo2Rku2gYWmxcq+Pyjkz6Q9YjDWz3Yb2SoV2E8Q=="], - "@waku/utils/chai/assertion-error": ["assertion-error@1.1.0", "", {}, "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw=="], "@waku/utils/chai/check-error": ["check-error@1.0.3", "", { "dependencies": { "get-func-name": "^2.0.2" } }, "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg=="], @@ -4686,7 +4670,7 @@ "edgedriver/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="], - "expect-webdriverio/@vitest/snapshot/@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="], + "expect-webdriverio/@vitest/snapshot/@vitest/pretty-format": ["@vitest/pretty-format@4.1.11", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw=="], "filelist/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="], diff --git a/package.json b/package.json index 09e51d16b..06f6ca383 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "@xmldom/xmldom": "^0.9.11", "ai": "^7.0.66", "aws4fetch": "^1.0.20", - "canvaskit-wasm": "^0.40.0", + "canvaskit-wasm": "^0.41.1", "culori": "^4.0.2", "dedent": "^1.7.2", "es-toolkit": "^1.51.0", diff --git a/packages/cli/package.json b/packages/cli/package.json index 209da63f3..09790b481 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -40,7 +40,7 @@ "@open-pencil/mcp": "workspace:*", "@open-pencil/scene-graph": "workspace:*", "agentfmt": "^0.1.3", - "canvaskit-wasm": "^0.40.0", + "canvaskit-wasm": "^0.41.1", "citty": "^0.1.6" }, "devDependencies": { diff --git a/packages/core/package.json b/packages/core/package.json index c321ce4bc..4a309049e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -181,7 +181,7 @@ "@tauri-apps/api": "^2.11.1", "@xmldom/xmldom": "^0.9.11", "acorn": "^8.18.0", - "canvaskit-wasm": "^0.40.0", + "canvaskit-wasm": "^0.41.1", "culori": "^4.0.2", "destr": "^2.0.5", "diff": "^8.0.4", diff --git a/packages/core/src/canvas/boolean.ts b/packages/core/src/canvas/boolean.ts index a83fbf77a..d6ba34127 100644 --- a/packages/core/src/canvas/boolean.ts +++ b/packages/core/src/canvas/boolean.ts @@ -1,4 +1,4 @@ -import type { Canvas, Path, PathOp } from 'canvaskit-wasm' +import type { Canvas, Path, PathBuilder, PathOp } from 'canvaskit-wasm' import type { SceneGraph, SceneNode } from '@open-pencil/scene-graph' @@ -63,12 +63,13 @@ export function canMakeBooleanSourceNode(node: SceneNode, graph: SceneGraph): bo } function lineStrokePath(r: SkiaRenderer, node: SceneNode): Path | null { - const path = new r.ck.Path() + const path = new r.ck.PathBuilder() path.moveTo(0, 0) path.lineTo(node.width, node.height) const stroke = node.strokes.find((item) => item.visible) - const outline = path.stroke({ width: stroke?.weight ?? 1 }) - if (outline !== path) path.delete() + const source = path.detachAndDelete() + const outline = source.makeStroked({ width: stroke?.weight ?? 1 }) + source.delete() return outline } @@ -89,12 +90,13 @@ export function nodeHasVisibleStroke(node: SceneNode): boolean { return node.strokes.some((stroke) => stroke.visible && stroke.weight > 0) } -function addVisibleStrokeOutlines(target: Path, source: Path, node: SceneNode): void { +function addVisibleStrokeOutlines(target: PathBuilder, source: Path, node: SceneNode): void { for (const stroke of node.strokes) { if (!stroke.visible || stroke.weight <= 0) continue - const outline = source.stroke({ width: stroke.weight }) + const outline = source.makeStroked({ width: stroke.weight }) if (!outline) continue target.addPath(outline) + outline.delete() } } @@ -111,7 +113,7 @@ function appendVisibleChildPaths( r: SkiaRenderer, graph: SceneGraph, parent: SceneNode, - target: Path, + target: PathBuilder, makeChildPath: (child: SceneNode) => Path | null, failOnMissing: boolean ): boolean | null { @@ -124,8 +126,7 @@ function appendVisibleChildPaths( if (failOnMissing) return null continue } - childPath.transform(nodePathTransform(r, child)) - target.addPath(childPath) + target.addPath(childPath, nodePathTransform(r, child)) childPath.delete() hasPath = true } @@ -133,7 +134,7 @@ function appendVisibleChildPaths( } function containerSourcePath(r: SkiaRenderer, node: SceneNode, graph: SceneGraph): Path | null { - const path = new r.ck.Path() + const path = new r.ck.PathBuilder() let hasPath = false if (nodeHasVisibleFill(node) || nodeHasVisibleStroke(node)) { @@ -164,7 +165,7 @@ function containerSourcePath(r: SkiaRenderer, node: SceneNode, graph: SceneGraph path.delete() return null } - return path + return path.detachAndDelete() } export function makeBooleanSourcePath( @@ -179,8 +180,12 @@ export function makeBooleanSourcePath( const path = baseShapePath(r, node) if (!path) return null - if (nodeHasVisibleStroke(node)) addVisibleStrokeOutlines(path, path, node) - return path + if (!nodeHasVisibleStroke(node)) return path + const combined = new r.ck.PathBuilder() + combined.addPath(path) + addVisibleStrokeOutlines(combined, path, node) + path.delete() + return combined.detachAndDelete() } export function hasVisibleStrokeSourceNode(node: SceneNode, graph: SceneGraph): boolean { @@ -199,7 +204,7 @@ export function makeStrokeOutlinePath( ): Path | null { if (!canMakeBooleanSourcePath(node)) return null if (canContainFlattenableChildren(node)) { - const path = new r.ck.Path() + const path = new r.ck.PathBuilder() let hasPath = false if (nodeHasVisibleStroke(node)) { const ownPath = baseShapePath(r, node) @@ -218,7 +223,7 @@ export function makeStrokeOutlinePath( (child) => makeStrokeOutlinePath(r, child, graph), false ) ?? false - if (hasPath) return path + if (hasPath) return path.detachAndDelete() path.delete() return null } @@ -230,16 +235,19 @@ export function makeStrokeOutlinePath( : baseShapePath(r, node) if (!path) return null if (node.type === 'LINE') return path - const outline = new r.ck.Path() + const outline = new r.ck.PathBuilder() addVisibleStrokeOutlines(outline, path, node) - return outline + path.delete() + return outline.detachAndDelete() } function transformedShapePath(r: SkiaRenderer, child: SceneNode, graph: SceneGraph): Path | null { const path = makeBooleanSourcePath(r, child, graph) if (!path) return null - path.transform(nodePathTransform(r, child)) - return path + const transformed = new r.ck.PathBuilder() + transformed.addPath(path, nodePathTransform(r, child)) + path.delete() + return transformed.detachAndDelete() } function operationForNode(r: SkiaRenderer, node: SceneNode): PathOp { @@ -251,9 +259,9 @@ function makeImportedFillGeometryPath(r: SkiaRenderer, node: SceneNode): Path | if (typeof r.getFillGeometry !== 'function') return null const fillGeometry = r.getFillGeometry(node) if (!fillGeometry) return null - const result = new r.ck.Path() + const result = new r.ck.PathBuilder() for (const path of fillGeometry) result.addPath(path) - return result + return result.detachAndDelete() } export function makeBooleanOperationPath( @@ -271,17 +279,18 @@ export function makeBooleanOperationPath( if (childPaths.length === 0) return makeImportedFillGeometryPath(r, node) - const result = childPaths[0] + let result = childPaths[0] const operation = operationForNode(r, node) for (let index = 1; index < childPaths.length; index++) { const path = childPaths[index] - const didApply = result.op(path, operation) + const combined = r.ck.Path.MakeFromOp(result, path, operation) + result.delete() path.delete() - if (!didApply) { - result.delete() + if (!combined) { for (const remaining of childPaths.slice(index + 1)) remaining.delete() return makeImportedFillGeometryPath(r, node) } + result = combined } return result } diff --git a/packages/core/src/canvas/fills.ts b/packages/core/src/canvas/fills.ts index 7cc262990..0fff4de8f 100644 --- a/packages/core/src/canvas/fills.ts +++ b/packages/core/src/canvas/fills.ts @@ -488,18 +488,19 @@ export function makeArcPath(r: SkiaRenderer, node: SceneNode) { const endDeg = arc.endingAngle * (180 / Math.PI) const sweepDeg = endDeg - startDeg - const path = new r.ck.Path() + const path = new r.ck.PathBuilder() const oval = r.ck.LTRBRect(0, 0, node.width, node.height) if (arc.innerRadius > 0) { path.addArc(oval, startDeg, sweepDeg) const innerOval = r.ck.LTRBRect(cx - innerRx, cy - innerRy, cx + innerRx, cy + innerRy) - const innerPath = new r.ck.Path() + const innerPath = new r.ck.PathBuilder() innerPath.addArc(innerOval, startDeg + sweepDeg, -sweepDeg) - path.addPath(innerPath) + const immutableInnerPath = innerPath.detachAndDelete() + path.addPath(immutableInnerPath) + immutableInnerPath.delete() path.close() - innerPath.delete() - return path + return path.detachAndDelete() } const isFullCircle = Math.abs(sweepDeg) >= 359.99 @@ -510,7 +511,7 @@ export function makeArcPath(r: SkiaRenderer, node: SceneNode) { path.addArc(oval, startDeg, sweepDeg) path.close() } - return path + return path.detachAndDelete() } export function drawArc(r: SkiaRenderer, canvas: Canvas, node: SceneNode, paint: Paint): void { diff --git a/packages/core/src/canvas/flatten.ts b/packages/core/src/canvas/flatten.ts index 1412c0e89..0cd4b8c57 100644 --- a/packages/core/src/canvas/flatten.ts +++ b/packages/core/src/canvas/flatten.ts @@ -22,15 +22,14 @@ function nodesToVectorProps( nodes: SceneNode[], makeNodePath: NodePathFactory ): VectorFlattenProps | null { - const path = new renderer.ck.Path() + const path = new renderer.ck.PathBuilder() for (const node of nodes) { const nodePath = makeNodePath(renderer, graph, node) if (!nodePath) { path.delete() return null } - nodePath.transform(nodePathTransform(renderer, node)) - path.addPath(nodePath) + path.addPath(nodePath, nodePathTransform(renderer, node)) nodePath.delete() } @@ -41,8 +40,9 @@ function nodesToVectorProps( } path.transform(renderer.ck.Matrix.translated(-bounds[0], -bounds[1])) - const vectorNetwork = parseSVGPath(path.toSVGString()) - path.delete() + const immutablePath = path.detachAndDelete() + const vectorNetwork = parseSVGPath(immutablePath.toSVGString()) + immutablePath.delete() return { name: 'Flatten', diff --git a/packages/core/src/canvas/labels/draw.ts b/packages/core/src/canvas/labels/draw.ts index d05cef959..6f4d52f07 100644 --- a/packages/core/src/canvas/labels/draw.ts +++ b/packages/core/src/canvas/labels/draw.ts @@ -138,7 +138,7 @@ export function drawComponentLabels(r: SkiaRenderer, canvas: Canvas, graph: Scen if (node.type === 'COMPONENT_SET') { const s = iconR * 0.45 const gap = iconR * 0.2 - const path = new r.ck.Path() + const path = new r.ck.PathBuilder() for (const [dx, dy] of [ [-1, -1], [1, -1], @@ -153,17 +153,19 @@ export function drawComponentLabels(r: SkiaRenderer, canvas: Canvas, graph: Scen path.lineTo(cx - s, cy) path.close() } - canvas.drawPath(path, r.auxFill) - path.delete() + const immutablePath = path.detachAndDelete() + canvas.drawPath(immutablePath, r.auxFill) + immutablePath.delete() } else { - const path = new r.ck.Path() + const path = new r.ck.PathBuilder() path.moveTo(iconCx, iconCy - iconR) path.lineTo(iconCx + iconR, iconCy) path.lineTo(iconCx, iconCy + iconR) path.lineTo(iconCx - iconR, iconCy) path.close() - canvas.drawPath(path, r.auxFill) - path.delete() + const immutablePath = path.detachAndDelete() + canvas.drawPath(immutablePath, r.auxFill) + immutablePath.delete() } canvas.drawText(displayText, labelX + iconS + COMPONENT_LABEL_ICON_GAP, labelY, r.auxFill, font) diff --git a/packages/core/src/canvas/node-edit-overlay.ts b/packages/core/src/canvas/node-edit-overlay.ts index 740c70efd..f7d100ced 100644 --- a/packages/core/src/canvas/node-edit-overlay.ts +++ b/packages/core/src/canvas/node-edit-overlay.ts @@ -415,17 +415,18 @@ function drawHandleDiamond( strokePaint: Paint ): void { const s = PEN_HANDLE_RADIUS - const path = new r.ck.Path() + const path = new r.ck.PathBuilder() path.moveTo(x, y - s) path.lineTo(x + s, y) path.lineTo(x, y + s) path.lineTo(x - s, y) path.close() + const immutablePath = path.detachAndDelete() // Draw stroke first (outset) then fill on top - canvas.drawPath(path, strokePaint) - canvas.drawPath(path, fillPaint) - path.delete() + canvas.drawPath(immutablePath, strokePaint) + canvas.drawPath(immutablePath, fillPaint) + immutablePath.delete() } // --------------------------------------------------------------------------- diff --git a/packages/core/src/canvas/overlays/auto-layout-hover.ts b/packages/core/src/canvas/overlays/auto-layout-hover.ts index a6b4d6177..8aad135df 100644 --- a/packages/core/src/canvas/overlays/auto-layout-hover.ts +++ b/packages/core/src/canvas/overlays/auto-layout-hover.ts @@ -84,7 +84,7 @@ function drawStripedRect( r.auxFill.setColor(canvasColor(r, fill)) canvas.drawRect(rect, r.auxFill) - const path = new r.ck.Path() + const path = new r.ck.PathBuilder() const left = rect[0] const top = rect[1] const right = rect[2] @@ -98,9 +98,10 @@ function drawStripedRect( r.auxStroke.setPathEffect(null) canvas.save() canvas.clipRect(rect, r.ck.ClipOp.Intersect, true) - canvas.drawPath(path, r.auxStroke) + const immutablePath = path.detachAndDelete() + canvas.drawPath(immutablePath, r.auxStroke) canvas.restore() - path.delete() + immutablePath.delete() } function drawValuePill(r: SkiaRenderer, canvas: Canvas, text: string, x: number, y: number) { diff --git a/packages/core/src/canvas/overlays/measurement.ts b/packages/core/src/canvas/overlays/measurement.ts index 7074130c3..0a56b5130 100644 --- a/packages/core/src/canvas/overlays/measurement.ts +++ b/packages/core/src/canvas/overlays/measurement.ts @@ -194,14 +194,15 @@ function drawTargetOutline(r: SkiaRenderer, canvas: Canvas, graph: SceneGraph, t 0, target.height ]) - const path = new r.ck.Path() + const path = new r.ck.PathBuilder() path.moveTo(points[0], points[1]) path.lineTo(points[2], points[3]) path.lineTo(points[4], points[5]) path.lineTo(points[6], points[7]) path.close() - canvas.drawPath(path, r.auxStroke) - path.delete() + const immutablePath = path.detachAndDelete() + canvas.drawPath(immutablePath, r.auxStroke) + immutablePath.delete() } export function drawMeasurements( diff --git a/packages/core/src/canvas/overlays/selection.ts b/packages/core/src/canvas/overlays/selection.ts index 3d6031e6c..76674e2af 100644 --- a/packages/core/src/canvas/overlays/selection.ts +++ b/packages/core/src/canvas/overlays/selection.ts @@ -230,17 +230,18 @@ function drawTextPathSelection( // the path (replaces the flat, path-blind text-edit selection rects). const bandPoly = pathTextSelectionBand(data, box, node.derivedTextGlyphs, sampled) if (bandPoly && bandPoly.length >= 6) { - const band = new r.ck.Path() + const band = new r.ck.PathBuilder() band.moveTo(bandPoly[0], bandPoly[1]) for (let i = 2; i < bandPoly.length; i += 2) band.lineTo(bandPoly[i], bandPoly[i + 1]) band.close() + const immutableBand = band.detachAndDelete() r.auxFill.setColor(r.selColor(0.16)) - canvas.drawPath(band, r.auxFill) + canvas.drawPath(immutableBand, r.auxFill) r.auxStroke.setStrokeWidth(1 / r.zoom) r.auxStroke.setColor(r.selColor()) r.auxStroke.setPathEffect(null) - canvas.drawPath(band, r.auxStroke) - band.delete() + canvas.drawPath(immutableBand, r.auxStroke) + immutableBand.delete() } // Faint dashed bounds + resize/rotate handles from the fitted path box. @@ -256,7 +257,7 @@ function drawTextPathSelection( drawBoundsHandles(r, canvas, box.x, box.y, box.x + box.width, box.y + box.height) // The path curve itself, sampled from the headless path metrics only for drawing. - const path = new r.ck.Path() + const path = new r.ck.PathBuilder() const overlaySteps = Math.max(64, Math.min(1024, Math.ceil(sampled.length))) const first = pointAtArc(sampled, 0) path.moveTo(first.x, first.y) @@ -265,8 +266,9 @@ function drawTextPathSelection( path.lineTo(point.x, point.y) } if (sampled.closed) path.close() - canvas.drawPath(path, r.selectionPaint) - path.delete() + const immutablePath = path.detachAndDelete() + canvas.drawPath(immutablePath, r.selectionPaint) + immutablePath.delete() // Center crosshair at the fitted path box center (screen-constant size). const cx = box.x + box.width / 2 @@ -367,14 +369,16 @@ export function drawParentFrameOutlines( parent.height ]) - const path = new r.ck.Path() + const path = new r.ck.PathBuilder() path.moveTo(pts[0], pts[1]) path.lineTo(pts[2], pts[3]) path.lineTo(pts[4], pts[5]) path.lineTo(pts[6], pts[7]) path.close() - canvas.drawPath(path, r.parentOutlinePaint) + const immutablePath = path.detachAndDelete() + canvas.drawPath(immutablePath, r.parentOutlinePaint) + immutablePath.delete() } } diff --git a/packages/core/src/canvas/pen-overlay.ts b/packages/core/src/canvas/pen-overlay.ts index f63f3ba0f..71bd83a46 100644 --- a/packages/core/src/canvas/pen-overlay.ts +++ b/packages/core/src/canvas/pen-overlay.ts @@ -18,7 +18,7 @@ function buildSegmentsPath( toScreen: ToScreenFn ): InstanceType { const { vertices, segments } = penState - const path = new r.ck.Path() + const path = new r.ck.PathBuilder() for (const seg of segments) { const s = toScreen(vertices[seg.start].x, vertices[seg.start].y) const e = toScreen(vertices[seg.end].x, vertices[seg.end].y) @@ -43,7 +43,7 @@ function buildSegmentsPath( path.cubicTo(cp1.x, cp1.y, cp2.x, cp2.y, e.x, e.y) } } - return path + return path.detachAndDelete() } /** Build a Path for the preview line from last vertex to cursor */ @@ -56,7 +56,7 @@ function buildCursorPath( if (vertices.length === 0) return null if (penState.pendingClose && vertices.length > 2) { - const path = new r.ck.Path() + const path = new r.ck.PathBuilder() const last = toScreen(vertices[vertices.length - 1].x, vertices[vertices.length - 1].y) const first = toScreen(vertices[0].x, vertices[0].y) path.moveTo(last.x, last.y) @@ -67,12 +67,12 @@ function buildCursorPath( } else { path.lineTo(first.x, first.y) } - return path + return path.detachAndDelete() } if (cursorX == null || cursorY == null) return null - const path = new r.ck.Path() + const path = new r.ck.PathBuilder() const last = toScreen(vertices[vertices.length - 1].x, vertices[vertices.length - 1].y) const cursor = toScreen(cursorX, cursorY) path.moveTo(last.x, last.y) @@ -85,7 +85,7 @@ function buildCursorPath( } else { path.lineTo(cursor.x, cursor.y) } - return path + return path.detachAndDelete() } function drawPenPaths( @@ -234,19 +234,20 @@ export function drawRemoteCursors( 0, node.height ]) - const box = new r.ck.Path() + const box = new r.ck.PathBuilder() box.moveTo(c[0] * r.zoom + r.panX, c[1] * r.zoom + r.panY) for (let i = 2; i < c.length; i += 2) { box.lineTo(c[i] * r.zoom + r.panX, c[i + 1] * r.zoom + r.panY) } box.close() - canvas.drawPath(box, r.auxStroke) - box.delete() + const immutableBox = box.detachAndDelete() + canvas.drawPath(immutableBox, r.auxStroke) + immutableBox.delete() } } const S = CURSOR_SIZE - const path = new r.ck.Path() + const path = new r.ck.PathBuilder() path.moveTo(screenX, screenY) path.lineTo(screenX, screenY + S * 1.35) path.lineTo(screenX + S * 0.38, screenY + S * 1.0) @@ -256,14 +257,15 @@ export function drawRemoteCursors( path.lineTo(screenX + S * 1.0, screenY + S * 0.82) path.close() + const immutablePath = path.detachAndDelete() r.auxStroke.setColor(r.ck.Color4f(1, 1, 1, 1)) r.auxStroke.setStrokeWidth(2) r.auxStroke.setPathEffect(null) - canvas.drawPath(path, r.auxStroke) + canvas.drawPath(immutablePath, r.auxStroke) r.auxFill.setColor(r.ck.Color4f(cr, g, b, 1)) - canvas.drawPath(path, r.auxFill) - path.delete() + canvas.drawPath(immutablePath, r.auxFill) + immutablePath.delete() if (cursor.name) { const font = r.labelFont diff --git a/packages/core/src/canvas/scene.ts b/packages/core/src/canvas/scene.ts index 03d81e75f..652776420 100644 --- a/packages/core/src/canvas/scene.ts +++ b/packages/core/src/canvas/scene.ts @@ -461,7 +461,7 @@ function vectorStrokePaths(r: SkiaRenderer, node: SceneNode): Path[] | null { const start = node.vectorNetwork.vertices[segment.start] const end = node.vectorNetwork.vertices[segment.end] - const path = new r.ck.Path() + const path = new r.ck.PathBuilder() path.moveTo(start.x, start.y) const isStraight = Math.abs(segment.tangentStart.x) < 0.001 && @@ -480,7 +480,7 @@ function vectorStrokePaths(r: SkiaRenderer, node: SceneNode): Path[] | null { end.y ) } - paths.push(path) + paths.push(path.detachAndDelete()) } if (paths.length === 0) return null @@ -527,7 +527,7 @@ function drawVectorPathStrokes( if (!outlines) { outlines = [] for (const vp of vectorPaths) { - const outline = vp.copy().stroke(strokeOpts) + const outline = vp.makeStroked(strokeOpts) if (outline) outlines.push(outline) } if (outlineCacheKey) r.vectorStrokeOutlineCache.set(outlineCacheKey, outlines) diff --git a/packages/core/src/canvas/shadows.ts b/packages/core/src/canvas/shadows.ts index a14eb3360..4e4e3ec4b 100644 --- a/packages/core/src/canvas/shadows.ts +++ b/packages/core/src/canvas/shadows.ts @@ -141,15 +141,17 @@ function effectLayerBounds( ) } -function applySpreadToPath(r: SkiaRenderer, path: Path, spread: number): boolean { - if (spread === 0) return true - const ring = path.copy() - try { - if (!ring.stroke({ width: Math.abs(spread) * 2, join: r.ck.StrokeJoin.Round })) return false - return path.op(ring, spread > 0 ? r.ck.PathOp.Union : r.ck.PathOp.Difference) - } finally { - ring.delete() - } +function pathWithSpread(r: SkiaRenderer, path: Path, spread: number): Path | null { + if (spread === 0) return path.copy() + const ring = path.makeStroked({ width: Math.abs(spread) * 2, join: r.ck.StrokeJoin.Round }) + if (!ring) return null + const result = r.ck.Path.MakeFromOp( + path, + ring, + spread > 0 ? r.ck.PathOp.Union : r.ck.PathOp.Difference + ) + ring.delete() + return result } function drawPathShape( @@ -160,11 +162,13 @@ function drawPathShape( spread = 0 ): void { const path = makeNodeShapePath(r, node, r.ltrb(0, 0, node.width, node.height), hasRadius) + const spreadPath = pathWithSpread(r, path, spread) + path.delete() + if (!spreadPath) return try { - applySpreadToPath(r, path, spread) - canvas.drawPath(path, r.auxFill) + canvas.drawPath(spreadPath, r.auxFill) } finally { - path.delete() + spreadPath.delete() } } @@ -173,12 +177,12 @@ function drawShadowGeometryPath(r: SkiaRenderer, canvas: Canvas, path: Path, spr canvas.drawPath(path, r.auxFill) return } - const copy = path.copy() + const spreadPath = pathWithSpread(r, path, spread) + if (!spreadPath) return try { - applySpreadToPath(r, copy, spread) - canvas.drawPath(copy, r.auxFill) + canvas.drawPath(spreadPath, r.auxFill) } finally { - copy.delete() + spreadPath.delete() } } @@ -420,9 +424,10 @@ function drawShapeInnerShadow( if (shadowShapeChild) drawChildTransform(canvas, shadowShapeChild) if (shapeNode.type === 'ELLIPSE') { - const path = new r.ck.Path() + const builder = new r.ck.PathBuilder() + builder.addOval(shapeRect) + const path = builder.detachAndDelete() try { - path.addOval(shapeRect) canvas.clipPath(path, r.ck.ClipOp.Intersect, true) } finally { path.delete() @@ -454,66 +459,55 @@ function drawShapeInnerShadow( Math.max(shapeNode.width + expand, shapeNode.width + expand + localOffsetX + spreadPadding), Math.max(shapeNode.height + expand, shapeNode.height + expand + localOffsetY + spreadPadding) ) - const bigPath = new r.ck.Path() - try { - bigPath.addRect(big) - if (shapeNode.type === 'ELLIPSE') { - const innerPath = new r.ck.Path() - try { - const offsetRect = r.ck.LTRBRect( - localOffsetX + sp, - localOffsetY + sp, - shapeNode.width + localOffsetX - sp, - shapeNode.height + localOffsetY - sp - ) - innerPath.addOval(offsetRect) - bigPath.op(innerPath, r.ck.PathOp.Difference) - } finally { - innerPath.delete() - } - } else if (isPathShape(shapeNode)) { - const innerPath = makeNodeShapePath(r, shapeNode, shapeRect, shapeHasRadius) - try { - innerPath.transform(r.ck.Matrix.translated(localOffsetX, localOffsetY)) - applySpreadToPath(r, innerPath, -sp) - bigPath.op(innerPath, r.ck.PathOp.Difference) - } finally { - innerPath.delete() - } - } else if (nodeHasSmoothCorners(shapeNode)) { - const innerPath = makeSmoothRRectPath(r, shapeNode, -sp, localOffsetX, localOffsetY) - try { - bigPath.op(innerPath, r.ck.PathOp.Difference) - } finally { - innerPath.delete() - } - } else if (shapeHasRadius) { - const innerPath = new r.ck.Path() - try { - innerPath.addRRect(r.makeRRectWithOffset(shapeNode, localOffsetX, localOffsetY, sp)) - bigPath.op(innerPath, r.ck.PathOp.Difference) - } finally { - innerPath.delete() - } - } else { - const innerPath = new r.ck.Path() - try { - innerPath.addRect( - r.ck.LTRBRect( - localOffsetX + sp, - localOffsetY + sp, - shapeNode.width + localOffsetX - sp, - shapeNode.height + localOffsetY - sp - ) - ) - bigPath.op(innerPath, r.ck.PathOp.Difference) - } finally { - innerPath.delete() - } - } - canvas.drawPath(bigPath, r.auxFill) - } finally { - bigPath.delete() + const bigPathBuilder = new r.ck.PathBuilder() + bigPathBuilder.addRect(big) + const bigPath = bigPathBuilder.detachAndDelete() + let innerPath: Path | null = null + if (shapeNode.type === 'ELLIPSE') { + const builder = new r.ck.PathBuilder() + const offsetRect = r.ck.LTRBRect( + localOffsetX + sp, + localOffsetY + sp, + shapeNode.width + localOffsetX - sp, + shapeNode.height + localOffsetY - sp + ) + builder.addOval(offsetRect) + innerPath = builder.detachAndDelete() + } else if (isPathShape(shapeNode)) { + const sourcePath = makeNodeShapePath(r, shapeNode, shapeRect, shapeHasRadius) + const transformedBuilder = new r.ck.PathBuilder() + transformedBuilder.addPath(sourcePath, r.ck.Matrix.translated(localOffsetX, localOffsetY)) + sourcePath.delete() + const transformed = transformedBuilder.detachAndDelete() + innerPath = pathWithSpread(r, transformed, -sp) + transformed.delete() + } else if (nodeHasSmoothCorners(shapeNode)) { + innerPath = makeSmoothRRectPath(r, shapeNode, -sp, localOffsetX, localOffsetY) + } else if (shapeHasRadius) { + const builder = new r.ck.PathBuilder() + builder.addRRect(r.makeRRectWithOffset(shapeNode, localOffsetX, localOffsetY, sp)) + innerPath = builder.detachAndDelete() + } else { + const builder = new r.ck.PathBuilder() + builder.addRect( + r.ck.LTRBRect( + localOffsetX + sp, + localOffsetY + sp, + shapeNode.width + localOffsetX - sp, + shapeNode.height + localOffsetY - sp + ) + ) + innerPath = builder.detachAndDelete() + } + + const shadowPath = innerPath + ? r.ck.Path.MakeFromOp(bigPath, innerPath, r.ck.PathOp.Difference) + : null + innerPath?.delete() + bigPath.delete() + if (shadowPath) { + canvas.drawPath(shadowPath, r.auxFill) + shadowPath.delete() } } finally { canvas.restore() diff --git a/packages/core/src/canvas/shapes.ts b/packages/core/src/canvas/shapes.ts index 83537ac1b..c5987a38c 100644 --- a/packages/core/src/canvas/shapes.ts +++ b/packages/core/src/canvas/shapes.ts @@ -1,4 +1,4 @@ -import type { Canvas, Path } from 'canvaskit-wasm' +import type { Canvas, Path, PathBuilder } from 'canvaskit-wasm' import type { SceneNode } from '@open-pencil/scene-graph' import { polygonVertices } from '@open-pencil/scene-graph/geometry' @@ -170,7 +170,7 @@ function smoothCornerPathParams(corner: SmoothCorner, smoothing: number): Smooth } function drawTopRightSmoothCorner( - path: Path, + path: PathBuilder, corner: SmoothCornerPathParams, x: number, y: number @@ -207,7 +207,7 @@ function drawTopRightSmoothCorner( } function drawBottomRightSmoothCorner( - path: Path, + path: PathBuilder, corner: SmoothCornerPathParams, x: number, y: number @@ -244,7 +244,7 @@ function drawBottomRightSmoothCorner( } function drawBottomLeftSmoothCorner( - path: Path, + path: PathBuilder, corner: SmoothCornerPathParams, x: number, y: number @@ -280,7 +280,12 @@ function drawBottomLeftSmoothCorner( ) } -function drawTopLeftSmoothCorner(path: Path, corner: SmoothCornerPathParams, x: number, y: number) { +function drawTopLeftSmoothCorner( + path: PathBuilder, + corner: SmoothCornerPathParams, + x: number, + y: number +) { if (corner.radius === 0) { path.lineTo(x, y - corner.p) return @@ -319,7 +324,7 @@ export function makeSmoothRRectPath( offsetX = 0, offsetY = 0 ): Path { - const path = new r.ck.Path() + const path = new r.ck.PathBuilder() const left = offsetX - spread const top = offsetY - spread const right = offsetX + node.width + spread @@ -328,7 +333,7 @@ export function makeSmoothRRectPath( const height = bottom - top if (width <= 0 || height <= 0) { path.addRect(r.ck.LTRBRect(left, top, Math.max(left, right), Math.max(top, bottom))) - return path + return path.detachAndDelete() } const smoothing = Math.max(0, Math.min(node.cornerSmoothing, 1)) @@ -345,7 +350,7 @@ export function makeSmoothRRectPath( bottomLeftCorner.radius === 0 ) { path.addRect(r.ck.LTRBRect(left, top, right, bottom)) - return path + return path.detachAndDelete() } path.moveTo(right - topRightCorner.p, top) @@ -357,7 +362,7 @@ export function makeSmoothRRectPath( path.lineTo(left, top + topLeftCorner.p) drawTopLeftSmoothCorner(path, topLeftCorner, left, top + topLeftCorner.p) path.close() - return path + return path.detachAndDelete() } export function makeNodeShapePath( @@ -366,7 +371,7 @@ export function makeNodeShapePath( rect: Float32Array, hasRadius: boolean ): Path { - const path = new r.ck.Path() + const path = new r.ck.PathBuilder() switch (node.type) { case 'ELLIPSE': path.addOval(rect) @@ -396,17 +401,17 @@ export function makeNodeShapePath( path.addRect(rect) } } - return path + return path.detachAndDelete() } export function makePolygonPath(r: SkiaRenderer, node: SceneNode): Path { - const path = new r.ck.Path() + const path = new r.ck.PathBuilder() polygonVertices(node).forEach((point, index) => { if (index === 0) path.moveTo(point.x, point.y) else path.lineTo(point.x, point.y) }) path.close() - return path + return path.detachAndDelete() } export function makeRRect(r: SkiaRenderer, node: SceneNode): Float32Array { @@ -500,10 +505,11 @@ export function clipNodeShape( hasRadius: boolean ): void { if (node.type === 'ELLIPSE') { - const clipPath = new r.ck.Path() + const clipPath = new r.ck.PathBuilder() clipPath.addOval(rect) - canvas.clipPath(clipPath, r.ck.ClipOp.Intersect, true) - clipPath.delete() + const immutableClipPath = clipPath.detachAndDelete() + canvas.clipPath(immutableClipPath, r.ck.ClipOp.Intersect, true) + immutableClipPath.delete() } else if (nodeHasSmoothCorners(node)) { const clipPath = makeSmoothRRectPath(r, node) canvas.clipPath(clipPath, r.ck.ClipOp.Intersect, true) diff --git a/packages/core/src/canvas/strokes.ts b/packages/core/src/canvas/strokes.ts index b74b482fe..2b2a380b3 100644 --- a/packages/core/src/canvas/strokes.ts +++ b/packages/core/src/canvas/strokes.ts @@ -191,13 +191,19 @@ export function drawStrokeWithAlign( } else if (align === 'OUTSIDE') { canvas.save() const bigRect = r.ck.LTRBRect(-node.width, -node.height, node.width * 2, node.height * 2) - const outerPath = new r.ck.Path() + const outerPath = new r.ck.PathBuilder() outerPath.addRect(bigRect) const innerPath = r.makeNodeShapePath(node, rect, hasRadius) - outerPath.op(innerPath, r.ck.PathOp.Difference) + const immutableOuterPath = outerPath.detachAndDelete() + const clipPath = r.ck.Path.MakeFromOp(immutableOuterPath, innerPath, r.ck.PathOp.Difference) + immutableOuterPath.delete() innerPath.delete() - canvas.clipPath(outerPath, r.ck.ClipOp.Intersect, true) - outerPath.delete() + if (!clipPath) { + canvas.restore() + return + } + canvas.clipPath(clipPath, r.ck.ClipOp.Intersect, true) + clipPath.delete() const origWidth = r.strokePaint.getStrokeWidth() r.strokePaint.setStrokeWidth(origWidth * 2) r.drawNodeStroke(canvas, node, rect, hasRadius) @@ -236,14 +242,25 @@ export function drawRRectStrokeWithAlign( canvas.restore() } else if (stroke.align === 'OUTSIDE') { canvas.save() - const outerPath = new r.ck.Path() + const outerPath = new r.ck.PathBuilder() outerPath.addRect(r.ck.LTRBRect(-node.width, -node.height, node.width * 2, node.height * 2)) - const innerPath = new r.ck.Path() + const innerPath = new r.ck.PathBuilder() innerPath.addRRect(rrect) - outerPath.op(innerPath, r.ck.PathOp.Difference) - innerPath.delete() - canvas.clipPath(outerPath, r.ck.ClipOp.Intersect, true) - outerPath.delete() + const immutableOuterPath = outerPath.detachAndDelete() + const immutableInnerPath = innerPath.detachAndDelete() + const clipPath = r.ck.Path.MakeFromOp( + immutableOuterPath, + immutableInnerPath, + r.ck.PathOp.Difference + ) + immutableOuterPath.delete() + immutableInnerPath.delete() + if (!clipPath) { + canvas.restore() + return + } + canvas.clipPath(clipPath, r.ck.ClipOp.Intersect, true) + clipPath.delete() r.strokePaint.setStrokeWidth(stroke.weight * 2) canvas.drawRRect(rrect, r.strokePaint) r.strokePaint.setStrokeWidth(stroke.weight) diff --git a/packages/core/src/canvas/text/derived.ts b/packages/core/src/canvas/text/derived.ts index 3fcc3e01a..6586d6397 100644 --- a/packages/core/src/canvas/text/derived.ts +++ b/packages/core/src/canvas/text/derived.ts @@ -183,14 +183,15 @@ function drawWavyDecoration( ): void { const amplitude = Math.max(0.5, span.thickness * 0.5) const wavelength = Math.max(6, span.thickness * 5) - const path = new r.ck.Path() + const path = new r.ck.PathBuilder() path.moveTo(span.x1, y) for (let x = span.x1; x <= span.x2; x += 2) { path.lineTo(x, y + Math.sin(((x - span.x1) / wavelength) * Math.PI * 2) * amplitude) } path.lineTo(span.x2, y) - canvas.drawPath(path, paint) - path.delete() + const immutablePath = path.detachAndDelete() + canvas.drawPath(immutablePath, paint) + immutablePath.delete() } function derivedDecorationY(node: SceneNode, span: DecorationSpan, baselineY: number): number { @@ -278,27 +279,29 @@ function getGlyphSilhouette( // Round join/cap: miter spikes on glyph cusps (e.g. 'A' apex) shoot far // outside the letterform. Width doubles because half the band is swallowed // by the union with the glyph body (OUTSIDE-stroke look). - const stroked = outline.stroke({ + const stroked = outline.makeStroked({ width: relativeWeight * 2, join: r.ck.StrokeJoin.Round, cap: r.ck.StrokeCap.Round }) - // stroke()/op() mutate `outline` in place; both report failure, not a new path. - const merged = stroked?.op(base, r.ck.PathOp.Union) + const merged = stroked ? r.ck.Path.MakeFromOp(stroked, base, r.ck.PathOp.Union) : null if (!merged) { // Degenerate outline — draw the bare glyph, uncached so callers free it. outline.delete() + stroked?.delete() return { path: base, cached: false } } base.delete() + stroked?.delete() + outline.delete() // ponytail: crude bound — a live stroke-weight drag mints a key per tick; // wholesale clear beats an LRU here since rebuild is cheap. if (r.glyphSilhouetteCache.size >= 512) { for (const path of r.glyphSilhouetteCache.values()) path.delete() r.glyphSilhouetteCache.clear() } - r.glyphSilhouetteCache.set(key, outline) - return { path: outline, cached: true } + r.glyphSilhouetteCache.set(key, merged) + return { path: merged, cached: true } } /** diff --git a/packages/core/src/canvas/text/outlines.ts b/packages/core/src/canvas/text/outlines.ts index 17388cb3b..468936eac 100644 --- a/packages/core/src/canvas/text/outlines.ts +++ b/packages/core/src/canvas/text/outlines.ts @@ -1,4 +1,4 @@ -import type { Path } from 'canvaskit-wasm' +import type { Path, PathBuilder } from 'canvaskit-wasm' import type { SceneNode } from '@open-pencil/scene-graph' @@ -7,7 +7,7 @@ import type { OutlineCommand } from '#core/text/opentype' import { textNodeToOutlineLayout } from '#core/text/outlines' function appendOutlineCommand( - path: Path, + path: PathBuilder, command: OutlineCommand, xOffset: number, yOffset: number @@ -47,9 +47,9 @@ export function textNodeToOutlinePath(r: SkiaRenderer, node: SceneNode): Path | const layout = textNodeToOutlineLayout(node) if (!layout) return null - const path = new r.ck.Path() + const path = new r.ck.PathBuilder() for (const glyph of layout.glyphs) { for (const command of glyph.commands) appendOutlineCommand(path, command, glyph.x, glyph.y) } - return path + return path.detachAndDelete() } diff --git a/packages/core/src/vector/centerline.ts b/packages/core/src/vector/centerline.ts index 349915df6..5cefc09b4 100644 --- a/packages/core/src/vector/centerline.ts +++ b/packages/core/src/vector/centerline.ts @@ -173,17 +173,17 @@ function buildCenterlineFromCrescent( const arcParams = fitCircleArc(midpoints) if (arcParams) { const { cx, cy, r, startAngleDeg, sweepDeg } = arcParams - const path = new ck.Path() + const path = new ck.PathBuilder() path.addArc(ck.LTRBRect(cx - r, cy - r, cx + r, cy + r), startAngleDeg, sweepDeg) - return path + return path.detachAndDelete() } - const path = new ck.Path() + const path = new ck.PathBuilder() path.moveTo(midpoints[0].x, midpoints[0].y) for (let i = 1; i < midpoints.length; i++) { path.lineTo(midpoints[i].x, midpoints[i].y) } - return path + return path.detachAndDelete() } export function vectorNetworkToCenterlinePath(ck: CanvasKit, network: VectorNetwork): Path { @@ -195,7 +195,7 @@ export function vectorNetworkToCenterlinePath(ck: CanvasKit, network: VectorNetw if (result) return result } - const path = new ck.Path() + const path = new ck.PathBuilder() addOpenSegmentsToPath(path, segments, vertices) - return path + return path.detachAndDelete() } diff --git a/packages/core/src/vector/index.ts b/packages/core/src/vector/index.ts index ea7115cde..558bbb8e7 100644 --- a/packages/core/src/vector/index.ts +++ b/packages/core/src/vector/index.ts @@ -32,7 +32,7 @@ export function vectorNetworkToPath(ck: CanvasKit, network: VectorNetwork): Path const paths: Path[] = [] const regionSegmentIndexes = new Set() for (const region of regions) { - const regionPath = new ck.Path() + const regionPath = new ck.PathBuilder() for (const loop of region.loops) { for (const segmentIndex of loop) regionSegmentIndexes.add(segmentIndex) addLoopToPath(regionPath, loop, segments, vertices) @@ -40,21 +40,21 @@ export function vectorNetworkToPath(ck: CanvasKit, network: VectorNetwork): Path regionPath.setFillType( region.windingRule === 'EVENODD' ? ck.FillType.EvenOdd : ck.FillType.Winding ) - paths.push(regionPath) + paths.push(regionPath.detachAndDelete()) } const openSegments = segments.filter((_, index) => !regionSegmentIndexes.has(index)) if (openSegments.length > 0) { - const openPath = new ck.Path() + const openPath = new ck.PathBuilder() addOpenSegmentsToPath(openPath, openSegments, vertices) - paths.push(openPath) + paths.push(openPath.detachAndDelete()) } return paths } - const path = new ck.Path() + const path = new ck.PathBuilder() addOpenSegmentsToPath(path, segments, vertices) - return [path] + return [path.detachAndDelete()] } const CMD_CLOSE = 0 @@ -68,8 +68,8 @@ export function geometryBlobToPath( blob: Uint8Array, windingRule: WindingRule ): Path { - const path = new ck.Path() - if (!(blob.buffer instanceof ArrayBuffer)) return path + const path = new ck.PathBuilder() + if (!(blob.buffer instanceof ArrayBuffer)) return path.detachAndDelete() const dv = new DataView(blob.buffer, blob.byteOffset, blob.byteLength) let o = 0 @@ -114,10 +114,10 @@ export function geometryBlobToPath( break } default: - return path + return path.detachAndDelete() } } path.setFillType(windingRule === 'EVENODD' ? ck.FillType.EvenOdd : ck.FillType.Winding) - return path + return path.detachAndDelete() } diff --git a/packages/vue/package.json b/packages/vue/package.json index 3cb3b435b..a10ebe115 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -32,7 +32,7 @@ }, "peerDependencies": { "@open-pencil/core": "workspace:*", - "canvaskit-wasm": "^0.40.0", + "canvaskit-wasm": ">=0.41.1", "vue": "^3.5.41" }, "peerDependenciesMeta": { diff --git a/tests/engine/render/canvas/boolean.test.ts b/tests/engine/render/canvas/boolean.test.ts index e47b8e91d..089d1f740 100644 --- a/tests/engine/render/canvas/boolean.test.ts +++ b/tests/engine/render/canvas/boolean.test.ts @@ -168,8 +168,9 @@ describe('boolean operation paths', () => { test('uses imported fill geometry when child paths cannot produce a boolean path', async () => { const r = await createRenderer() - const importedPath = new r.ck.Path() - importedPath.addRect(r.ck.LTRBRect(5, 6, 25, 36)) + const importedPathBuilder = new r.ck.PathBuilder() + importedPathBuilder.addRect(r.ck.LTRBRect(5, 6, 25, 36)) + const importedPath = importedPathBuilder.detachAndDelete() r.getFillGeometry = () => [importedPath] const node = { id: 'boolean', diff --git a/tests/engine/render/canvas/corner-smoothing.test.ts b/tests/engine/render/canvas/corner-smoothing.test.ts index 5edace20e..9b86fac6d 100644 --- a/tests/engine/render/canvas/corner-smoothing.test.ts +++ b/tests/engine/render/canvas/corner-smoothing.test.ts @@ -23,14 +23,15 @@ function createRenderer() { delete: ReturnType }> = [] - class MockPath { - addRect = mock(() => undefined) - moveTo = mock(() => undefined) - lineTo = mock(() => undefined) - cubicTo = mock(() => undefined) - arcToRotated = mock(() => undefined) - close = mock(() => undefined) + class MockPathBuilder { + addRect = mock(() => this) + moveTo = mock(() => this) + lineTo = mock(() => this) + cubicTo = mock(() => this) + arcToRotated = mock(() => this) + close = mock(() => this) delete = mock(() => undefined) + detachAndDelete = mock(() => ({ delete: mock(() => undefined) })) constructor() { paths.push(this) @@ -39,7 +40,7 @@ function createRenderer() { const renderer = { ck: { - Path: MockPath, + PathBuilder: MockPathBuilder, LTRBRect: mock((l, t, r, b) => new Float32Array([l, t, r, b])) }, fillPaint: {} diff --git a/tests/engine/render/canvas/effects/helpers.ts b/tests/engine/render/canvas/effects/helpers.ts index 05b3a55b8..63e065256 100644 --- a/tests/engine/render/canvas/effects/helpers.ts +++ b/tests/engine/render/canvas/effects/helpers.ts @@ -8,26 +8,34 @@ export function mockCalls(fn: ReturnType): unknown[][] { } export function createMockRenderer(overrides: Partial = {}): SkiaRenderer { + class MockPath { + delete = mock(() => undefined) + copy = mock(() => new MockPath()) + makeCombined = mock(() => new MockPath()) + makeStroked = mock(() => new MockPath()) + } + return { ck: { Color4f: mock((r, g, b, a) => new Float32Array([r, g, b, a])), LTRBRect: mock((l, t, r, b) => new Float32Array([l, t, r, b])), RRectXY: mock(() => new Float32Array(12)), ClipOp: { Intersect: 0 }, - Path: class { - addOval = mock(() => undefined) - addRect = mock(() => undefined) - addRRect = mock(() => undefined) - addPath = mock(() => undefined) - op = mock(() => true) - transform = mock(() => undefined) + Path: Object.assign(MockPath, { + MakeFromOp: mock(() => new MockPath()) + }), + PathBuilder: class { + addOval = mock(() => this) + addRect = mock(() => this) + addRRect = mock(() => this) + addPath = mock(() => this) + transform = mock(() => this) delete = mock(() => undefined) - copy = mock(() => this) - stroke = mock(() => this) - moveTo = mock(() => undefined) - lineTo = mock(() => undefined) - cubicTo = mock(() => undefined) - close = mock(() => undefined) + moveTo = mock(() => this) + lineTo = mock(() => this) + cubicTo = mock(() => this) + close = mock(() => this) + detachAndDelete = mock(() => new MockPath()) }, PathOp: { Difference: 0, Union: 1 }, StrokeCap: { Butt: 0, Round: 1, Square: 2 }, diff --git a/tests/engine/render/canvas/effects/path-shapes.test.ts b/tests/engine/render/canvas/effects/path-shapes.test.ts index 6ed82a356..3788258b8 100644 --- a/tests/engine/render/canvas/effects/path-shapes.test.ts +++ b/tests/engine/render/canvas/effects/path-shapes.test.ts @@ -39,12 +39,14 @@ describe('path shape shadows', () => { expect(canvas.drawRect).not.toHaveBeenCalled() }) - test('drop shadow spread copies imported geometry instead of mutating cache paths', () => { + test('drop shadow spread combines and deletes a detached stroked ring', () => { const r = createMockRenderer() const canvas = createMockCanvas() const cachedPath = new r.ck.Path() - const spreadPath = new r.ck.Path() - cachedPath.copy = mock(() => spreadPath) + const ringPath = new r.ck.Path() + const combinedPath = new r.ck.Path() + cachedPath.makeStroked = mock(() => ringPath) + r.ck.Path.MakeFromOp = mock(() => combinedPath) r.getFillGeometry = mock(() => [cachedPath]) const node: Partial = { @@ -67,10 +69,11 @@ describe('path shape shadows', () => { renderEffects(r, canvas as Canvas, node as SceneNode, new Float32Array(4), false, 'behind') - expect(cachedPath.copy).toHaveBeenCalled() - expect(cachedPath.op).not.toHaveBeenCalled() - expect(spreadPath.op).toHaveBeenCalledWith(expect.anything(), r.ck.PathOp.Union) - expect(spreadPath.delete).toHaveBeenCalled() + const makeFromOp = r.ck.Path.MakeFromOp as ReturnType + expect(makeFromOp).toHaveBeenCalledWith(cachedPath, ringPath, r.ck.PathOp.Union) + expect(ringPath.delete).toHaveBeenCalledTimes(1) + expect(combinedPath.delete).toHaveBeenCalledTimes(1) + expect(cachedPath.delete).not.toHaveBeenCalled() }) test('inner shadow for a polygon clips and cuts out with polygon paths', () => { diff --git a/tests/engine/render/canvas/text.test.ts b/tests/engine/render/canvas/text.test.ts index 75862d907..9cfb79686 100644 --- a/tests/engine/render/canvas/text.test.ts +++ b/tests/engine/render/canvas/text.test.ts @@ -57,13 +57,14 @@ function createMockRenderer(overrides: Partial> = {}) { }, ck: { MakePicture: mock(() => createMockPicture()), - Path: class { - moveTo = mock(() => undefined) - lineTo = mock(() => undefined) - cubicTo = mock(() => undefined) - quadTo = mock(() => undefined) - close = mock(() => undefined) + PathBuilder: class { + moveTo = mock(() => this) + lineTo = mock(() => this) + cubicTo = mock(() => this) + quadTo = mock(() => this) + close = mock(() => this) delete = mock(() => undefined) + detachAndDelete = mock(() => ({ delete: mock(() => undefined) })) }, LTRBRect: mock((...args: number[]) => args), Color4f: mock((...args: number[]) => new Float32Array(args)), diff --git a/tests/engine/vector/path-rendering.test.ts b/tests/engine/vector/path-rendering.test.ts index b8127ee22..a521c3ff4 100644 --- a/tests/engine/vector/path-rendering.test.ts +++ b/tests/engine/vector/path-rendering.test.ts @@ -41,9 +41,41 @@ function createCanvasKit() { } } + class MockPathBuilder { + operations: string[] = [] + fillType: number | null = null + + moveTo(x: number, y: number): void { + this.operations.push(`M${x},${y}`) + } + + lineTo(x: number, y: number): void { + this.operations.push(`L${x},${y}`) + } + + cubicTo(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number): void { + this.operations.push(`C${x1},${y1},${x2},${y2},${x3},${y3}`) + } + + close(): void { + this.operations.push('Z') + } + + setFillType(value: number): void { + this.fillType = value + } + + detachAndDelete(): MockPath { + const path = new MockPath() + path.operations = [...this.operations] + path.fillType = this.fillType + return path + } + } + return { ck: { - Path: MockPath, + PathBuilder: MockPathBuilder, FillType: { Winding: 0, EvenOdd: 1 } } as CanvasKit, paths