pixelweaver 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (392) hide show
  1. package/.env.development +1 -0
  2. package/.github/workflows/ci.yml +22 -0
  3. package/.github/workflows/publish.yml +18 -0
  4. package/.prettierignore +5 -0
  5. package/.prettierrc +16 -0
  6. package/.python-version +1 -0
  7. package/.rlsbl/bases/.github/workflows/ci.yml +21 -0
  8. package/.rlsbl/bases/.github/workflows/publish.yml +18 -0
  9. package/.rlsbl/bases/.rlsbl/changes/unreleased.jsonl +0 -0
  10. package/.rlsbl/bases/.rlsbl/hooks/post-release.sh +8 -0
  11. package/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +5 -0
  12. package/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +8 -0
  13. package/.rlsbl/bases/.rlsbl/lint/go.toml +17 -0
  14. package/.rlsbl/bases/.rlsbl/lint/npm.toml +19 -0
  15. package/.rlsbl/bases/.rlsbl/lint/python.toml +25 -0
  16. package/.rlsbl/bases/CHANGELOG.md +5 -0
  17. package/.rlsbl/bases/LICENSE +21 -0
  18. package/.rlsbl/changes/.validated +1 -0
  19. package/.rlsbl/changes/0.1.0.jsonl +85 -0
  20. package/.rlsbl/changes/0.1.0.md +13 -0
  21. package/.rlsbl/changes/unreleased.jsonl +0 -0
  22. package/.rlsbl/config.json +6 -0
  23. package/.rlsbl/hashes.json +14 -0
  24. package/.rlsbl/hooks/post-release.sh +8 -0
  25. package/.rlsbl/hooks/pre-checks.sh +5 -0
  26. package/.rlsbl/hooks/pre-release.sh +8 -0
  27. package/.rlsbl/lint/go.toml +17 -0
  28. package/.rlsbl/lint/npm.toml +19 -0
  29. package/.rlsbl/lint/python.toml +25 -0
  30. package/.rlsbl/releases/unreleased.toml +0 -0
  31. package/.rlsbl/releases/v0.1.0.toml +3 -0
  32. package/.rlsbl/version +1 -0
  33. package/.selfdoc/hashes/hashes.json +146 -0
  34. package/.strictcli/schema.json +227 -0
  35. package/CHANGELOG.md +17 -0
  36. package/CLAUDE.md +100 -0
  37. package/LICENSE +21 -0
  38. package/README.md +116 -0
  39. package/assets/icon.png +0 -0
  40. package/docs/_README.md +117 -0
  41. package/docs/cli-config.md +35 -0
  42. package/docs/cli-dev.md +21 -0
  43. package/docs/cli-diagnose.md +12 -0
  44. package/docs/cli-index.md +30 -0
  45. package/docs/cli-list.md +18 -0
  46. package/docs/cli-mcp.md +18 -0
  47. package/docs/cli-new.md +26 -0
  48. package/docs/cli-open.md +21 -0
  49. package/docs/cli-serve.md +21 -0
  50. package/docs/cli-stop.md +12 -0
  51. package/docs/gen-index.md +36 -0
  52. package/docs/index.md +13 -0
  53. package/docs/server-src-pixelweaver-__main__.md +12 -0
  54. package/docs/server-src-pixelweaver-autosave.md +12 -0
  55. package/docs/server-src-pixelweaver-bridge.md +12 -0
  56. package/docs/server-src-pixelweaver-cli.md +12 -0
  57. package/docs/server-src-pixelweaver-config.md +12 -0
  58. package/docs/server-src-pixelweaver-connections.md +12 -0
  59. package/docs/server-src-pixelweaver-main.md +12 -0
  60. package/docs/server-src-pixelweaver-mcp_bridge.md +12 -0
  61. package/docs/server-src-pixelweaver-mcp_drawing_tools.md +12 -0
  62. package/docs/server-src-pixelweaver-mcp_export_tools.md +12 -0
  63. package/docs/server-src-pixelweaver-mcp_frame_tools.md +12 -0
  64. package/docs/server-src-pixelweaver-mcp_history_tools.md +12 -0
  65. package/docs/server-src-pixelweaver-mcp_layer_tools.md +12 -0
  66. package/docs/server-src-pixelweaver-mcp_lock.md +12 -0
  67. package/docs/server-src-pixelweaver-mcp_project_tools.md +12 -0
  68. package/docs/server-src-pixelweaver-mcp_read_tools.md +12 -0
  69. package/docs/server-src-pixelweaver-mcp_registry.md +12 -0
  70. package/docs/server-src-pixelweaver-mcp_resources.md +12 -0
  71. package/docs/server-src-pixelweaver-mcp_server.md +12 -0
  72. package/docs/server-src-pixelweaver-protocol.md +12 -0
  73. package/docs/server-src-pixelweaver-state.md +12 -0
  74. package/docs/server-src-pixelweaver-storage.md +12 -0
  75. package/docs/server-src-pixelweaver-websocket.md +12 -0
  76. package/docs/server-src-pixelweaver.md +12 -0
  77. package/e2e/app-launch.test.ts +35 -0
  78. package/e2e/menus.test.ts +26 -0
  79. package/e2e/tools.test.ts +27 -0
  80. package/e2e/undo-redo.test.ts +11 -0
  81. package/eslint.config.js +62 -0
  82. package/index.html +13 -0
  83. package/package.json +48 -0
  84. package/playwright.config.ts +19 -0
  85. package/plugins/builtin/.gitkeep +0 -0
  86. package/plugins/builtin/advanced-fill-tool.ts +146 -0
  87. package/plugins/builtin/circle-tool.ts +186 -0
  88. package/plugins/builtin/diamond-tool.ts +182 -0
  89. package/plugins/builtin/dither-tool.ts +186 -0
  90. package/plugins/builtin/drawing-primitives-plugin.ts +362 -0
  91. package/plugins/builtin/drawing-utils.test.ts +495 -0
  92. package/plugins/builtin/drawing-utils.ts +431 -0
  93. package/plugins/builtin/effects/blur.ts +97 -0
  94. package/plugins/builtin/effects/color-effects.ts +278 -0
  95. package/plugins/builtin/effects/flip.ts +83 -0
  96. package/plugins/builtin/effects/glow.ts +118 -0
  97. package/plugins/builtin/effects/outline.ts +110 -0
  98. package/plugins/builtin/effects/rotate.ts +357 -0
  99. package/plugins/builtin/effects/scale.ts +258 -0
  100. package/plugins/builtin/effects/shadow.ts +111 -0
  101. package/plugins/builtin/effects/sharpen.ts +102 -0
  102. package/plugins/builtin/effects.test.ts +715 -0
  103. package/plugins/builtin/eraser-tool.ts +23 -0
  104. package/plugins/builtin/eyedropper-tool.ts +83 -0
  105. package/plugins/builtin/fill-tool.ts +93 -0
  106. package/plugins/builtin/gradient-tool.ts +204 -0
  107. package/plugins/builtin/gradient.test.ts +142 -0
  108. package/plugins/builtin/importers/aseprite-importer-plugin.ts +174 -0
  109. package/plugins/builtin/importers/aseprite-parser.ts +497 -0
  110. package/plugins/builtin/importers/piskel-importer-plugin.ts +222 -0
  111. package/plugins/builtin/importers/sky-spec-plugin.ts +409 -0
  112. package/plugins/builtin/line-tool.ts +267 -0
  113. package/plugins/builtin/make-stroke-tool.ts +271 -0
  114. package/plugins/builtin/noise-dither.test.ts +151 -0
  115. package/plugins/builtin/noise-tool.ts +131 -0
  116. package/plugins/builtin/pattern-stamp-tool.ts +162 -0
  117. package/plugins/builtin/pencil-tool.ts +25 -0
  118. package/plugins/builtin/rect-tool.ts +179 -0
  119. package/plugins/builtin/selection-tool.ts +388 -0
  120. package/plugins/builtin/selection.test.ts +195 -0
  121. package/plugins/builtin/tool-plugins.test.ts +529 -0
  122. package/public/favicon.svg +7 -0
  123. package/public/sw.js +91 -0
  124. package/pyproject.toml +49 -0
  125. package/scripts/eslint-wave-a-list.sh +24 -0
  126. package/scripts/eslint-wave-a-status.sh +25 -0
  127. package/scripts/fix-index-signature-access.py +127 -0
  128. package/scripts/fix-unchecked-index.py +128 -0
  129. package/scripts/fix-unchecked-vars.py +127 -0
  130. package/scripts/fix-wave-a-bangs.py +36 -0
  131. package/scripts/fix-wave-a-templates.py +108 -0
  132. package/scripts/fix-wave-b-void-expr.py +167 -0
  133. package/scripts/generate-command-params.py +540 -0
  134. package/scripts/migrate-single-frame-to-multi.py +171 -0
  135. package/scripts/smoke-test.sh +77 -0
  136. package/selfdoc.json +10 -0
  137. package/server/README.md +0 -0
  138. package/server/src/pixelweaver/__init__.py +1 -0
  139. package/server/src/pixelweaver/__main__.py +4 -0
  140. package/server/src/pixelweaver/autosave.py +114 -0
  141. package/server/src/pixelweaver/bridge.py +127 -0
  142. package/server/src/pixelweaver/cli.py +199 -0
  143. package/server/src/pixelweaver/config.py +24 -0
  144. package/server/src/pixelweaver/connections.py +54 -0
  145. package/server/src/pixelweaver/main.py +271 -0
  146. package/server/src/pixelweaver/mcp_bridge.py +189 -0
  147. package/server/src/pixelweaver/mcp_drawing_tools.py +178 -0
  148. package/server/src/pixelweaver/mcp_export_tools.py +291 -0
  149. package/server/src/pixelweaver/mcp_frame_tools.py +423 -0
  150. package/server/src/pixelweaver/mcp_history_tools.py +106 -0
  151. package/server/src/pixelweaver/mcp_layer_tools.py +64 -0
  152. package/server/src/pixelweaver/mcp_lock.py +37 -0
  153. package/server/src/pixelweaver/mcp_project_tools.py +302 -0
  154. package/server/src/pixelweaver/mcp_read_tools.py +163 -0
  155. package/server/src/pixelweaver/mcp_registry.py +247 -0
  156. package/server/src/pixelweaver/mcp_resources.py +312 -0
  157. package/server/src/pixelweaver/mcp_server.py +234 -0
  158. package/server/src/pixelweaver/protocol.py +219 -0
  159. package/server/src/pixelweaver/state.py +267 -0
  160. package/server/src/pixelweaver/storage.py +293 -0
  161. package/server/src/pixelweaver/websocket.py +282 -0
  162. package/server/tests/__init__.py +0 -0
  163. package/server/tests/conftest.py +17 -0
  164. package/server/tests/test_api.py +96 -0
  165. package/server/tests/test_bridge.py +161 -0
  166. package/server/tests/test_health.py +9 -0
  167. package/server/tests/test_integration.py +86 -0
  168. package/server/tests/test_mcp_bridge.py +293 -0
  169. package/server/tests/test_mcp_lock.py +34 -0
  170. package/server/tests/test_mcp_registry.py +679 -0
  171. package/server/tests/test_mcp_resources.py +648 -0
  172. package/server/tests/test_protocol.py +125 -0
  173. package/server/tests/test_state.py +87 -0
  174. package/server/tests/test_storage.py +306 -0
  175. package/server/tests/test_websocket.py +275 -0
  176. package/src/App.svelte +107 -0
  177. package/src/app.css +215 -0
  178. package/src/lib/animation/AnimationPreviewPanel.svelte +667 -0
  179. package/src/lib/animation/animation-commands.test.ts +228 -0
  180. package/src/lib/animation/animation-commands.ts +540 -0
  181. package/src/lib/animation/animation-preview-panel-plugin.ts +25 -0
  182. package/src/lib/animation/animation-preview.svelte.ts +151 -0
  183. package/src/lib/animation/clipboard.ts +134 -0
  184. package/src/lib/animation/frame-model.svelte.ts +437 -0
  185. package/src/lib/animation/frame-model.test.ts +314 -0
  186. package/src/lib/animation/frame-selection.svelte.ts +77 -0
  187. package/src/lib/animation/frame-tags.svelte.ts +238 -0
  188. package/src/lib/animation/frame-tags.test.ts +136 -0
  189. package/src/lib/animation/import.test.ts +141 -0
  190. package/src/lib/animation/import.ts +112 -0
  191. package/src/lib/animation/spritesheet-export.test.ts +239 -0
  192. package/src/lib/animation/spritesheet-export.ts +314 -0
  193. package/src/lib/canvas/CanvasViewport.svelte +216 -0
  194. package/src/lib/canvas/canvas-init-plugin.ts +178 -0
  195. package/src/lib/canvas/canvas-renderer.ts +408 -0
  196. package/src/lib/canvas/canvas-state.svelte.ts +232 -0
  197. package/src/lib/canvas/canvas-state.test.ts +139 -0
  198. package/src/lib/canvas/input-handler.ts +221 -0
  199. package/src/lib/canvas/input-plugin.ts +150 -0
  200. package/src/lib/canvas/onion-skin.ts +94 -0
  201. package/src/lib/canvas/pixel-buffer.test.ts +249 -0
  202. package/src/lib/canvas/pixel-buffer.ts +151 -0
  203. package/src/lib/canvas/render-state.svelte.ts +18 -0
  204. package/src/lib/canvas/shape-preview-state.svelte.ts +36 -0
  205. package/src/lib/canvas/tile-mode.test.ts +53 -0
  206. package/src/lib/canvas/tile-mode.ts +92 -0
  207. package/src/lib/canvas/viewport-utils.ts +24 -0
  208. package/src/lib/canvas/zoom-utils.ts +31 -0
  209. package/src/lib/color/.gitkeep +0 -0
  210. package/src/lib/color/color-commands.ts +87 -0
  211. package/src/lib/color/color-state.svelte.ts +98 -0
  212. package/src/lib/color/color-state.test.ts +91 -0
  213. package/src/lib/color/color-utils.test.ts +220 -0
  214. package/src/lib/color/color-utils.ts +243 -0
  215. package/src/lib/color/palette-state.svelte.ts +127 -0
  216. package/src/lib/color/palette-state.test.ts +154 -0
  217. package/src/lib/color/palette.ts +79 -0
  218. package/src/lib/core/bootstrap.ts +66 -0
  219. package/src/lib/core/command-params.generated.ts +1549 -0
  220. package/src/lib/core/command-params.ts +20 -0
  221. package/src/lib/core/command-runner.ts +79 -0
  222. package/src/lib/core/commands.ts +134 -0
  223. package/src/lib/core/dispatcher.test.ts +548 -0
  224. package/src/lib/core/dispatcher.ts +361 -0
  225. package/src/lib/core/index.test.ts +7 -0
  226. package/src/lib/core/notification-state.svelte.ts +119 -0
  227. package/src/lib/core/plugin-api.ts +210 -0
  228. package/src/lib/core/plugin-discovery.test.ts +53 -0
  229. package/src/lib/core/plugin-discovery.ts +65 -0
  230. package/src/lib/core/plugin-loader.test.ts +159 -0
  231. package/src/lib/core/plugin-loader.ts +240 -0
  232. package/src/lib/core/plugin-types.ts +286 -0
  233. package/src/lib/core/registries.svelte.ts +74 -0
  234. package/src/lib/core/tool-options-state.svelte.ts +61 -0
  235. package/src/lib/dock/DockLayout.svelte +375 -0
  236. package/src/lib/dock/TabAddMenu.svelte +90 -0
  237. package/src/lib/dock/dock-persistence.ts +46 -0
  238. package/src/lib/dock/dock-plugin.ts +49 -0
  239. package/src/lib/dock/dock-presets.ts +156 -0
  240. package/src/lib/dock/dock-state.svelte.ts +77 -0
  241. package/src/lib/dock/dock-types.ts +2 -0
  242. package/src/lib/dock/dockview-theme.css +226 -0
  243. package/src/lib/dock/dockview-theme.ts +17 -0
  244. package/src/lib/dock/header-action-renderer.ts +77 -0
  245. package/src/lib/edit/clipboard-state.ts +34 -0
  246. package/src/lib/export/download.ts +201 -0
  247. package/src/lib/export/png-metadata.ts +181 -0
  248. package/src/lib/history/ActionLogPanel.svelte +418 -0
  249. package/src/lib/history/action-log-panel-plugin.ts +24 -0
  250. package/src/lib/history/action-log.svelte.ts +172 -0
  251. package/src/lib/history/action-log.test.ts +168 -0
  252. package/src/lib/history/history-commands.ts +403 -0
  253. package/src/lib/history/macros.svelte.ts +320 -0
  254. package/src/lib/history/macros.test.ts +224 -0
  255. package/src/lib/history/replay-engine.test.ts +241 -0
  256. package/src/lib/history/replay-engine.ts +149 -0
  257. package/src/lib/history/spec-format.test.ts +250 -0
  258. package/src/lib/history/spec-format.ts +210 -0
  259. package/src/lib/iso/SeamCheckerPanel.svelte +251 -0
  260. package/src/lib/iso/iso-math.test.ts +77 -0
  261. package/src/lib/iso/iso-math.ts +77 -0
  262. package/src/lib/iso/seam-checker-panel-plugin.ts +25 -0
  263. package/src/lib/iso/seam-checker.test.ts +126 -0
  264. package/src/lib/iso/seam-checker.ts +93 -0
  265. package/src/lib/iso/tessellation.ts +67 -0
  266. package/src/lib/layers/compositor.test.ts +193 -0
  267. package/src/lib/layers/compositor.ts +175 -0
  268. package/src/lib/layers/layer-commands.test.ts +263 -0
  269. package/src/lib/layers/layer-commands.ts +429 -0
  270. package/src/lib/layers/layer-tree.svelte.ts +516 -0
  271. package/src/lib/layers/layer-tree.test.ts +383 -0
  272. package/src/lib/layers/layer-types.ts +56 -0
  273. package/src/lib/leveleditor/LevelEditorViewport.svelte +1808 -0
  274. package/src/lib/leveleditor/MapPropertiesPanel.svelte +266 -0
  275. package/src/lib/leveleditor/TilePickerPanel.svelte +324 -0
  276. package/src/lib/leveleditor/depth-sort.test.ts +70 -0
  277. package/src/lib/leveleditor/depth-sort.ts +39 -0
  278. package/src/lib/leveleditor/level-editor-commands.ts +353 -0
  279. package/src/lib/leveleditor/level-editor-viewport-plugin.ts +25 -0
  280. package/src/lib/leveleditor/map-properties-panel-plugin.ts +25 -0
  281. package/src/lib/leveleditor/map-state.svelte.ts +372 -0
  282. package/src/lib/leveleditor/map-state.test.ts +243 -0
  283. package/src/lib/leveleditor/tile-picker-panel-plugin.ts +25 -0
  284. package/src/lib/leveleditor/tile-source-registry.svelte.ts +91 -0
  285. package/src/lib/leveleditor/tiled-export.test.ts +144 -0
  286. package/src/lib/leveleditor/tiled-export.ts +374 -0
  287. package/src/lib/pywebview.d.ts +15 -0
  288. package/src/lib/recovery/.gitkeep +0 -0
  289. package/src/lib/recovery/idb-store.ts +118 -0
  290. package/src/lib/recovery/recovery-manager.test.ts +321 -0
  291. package/src/lib/recovery/recovery-manager.ts +115 -0
  292. package/src/lib/recovery/recovery-plugin.ts +55 -0
  293. package/src/lib/recovery/recovery-state.svelte.ts +21 -0
  294. package/src/lib/recovery/sw-plugin.ts +18 -0
  295. package/src/lib/recovery/sw-register.ts +17 -0
  296. package/src/lib/save/directory-format.ts +42 -0
  297. package/src/lib/save/project-snapshot.ts +139 -0
  298. package/src/lib/save/recent-projects.ts +56 -0
  299. package/src/lib/save/save-state.svelte.ts +35 -0
  300. package/src/lib/save/storage.ts +167 -0
  301. package/src/lib/save/zip-format.ts +45 -0
  302. package/src/lib/shortcuts/.gitkeep +0 -0
  303. package/src/lib/shortcuts/ShortcutEditorPanel.svelte +690 -0
  304. package/src/lib/shortcuts/default-bindings.ts +61 -0
  305. package/src/lib/shortcuts/shortcut-editor-panel-plugin.ts +25 -0
  306. package/src/lib/shortcuts/shortcut-init.ts +380 -0
  307. package/src/lib/shortcuts/shortcut-manager.test.ts +466 -0
  308. package/src/lib/shortcuts/shortcut-manager.ts +281 -0
  309. package/src/lib/shortcuts/shortcut-state.svelte.ts +78 -0
  310. package/src/lib/shortcuts/shortcuts-plugin.ts +17 -0
  311. package/src/lib/sync/patch-applicator.ts +300 -0
  312. package/src/lib/sync/patch-types.ts +65 -0
  313. package/src/lib/sync/project-manager.ts +108 -0
  314. package/src/lib/sync/sync-init.ts +152 -0
  315. package/src/lib/sync/sync-plugin.ts +19 -0
  316. package/src/lib/sync/sync-state.svelte.ts +56 -0
  317. package/src/lib/sync/ws-client.test.ts +604 -0
  318. package/src/lib/sync/ws-client.ts +574 -0
  319. package/src/lib/tools/.gitkeep +0 -0
  320. package/src/lib/ui/.gitkeep +0 -0
  321. package/src/lib/ui/AboutDialog.svelte +113 -0
  322. package/src/lib/ui/ColorPicker.svelte +761 -0
  323. package/src/lib/ui/ContextMenu.svelte +216 -0
  324. package/src/lib/ui/ExportDialog.svelte +747 -0
  325. package/src/lib/ui/FrameStrip.svelte +854 -0
  326. package/src/lib/ui/LayerPanel.svelte +810 -0
  327. package/src/lib/ui/MenuBar.svelte +590 -0
  328. package/src/lib/ui/NewProjectDialog.svelte +803 -0
  329. package/src/lib/ui/PluginManagerPanel.svelte +475 -0
  330. package/src/lib/ui/PromptDialog.svelte +252 -0
  331. package/src/lib/ui/RecoveryDialog.svelte +295 -0
  332. package/src/lib/ui/ResizeDialog.svelte +416 -0
  333. package/src/lib/ui/StatusBar.svelte +145 -0
  334. package/src/lib/ui/ToolbarPanel.svelte +488 -0
  335. package/src/lib/ui/animation-menu-commands.ts +194 -0
  336. package/src/lib/ui/command-palette/CommandPalette.svelte +232 -0
  337. package/src/lib/ui/command-palette/command-palette-plugin.ts +30 -0
  338. package/src/lib/ui/command-palette/command-palette-state.svelte.ts +190 -0
  339. package/src/lib/ui/command-palette/command-palette.test.ts +129 -0
  340. package/src/lib/ui/dialog-state.svelte.ts +70 -0
  341. package/src/lib/ui/edit-commands.ts +271 -0
  342. package/src/lib/ui/file-commands.ts +275 -0
  343. package/src/lib/ui/file-open.ts +99 -0
  344. package/src/lib/ui/help-commands.ts +93 -0
  345. package/src/lib/ui/image-commands.ts +181 -0
  346. package/src/lib/ui/layer-menu-commands.ts +420 -0
  347. package/src/lib/ui/menu-builder.ts +224 -0
  348. package/src/lib/ui/notifications/NotificationBanner.svelte +137 -0
  349. package/src/lib/ui/notifications/notification-plugin.ts +29 -0
  350. package/src/lib/ui/notifications/notification-state.svelte.ts +9 -0
  351. package/src/lib/ui/plugin-manager-panel-plugin.ts +26 -0
  352. package/src/lib/ui/plugin-state.svelte.ts +62 -0
  353. package/src/lib/ui/select-commands.ts +75 -0
  354. package/src/lib/ui/theme-plugin.ts +18 -0
  355. package/src/lib/ui/theme.svelte.ts +45 -0
  356. package/src/lib/ui/theme.test.ts +51 -0
  357. package/src/lib/ui/toolbar-config.ts +90 -0
  358. package/src/lib/ui/toolbar-plugin.ts +39 -0
  359. package/src/lib/ui/view-commands.ts +629 -0
  360. package/src/lib/variants/BisectionExportDialog.svelte +500 -0
  361. package/src/lib/variants/VariantPanel.svelte +822 -0
  362. package/src/lib/variants/bisection-export.test.ts +113 -0
  363. package/src/lib/variants/bisection-export.ts +148 -0
  364. package/src/lib/variants/palette-extraction.test.ts +111 -0
  365. package/src/lib/variants/palette-extraction.ts +84 -0
  366. package/src/lib/variants/palette-interpolation.test.ts +113 -0
  367. package/src/lib/variants/palette-interpolation.ts +87 -0
  368. package/src/lib/variants/palette-swap.test.ts +101 -0
  369. package/src/lib/variants/palette-swap.ts +114 -0
  370. package/src/lib/variants/variant-commands.ts +594 -0
  371. package/src/lib/variants/variant-panel-plugin.ts +27 -0
  372. package/src/lib/variants/variant-randomizer.ts +101 -0
  373. package/src/lib/variants/variant-state.svelte.ts +166 -0
  374. package/src/lib/variants/variant-state.test.ts +138 -0
  375. package/src/main.ts +14 -0
  376. package/src/vite-env.d.ts +3 -0
  377. package/svelte.config.js +2 -0
  378. package/todo/.done/audit-design-decisions.md +812 -0
  379. package/todo/.done/audit-implementation-plan.md +1235 -0
  380. package/todo/.done/happy-path-polish.md +177 -0
  381. package/todo/.done/pixelweaver-full-build.md +937 -0
  382. package/todo/.done/server-multi-frame-design.md +405 -0
  383. package/todo/.done/typed-dispatcher-design.md +435 -0
  384. package/todo/.done/unified-toolbar-and-action-system.md +323 -0
  385. package/todo/.obsolete/comprehensive-audit-obsolete-items.md +33 -0
  386. package/todo/.obsolete/tauri-desktop-bundle.md +424 -0
  387. package/todo/comprehensive-audit.md +1085 -0
  388. package/tsconfig.app.json +26 -0
  389. package/tsconfig.json +7 -0
  390. package/tsconfig.node.json +20 -0
  391. package/uv.lock +1167 -0
  392. package/vite.config.ts +32 -0
@@ -0,0 +1,189 @@
1
+ """MCP Bridge -- HTTP client that forwards MCP state to/from the collab server.
2
+
3
+ Instead of the MCP server managing its own isolated state, every tool
4
+ execution round-trips through the collaboration server's REST API:
5
+
6
+ 1. Before execution: pull the latest state from the collab server
7
+ 2. Execute the tool against the local (synced) state
8
+ 3. After execution: push the modified state back, triggering a
9
+ broadcast to all WebSocket clients (the frontend)
10
+
11
+ This ensures the collab server remains the single source of truth and
12
+ that every MCP mutation is visible to the frontend in real time.
13
+
14
+ The collab server must be running on COLLAB_SERVER_URL for MCP to work.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import base64
20
+ import logging
21
+ from typing import Any
22
+
23
+ import httpx
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ COLLAB_SERVER_URL = "http://localhost:7779"
28
+
29
+ # Reusable client -- created lazily, lives for the process lifetime.
30
+ # The MCP stdio server is single-threaded so no concurrency concerns.
31
+ _client: httpx.AsyncClient | None = None
32
+
33
+
34
+ def _get_client() -> httpx.AsyncClient:
35
+ """Return (and lazily create) the shared HTTP client."""
36
+ global _client
37
+ if _client is None:
38
+ _client = httpx.AsyncClient(base_url=COLLAB_SERVER_URL, timeout=30.0)
39
+ return _client
40
+
41
+
42
+ class CollabServerUnreachableError(Exception):
43
+ """Raised when the collab server is not reachable."""
44
+
45
+
46
+ async def health_check() -> bool:
47
+ """Return True if the collab server is reachable."""
48
+ try:
49
+ resp = await _get_client().get("/health")
50
+ return resp.status_code == 200
51
+ except httpx.HTTPError:
52
+ return False
53
+
54
+
55
+ async def pull_full_state() -> dict[str, Any]:
56
+ """Fetch full serialized state from the collab server.
57
+
58
+ Returns the dict produced by GET /api/state/full.
59
+ Raises CollabServerUnreachableError if the server cannot be reached.
60
+ """
61
+ try:
62
+ resp = await _get_client().get("/api/state/full")
63
+ resp.raise_for_status()
64
+ return resp.json()
65
+ except httpx.HTTPError as exc:
66
+ raise CollabServerUnreachableError(
67
+ f"Cannot reach collab server at {COLLAB_SERVER_URL}: {exc}"
68
+ ) from exc
69
+
70
+
71
+ async def push_full_state(state_dict: dict[str, Any]) -> None:
72
+ """Push full serialized state to the collab server and trigger broadcast.
73
+
74
+ Sends POST /api/state/sync with the full state payload.
75
+ Raises CollabServerUnreachableError if the server cannot be reached.
76
+ """
77
+ try:
78
+ resp = await _get_client().post("/api/state/sync", json=state_dict)
79
+ resp.raise_for_status()
80
+ except httpx.HTTPError as exc:
81
+ raise CollabServerUnreachableError(
82
+ f"Cannot reach collab server at {COLLAB_SERVER_URL}: {exc}"
83
+ ) from exc
84
+
85
+
86
+ def serialize_state(state: Any) -> dict[str, Any]:
87
+ """Serialize a ServerState into a dict suitable for push_full_state.
88
+
89
+ Includes pixel data as base64-encoded strings so the JSON payload
90
+ stays valid (raw bytes are not JSON-serializable).
91
+ """
92
+ projects: dict[str, Any] = {}
93
+ for name, project in state.projects.items():
94
+ canvases: dict[str, Any] = {}
95
+ for cname, canvas in project.canvases.items():
96
+ layers_serialized = [dict(layer) for layer in canvas.layers]
97
+ # Serialize ALL frames with their per-layer pixel data
98
+ frames_serialized = [
99
+ {
100
+ "id": frame.id,
101
+ "duration_ms": frame.duration_ms,
102
+ "pixel_data": {
103
+ layer_id: base64.b64encode(data).decode("ascii")
104
+ for layer_id, data in frame.pixel_data.items()
105
+ },
106
+ }
107
+ for frame in canvas.frames
108
+ ]
109
+ canvases[cname] = {
110
+ "name": canvas.name,
111
+ "width": canvas.width,
112
+ "height": canvas.height,
113
+ "layers": layers_serialized,
114
+ "frames": frames_serialized,
115
+ "current_frame_index": canvas.current_frame_index,
116
+ "global_fps": canvas.global_fps,
117
+ }
118
+ projects[name] = {
119
+ "name": project.name,
120
+ "width": project.width,
121
+ "height": project.height,
122
+ "canvases": canvases,
123
+ "command_history": project.command_history,
124
+ "redo_stack": project.redo_stack,
125
+ }
126
+ return {
127
+ "projects": projects,
128
+ "active_project": state.active_project,
129
+ }
130
+
131
+
132
+ def deserialize_into_state(state: Any, data: dict[str, Any]) -> None:
133
+ """Overwrite a ServerState's contents from a serialized dict.
134
+
135
+ Inverse of serialize_state: restores projects, canvases, layers,
136
+ pixel data, and history from the dict pulled from the collab server.
137
+ """
138
+ import uuid
139
+
140
+ from pixelweaver.state import CanvasState, FrameState, ProjectState
141
+
142
+ state.projects.clear()
143
+ state.active_project = data.get("active_project")
144
+
145
+ for name, pdata in data.get("projects", {}).items():
146
+ project = ProjectState(
147
+ name=pdata["name"],
148
+ width=pdata["width"],
149
+ height=pdata["height"],
150
+ command_history=pdata.get("command_history", []),
151
+ redo_stack=pdata.get("redo_stack", []),
152
+ )
153
+ for cname, cdata in pdata.get("canvases", {}).items():
154
+ # Reconstruct frames from serialized data
155
+ if "frames" in cdata:
156
+ # New format: full frame list with per-frame pixel data
157
+ frames: list[FrameState] = []
158
+ for fdata in cdata["frames"]:
159
+ pixel_data: dict[str, bytes] = {
160
+ lid: base64.b64decode(b64)
161
+ for lid, b64 in fdata.get("pixel_data", {}).items()
162
+ }
163
+ frames.append(FrameState(
164
+ id=fdata["id"],
165
+ duration_ms=fdata.get("duration_ms"),
166
+ pixel_data=pixel_data,
167
+ ))
168
+ else:
169
+ # Legacy format: flat pixel_data dict, single frame
170
+ pixel_data_legacy: dict[str, bytes] = {
171
+ lid: base64.b64decode(b64)
172
+ for lid, b64 in cdata.get("pixel_data", {}).items()
173
+ }
174
+ frames = [FrameState(
175
+ id=str(uuid.uuid4()),
176
+ pixel_data=pixel_data_legacy,
177
+ )]
178
+
179
+ canvas = CanvasState(
180
+ name=cdata["name"],
181
+ width=cdata["width"],
182
+ height=cdata["height"],
183
+ layers=cdata.get("layers", []),
184
+ frames=frames,
185
+ current_frame_index=cdata.get("current_frame_index", 0),
186
+ global_fps=cdata.get("global_fps", 12.0),
187
+ )
188
+ project.canvases[cname] = canvas
189
+ state.projects[name] = project
@@ -0,0 +1,178 @@
1
+ """Drawing tools for MCP registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from .mcp_registry import MCPCommandRegistry
9
+
10
+
11
+ def register_drawing_tools(registry: MCPCommandRegistry) -> None:
12
+ """Register all drawing-related MCP tools."""
13
+ drawing_tools = [
14
+ ("draw_pixels", "Draw pixels at specified coordinates with a given color", {
15
+ "type": "object",
16
+ "properties": {
17
+ "pixels": {
18
+ "type": "array",
19
+ "items": {
20
+ "type": "object",
21
+ "properties": {
22
+ "x": {"type": "integer"},
23
+ "y": {"type": "integer"},
24
+ },
25
+ "required": ["x", "y"],
26
+ },
27
+ "description": "Array of {x, y} pixel positions",
28
+ },
29
+ "color": {
30
+ "type": "string",
31
+ "description": "RGBA hex color, e.g. '#ff0000ff'",
32
+ },
33
+ "layer_id": {"type": "string", "description": "Target layer ID"},
34
+ },
35
+ "required": ["pixels", "color"],
36
+ }),
37
+ ("erase_pixels", "Erase pixels at specified coordinates (set to transparent)", {
38
+ "type": "object",
39
+ "properties": {
40
+ "pixels": {
41
+ "type": "array",
42
+ "items": {
43
+ "type": "object",
44
+ "properties": {
45
+ "x": {"type": "integer"},
46
+ "y": {"type": "integer"},
47
+ },
48
+ "required": ["x", "y"],
49
+ },
50
+ },
51
+ "layer_id": {"type": "string"},
52
+ },
53
+ "required": ["pixels"],
54
+ }),
55
+ ("flood_fill", "Fill a contiguous area with a color starting from a seed point", {
56
+ "type": "object",
57
+ "properties": {
58
+ "x": {"type": "integer", "description": "Seed X coordinate"},
59
+ "y": {"type": "integer", "description": "Seed Y coordinate"},
60
+ "color": {"type": "string", "description": "Fill color (RGBA hex)"},
61
+ "tolerance": {
62
+ "type": "integer",
63
+ "description": "Color match tolerance (0-255)",
64
+ "default": 0,
65
+ },
66
+ "layer_id": {"type": "string"},
67
+ },
68
+ "required": ["x", "y", "color"],
69
+ }),
70
+ ("draw_line", "Draw a line between two points", {
71
+ "type": "object",
72
+ "properties": {
73
+ "x1": {"type": "integer"},
74
+ "y1": {"type": "integer"},
75
+ "x2": {"type": "integer"},
76
+ "y2": {"type": "integer"},
77
+ "color": {"type": "string"},
78
+ "thickness": {"type": "integer", "default": 1},
79
+ "layer_id": {"type": "string"},
80
+ },
81
+ "required": ["x1", "y1", "x2", "y2", "color"],
82
+ }),
83
+ ("draw_rect", "Draw a rectangle (filled or outline)", {
84
+ "type": "object",
85
+ "properties": {
86
+ "x": {"type": "integer"},
87
+ "y": {"type": "integer"},
88
+ "width": {"type": "integer"},
89
+ "height": {"type": "integer"},
90
+ "color": {"type": "string"},
91
+ "filled": {"type": "boolean", "default": True},
92
+ "layer_id": {"type": "string"},
93
+ },
94
+ "required": ["x", "y", "width", "height", "color"],
95
+ }),
96
+ ("draw_ellipse", "Draw an ellipse (filled or outline)", {
97
+ "type": "object",
98
+ "properties": {
99
+ "cx": {"type": "integer", "description": "Center X"},
100
+ "cy": {"type": "integer", "description": "Center Y"},
101
+ "rx": {"type": "integer", "description": "Radius X"},
102
+ "ry": {"type": "integer", "description": "Radius Y"},
103
+ "color": {"type": "string"},
104
+ "filled": {"type": "boolean", "default": True},
105
+ "layer_id": {"type": "string"},
106
+ },
107
+ "required": ["cx", "cy", "rx", "ry", "color"],
108
+ }),
109
+ ("draw_diamond", "Draw a diamond shape", {
110
+ "type": "object",
111
+ "properties": {
112
+ "cx": {"type": "integer", "description": "Center X"},
113
+ "cy": {"type": "integer", "description": "Center Y"},
114
+ "rx": {"type": "integer", "description": "Half-width"},
115
+ "ry": {"type": "integer", "description": "Half-height"},
116
+ "color": {"type": "string"},
117
+ "filled": {"type": "boolean", "default": True},
118
+ "layer_id": {"type": "string"},
119
+ },
120
+ "required": ["cx", "cy", "rx", "ry", "color"],
121
+ }),
122
+ ("draw_gradient", "Draw a gradient between two colors", {
123
+ "type": "object",
124
+ "properties": {
125
+ "x": {"type": "integer"},
126
+ "y": {"type": "integer"},
127
+ "width": {"type": "integer"},
128
+ "height": {"type": "integer"},
129
+ "color_start": {"type": "string"},
130
+ "color_end": {"type": "string"},
131
+ "direction": {
132
+ "type": "string",
133
+ "enum": ["horizontal", "vertical", "diagonal"],
134
+ "default": "horizontal",
135
+ },
136
+ "layer_id": {"type": "string"},
137
+ },
138
+ "required": ["x", "y", "width", "height", "color_start", "color_end"],
139
+ }),
140
+ ("draw_noise", "Fill a region with random noise", {
141
+ "type": "object",
142
+ "properties": {
143
+ "x": {"type": "integer"},
144
+ "y": {"type": "integer"},
145
+ "width": {"type": "integer"},
146
+ "height": {"type": "integer"},
147
+ "colors": {
148
+ "type": "array",
149
+ "items": {"type": "string"},
150
+ "description": "Palette of colors to sample from",
151
+ },
152
+ "density": {"type": "number", "default": 0.5, "description": "0.0-1.0"},
153
+ "layer_id": {"type": "string"},
154
+ },
155
+ "required": ["x", "y", "width", "height", "colors"],
156
+ }),
157
+ ("draw_dither", "Apply a dither pattern between two colors", {
158
+ "type": "object",
159
+ "properties": {
160
+ "x": {"type": "integer"},
161
+ "y": {"type": "integer"},
162
+ "width": {"type": "integer"},
163
+ "height": {"type": "integer"},
164
+ "color_a": {"type": "string"},
165
+ "color_b": {"type": "string"},
166
+ "pattern": {
167
+ "type": "string",
168
+ "enum": ["checker", "ordered", "bayer"],
169
+ "default": "checker",
170
+ },
171
+ "layer_id": {"type": "string"},
172
+ },
173
+ "required": ["x", "y", "width", "height", "color_a", "color_b"],
174
+ }),
175
+ ]
176
+
177
+ for name, desc, schema in drawing_tools:
178
+ registry._register_drawing_tool(name, desc, schema)
@@ -0,0 +1,291 @@
1
+ """Export and curated high-level tools for MCP registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ from io import BytesIO
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from PIL import Image
10
+
11
+ from pixelweaver.storage import export_frame_png
12
+
13
+ if TYPE_CHECKING:
14
+ from .mcp_registry import MCPCommandRegistry
15
+
16
+ from .mcp_registry import ToolDef
17
+
18
+
19
+ def register_export_tools(registry: MCPCommandRegistry) -> None:
20
+ """Register all export and curated high-level MCP tools."""
21
+
22
+ # -- Export tools ---------------------------------------------------------
23
+
24
+ # export_png
25
+ async def handle_export_png(args: dict[str, Any]) -> dict[str, Any]:
26
+ project = registry.state.get_active_project()
27
+ if project is None:
28
+ return {"success": False, "error": "No active project"}
29
+
30
+ canvas_name = args.get("canvas_name")
31
+ if canvas_name is None:
32
+ canvas_name = next(iter(project.canvases.keys()), None)
33
+ if canvas_name is None:
34
+ return {"success": False, "error": "No canvas in project"}
35
+
36
+ try:
37
+ png_bytes = export_frame_png(project, canvas_name)
38
+ b64 = base64.b64encode(png_bytes).decode("ascii")
39
+ except ValueError as exc:
40
+ return {"success": False, "error": str(exc)}
41
+
42
+ return {"success": True, "image_base64": b64, "format": "png"}
43
+
44
+ registry._register(
45
+ ToolDef(
46
+ "export_png",
47
+ "Export the active canvas as a composited PNG (returns base64)",
48
+ {
49
+ "type": "object",
50
+ "properties": {
51
+ "canvas_name": {
52
+ "type": "string",
53
+ "description": "Canvas to export (default: first canvas)",
54
+ },
55
+ },
56
+ },
57
+ mutates=False,
58
+ ),
59
+ handle_export_png,
60
+ )
61
+
62
+ # export_spritesheet
63
+ async def handle_export_spritesheet(args: dict[str, Any]) -> dict[str, Any]:
64
+ project = registry.state.get_active_project()
65
+ if project is None:
66
+ return {"success": False, "error": "No active project"}
67
+
68
+ if not project.canvases:
69
+ return {"success": False, "error": "No canvases in project"}
70
+
71
+ canvas_name = args.get("canvas_name")
72
+ if canvas_name is None:
73
+ canvas_name = next(iter(project.canvases.keys()), None)
74
+ if canvas_name is None:
75
+ return {"success": False, "error": "No canvas in project"}
76
+
77
+ canvas = project.canvases.get(canvas_name)
78
+ if canvas is None:
79
+ return {"success": False, "error": f"Canvas {canvas_name!r} not found"}
80
+
81
+ direction = args.get("direction", "horizontal")
82
+
83
+ # Iterate animation frames within a single canvas and composite
84
+ # each frame's layers into a spritesheet strip.
85
+ frame_images: list[Image.Image] = []
86
+ try:
87
+ for i in range(canvas.frame_count):
88
+ png_bytes = export_frame_png(project, canvas_name, frame=i)
89
+ frame_images.append(Image.open(BytesIO(png_bytes)))
90
+ except (ValueError, IndexError) as exc:
91
+ return {"success": False, "error": str(exc)}
92
+
93
+ if not frame_images:
94
+ return {"success": False, "error": "No frames to export"}
95
+
96
+ # Build the spritesheet from all composited frames
97
+ frame_count = len(frame_images)
98
+ fw, fh = frame_images[0].width, frame_images[0].height
99
+
100
+ if direction == "vertical":
101
+ sheet = Image.new("RGBA", (fw, fh * frame_count), (0, 0, 0, 0))
102
+ for i, img in enumerate(frame_images):
103
+ sheet.paste(img, (0, i * fh))
104
+ else:
105
+ sheet = Image.new("RGBA", (fw * frame_count, fh), (0, 0, 0, 0))
106
+ for i, img in enumerate(frame_images):
107
+ sheet.paste(img, (i * fw, 0))
108
+
109
+ buf = BytesIO()
110
+ sheet.save(buf, format="PNG")
111
+ b64 = base64.b64encode(buf.getvalue()).decode("ascii")
112
+
113
+ return {
114
+ "success": True,
115
+ "image_base64": b64,
116
+ "format": "png",
117
+ "frame_count": frame_count,
118
+ "direction": direction,
119
+ "frame_width": fw,
120
+ "frame_height": fh,
121
+ }
122
+
123
+ registry._register(
124
+ ToolDef(
125
+ "export_spritesheet",
126
+ "Export all animation frames of a canvas as a spritesheet (returns base64 PNG)",
127
+ {
128
+ "type": "object",
129
+ "properties": {
130
+ "canvas_name": {
131
+ "type": "string",
132
+ "description": "Canvas to export (default: first canvas)",
133
+ },
134
+ "direction": {
135
+ "type": "string",
136
+ "enum": ["horizontal", "vertical"],
137
+ "description": "Layout direction (default: horizontal)",
138
+ },
139
+ },
140
+ },
141
+ mutates=False,
142
+ ),
143
+ handle_export_spritesheet,
144
+ )
145
+
146
+ # -- Curated high-level tools --------------------------------------------
147
+
148
+ # draw_shape: combines rect/ellipse/diamond/line
149
+ async def handle_draw_shape(args: dict[str, Any]) -> dict[str, Any]:
150
+ err = registry._require_active_project()
151
+ if err:
152
+ return err
153
+
154
+ shape = args["shape"]
155
+ params = {k: v for k, v in args.items() if k != "shape"}
156
+
157
+ # Map shape names to the underlying command types
158
+ shape_to_cmd = {
159
+ "rect": "draw_rect",
160
+ "ellipse": "draw_ellipse",
161
+ "diamond": "draw_diamond",
162
+ "line": "draw_line",
163
+ }
164
+ cmd_type = shape_to_cmd.get(shape)
165
+ if cmd_type is None:
166
+ return {"success": False, "error": f"Unknown shape: {shape}"}
167
+
168
+ # Remap generic x/y/width/height to shape-specific params
169
+ if shape == "line":
170
+ params = {
171
+ "x1": args["x"],
172
+ "y1": args["y"],
173
+ "x2": args["width"],
174
+ "y2": args["height"],
175
+ "color": args["color"],
176
+ "layer_id": args.get("layer_id"),
177
+ }
178
+ # Remove None values
179
+ params = {k: v for k, v in params.items() if v is not None}
180
+ elif shape in ("ellipse", "diamond"):
181
+ params = {
182
+ "cx": args["x"],
183
+ "cy": args["y"],
184
+ "rx": args["width"],
185
+ "ry": args["height"],
186
+ "color": args["color"],
187
+ "filled": args.get("filled", True),
188
+ "layer_id": args.get("layer_id"),
189
+ }
190
+ params = {k: v for k, v in params.items() if v is not None}
191
+
192
+ return await registry._dispatch_command(cmd_type, params)
193
+
194
+ registry._register(
195
+ ToolDef(
196
+ "draw_shape",
197
+ "Draw a shape (rect, ellipse, diamond, or line) on the canvas",
198
+ {
199
+ "type": "object",
200
+ "properties": {
201
+ "shape": {
202
+ "type": "string",
203
+ "enum": ["rect", "ellipse", "diamond", "line"],
204
+ },
205
+ "x": {"type": "integer", "description": "X (or x1 for line)"},
206
+ "y": {"type": "integer", "description": "Y (or y1 for line)"},
207
+ "width": {"type": "integer", "description": "Width (or x2 for line)"},
208
+ "height": {"type": "integer", "description": "Height (or y2 for line)"},
209
+ "color": {"type": "string"},
210
+ "filled": {"type": "boolean", "default": True},
211
+ "layer_id": {"type": "string"},
212
+ },
213
+ "required": ["shape", "x", "y", "width", "height", "color"],
214
+ },
215
+ mutates=True,
216
+ ),
217
+ handle_draw_shape,
218
+ )
219
+
220
+ # fill_area: flood_fill with tolerance
221
+ async def handle_fill_area(args: dict[str, Any]) -> dict[str, Any]:
222
+ err = registry._require_active_project()
223
+ if err:
224
+ return err
225
+ return await registry._dispatch_command("flood_fill", args)
226
+
227
+ registry._register(
228
+ ToolDef(
229
+ "fill_area",
230
+ "Fill a contiguous area starting from a seed point, with optional tolerance",
231
+ {
232
+ "type": "object",
233
+ "properties": {
234
+ "x": {"type": "integer"},
235
+ "y": {"type": "integer"},
236
+ "color": {"type": "string"},
237
+ "tolerance": {"type": "integer", "default": 0},
238
+ "layer_id": {"type": "string"},
239
+ },
240
+ "required": ["x", "y", "color"],
241
+ },
242
+ mutates=True,
243
+ ),
244
+ handle_fill_area,
245
+ )
246
+
247
+ # apply_effect
248
+ async def handle_apply_effect(args: dict[str, Any]) -> dict[str, Any]:
249
+ err = registry._require_active_project()
250
+ if err:
251
+ return err
252
+ return await registry._dispatch_command("apply_effect", args)
253
+
254
+ registry._register(
255
+ ToolDef(
256
+ "apply_effect",
257
+ "Apply a visual effect to the canvas or a region",
258
+ {
259
+ "type": "object",
260
+ "properties": {
261
+ "effect": {
262
+ "type": "string",
263
+ "enum": [
264
+ "blur", "sharpen", "invert", "grayscale",
265
+ "brightness", "contrast", "hue_shift",
266
+ "outline", "shadow", "pixelate",
267
+ ],
268
+ },
269
+ "intensity": {
270
+ "type": "number",
271
+ "default": 1.0,
272
+ "description": "Effect strength (0.0 - 10.0)",
273
+ },
274
+ "region": {
275
+ "type": "object",
276
+ "properties": {
277
+ "x": {"type": "integer"},
278
+ "y": {"type": "integer"},
279
+ "width": {"type": "integer"},
280
+ "height": {"type": "integer"},
281
+ },
282
+ "description": "Optional bounding region; omit for full canvas",
283
+ },
284
+ "layer_id": {"type": "string"},
285
+ },
286
+ "required": ["effect"],
287
+ },
288
+ mutates=True,
289
+ ),
290
+ handle_apply_effect,
291
+ )