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,247 @@
1
+ """MCP command registry for PixelWeaver.
2
+
3
+ Maps MCP tool names to internal state mutations and read operations.
4
+ Each tool handler receives validated arguments and returns a result dict.
5
+
6
+ Tools are split into three categories:
7
+ - Raw command tools: one per drawing/layer/frame/project/canvas/history/export command
8
+ - Curated high-level tools: combine multiple raw tools behind a friendlier API
9
+ - Read/introspection tools: return data without mutating state
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import base64
15
+ import time
16
+ import uuid
17
+ from typing import Any
18
+
19
+ from pixelweaver.connections import ConnectionManager
20
+ from pixelweaver.state import ServerState
21
+ from pixelweaver.storage import export_frame_png
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Tool definition helpers
25
+ # ---------------------------------------------------------------------------
26
+
27
+
28
+ class ToolDef:
29
+ """Lightweight descriptor for an MCP tool."""
30
+
31
+ def __init__(
32
+ self,
33
+ name: str,
34
+ description: str,
35
+ parameters: dict[str, Any],
36
+ *,
37
+ mutates: bool = True,
38
+ ) -> None:
39
+ self.name = name
40
+ self.description = description
41
+ self.parameters = parameters
42
+ # Whether the tool mutates state (controls thumbnail inclusion)
43
+ self.mutates = mutates
44
+
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # Registry
48
+ # ---------------------------------------------------------------------------
49
+
50
+
51
+ class MCPCommandRegistry:
52
+ """Registry that maps MCP tool names to command dispatch."""
53
+
54
+ def __init__(self, state: ServerState, connections: ConnectionManager) -> None:
55
+ self.state = state
56
+ self.connections = connections
57
+ self._handlers: dict[str, Any] = {}
58
+ self._tool_defs: dict[str, ToolDef] = {}
59
+ self._register_all()
60
+
61
+ # -- public API ----------------------------------------------------------
62
+
63
+ async def execute_tool(self, tool_name: str, arguments: dict[str, Any]) -> dict[str, Any]:
64
+ """Execute an MCP tool and return the result."""
65
+ handler = self._handlers.get(tool_name)
66
+ if handler is None:
67
+ return {"success": False, "error": f"Unknown tool: {tool_name}"}
68
+
69
+ tool_def = self._tool_defs.get(tool_name)
70
+
71
+ # Mutating tools take the shared lock so MCP and WebSocket mutation
72
+ # paths can't interleave a partial state change. Read-only tools skip
73
+ # the lock to keep introspection cheap and non-blocking.
74
+ try:
75
+ if tool_def and tool_def.mutates:
76
+ async with self.state.mutation_lock:
77
+ result = await handler(arguments)
78
+ else:
79
+ result = await handler(arguments)
80
+ except Exception as exc:
81
+ return {"success": False, "error": str(exc)}
82
+
83
+ # Attach thumbnail for mutation operations
84
+ if tool_def and tool_def.mutates:
85
+ thumb = self._get_active_canvas_thumbnail()
86
+ if thumb:
87
+ result["thumbnail"] = thumb
88
+
89
+ return result
90
+
91
+ def get_tool_definitions(self) -> list[dict[str, Any]]:
92
+ """Return MCP-compatible tool definitions for all registered tools."""
93
+ defs = []
94
+ for td in self._tool_defs.values():
95
+ defs.append({
96
+ "name": td.name,
97
+ "description": td.description,
98
+ "inputSchema": td.parameters,
99
+ })
100
+ return defs
101
+
102
+ def get_tool_def(self, name: str) -> ToolDef | None:
103
+ """Get a single tool definition by name."""
104
+ return self._tool_defs.get(name)
105
+
106
+ # -- registration --------------------------------------------------------
107
+
108
+ def _register(self, tool_def: ToolDef, handler: Any) -> None:
109
+ """Register a tool definition and its async handler."""
110
+ self._tool_defs[tool_def.name] = tool_def
111
+ self._handlers[tool_def.name] = handler
112
+
113
+ def _register_all(self) -> None:
114
+ """Register every tool via domain modules."""
115
+ from .mcp_drawing_tools import register_drawing_tools
116
+ from .mcp_export_tools import register_export_tools
117
+ from .mcp_frame_tools import register_frame_tools
118
+ from .mcp_history_tools import register_history_tools
119
+ from .mcp_layer_tools import register_layer_tools
120
+ from .mcp_project_tools import register_project_tools
121
+ from .mcp_read_tools import register_read_tools
122
+
123
+ register_drawing_tools(self)
124
+ register_layer_tools(self)
125
+ register_frame_tools(self)
126
+ register_project_tools(self)
127
+ register_history_tools(self)
128
+ register_export_tools(self)
129
+ register_read_tools(self)
130
+
131
+ # -- helpers -------------------------------------------------------------
132
+
133
+ def _get_active_canvas_thumbnail(self) -> str | None:
134
+ """Get a thumbnail of the first canvas in the active project."""
135
+ project = self.state.get_active_project()
136
+ if project is None:
137
+ return None
138
+ canvas = next(iter(project.canvases.values()), None)
139
+ if canvas is None:
140
+ return None
141
+ # Composite all visible layers
142
+ try:
143
+ png_bytes = export_frame_png(project, canvas.name)
144
+ # Re-encode as base64 thumbnail (the export already is a PNG)
145
+ return base64.b64encode(png_bytes).decode("ascii")
146
+ except Exception:
147
+ return None
148
+
149
+ def _make_command(
150
+ self,
151
+ cmd_type: str,
152
+ params: dict[str, Any],
153
+ *,
154
+ plugin: str = "mcp",
155
+ ) -> dict[str, Any]:
156
+ """Build a command dict matching the protocol format."""
157
+ return {
158
+ "type": cmd_type,
159
+ "plugin": plugin,
160
+ "version": "1.0.0",
161
+ "params": params,
162
+ "id": str(uuid.uuid4()),
163
+ "timestamp": int(time.time() * 1000),
164
+ }
165
+
166
+ async def _dispatch_command(
167
+ self,
168
+ cmd_type: str,
169
+ params: dict[str, Any],
170
+ *,
171
+ plugin: str = "mcp",
172
+ ) -> dict[str, Any]:
173
+ """Create a command, store in history, and broadcast to WebSocket clients."""
174
+ project = self.state.get_active_project()
175
+ if project is None:
176
+ return {"success": False, "error": "No active project"}
177
+
178
+ cmd = self._make_command(cmd_type, params, plugin=plugin)
179
+ project.command_history.append(cmd)
180
+ project.redo_stack.clear()
181
+
182
+ # Broadcast to all connected WebSocket clients
183
+ await self.connections.broadcast({
184
+ "type": "command_broadcast",
185
+ "command": cmd,
186
+ "source": "mcp",
187
+ })
188
+
189
+ return {"success": True, "command_id": cmd["id"]}
190
+
191
+ def _require_active_project(self) -> dict[str, Any] | None:
192
+ """Return an error dict if no project is active, else None."""
193
+ if self.state.get_active_project() is None:
194
+ return {"success": False, "error": "No active project"}
195
+ return None
196
+
197
+ def _register_drawing_tool(
198
+ self, name: str, description: str, schema: dict[str, Any],
199
+ ) -> None:
200
+ """Register a single drawing tool that dispatches a command.
201
+
202
+ Automatically injects an optional ``frame_index`` parameter so
203
+ MCP clients can target a specific animation frame. When provided,
204
+ the server sets ``canvas.current_frame_index`` before dispatching
205
+ the command (so the frontend draws on the correct frame) and
206
+ includes ``frame_index`` in the dispatched params for future
207
+ frontend-side routing.
208
+ """
209
+ # Inject frame_index into every drawing tool schema
210
+ schema["properties"]["frame_index"] = {
211
+ "type": "integer",
212
+ "description": "Target frame index (default: current frame)",
213
+ }
214
+
215
+ tool_def = ToolDef(name, description, schema, mutates=True)
216
+
217
+ async def handler(args: dict[str, Any], _name: str = name) -> dict[str, Any]:
218
+ err = self._require_active_project()
219
+ if err:
220
+ return err
221
+
222
+ project = self.state.get_active_project()
223
+ assert project is not None # guaranteed by _require_active_project
224
+ canvas = next(iter(project.canvases.values()))
225
+
226
+ # Resolve frame_index: use provided value or fall back to current
227
+ frame_index = args.pop("frame_index", canvas.current_frame_index)
228
+
229
+ # Validate bounds
230
+ if frame_index < 0 or frame_index >= len(canvas.frames):
231
+ return {
232
+ "success": False,
233
+ "error": (
234
+ f"frame_index {frame_index} out of range "
235
+ f"[0, {len(canvas.frames)})"
236
+ ),
237
+ }
238
+
239
+ # Point the canvas at the target frame before dispatch
240
+ canvas.current_frame_index = frame_index
241
+
242
+ # Include frame_index in dispatched params for frontend use
243
+ args["frame_index"] = frame_index
244
+
245
+ return await self._dispatch_command(_name, args)
246
+
247
+ self._register(tool_def, handler)
@@ -0,0 +1,312 @@
1
+ """MCP resources for PixelWeaver.
2
+
3
+ Exposes project state as MCP resources so that LLM clients can read and
4
+ subscribe to state changes. Resources are registered on the FastMCP
5
+ ``mcp`` instance; subscriptions are wired through the low-level server
6
+ so that mutating tool calls can push ``resource/updated`` notifications.
7
+
8
+ Resource URIs
9
+ -------------
10
+ - ``project://state`` -- full project overview
11
+ - ``project://canvas/{name}`` -- single canvas metadata
12
+ - ``project://canvas/{name}/frame/{index}`` -- composited frame as PNG
13
+ - ``project://palette`` -- colours in use on canvas
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import logging
20
+ from typing import TYPE_CHECKING, Any
21
+
22
+ from pydantic import AnyUrl
23
+
24
+ from pixelweaver.mcp_bridge import CollabServerUnreachableError
25
+
26
+ if TYPE_CHECKING:
27
+ from mcp.server.fastmcp import FastMCP
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Subscription tracking
33
+ # ---------------------------------------------------------------------------
34
+ # Tracks which resource URIs the client has subscribed to. Populated via
35
+ # the low-level subscribe/unsubscribe handlers registered in
36
+ # ``init_mcp_subscriptions``.
37
+ _subscribed_uris: set[str] = set()
38
+
39
+
40
+ def get_subscribed_uris() -> frozenset[str]:
41
+ """Return a snapshot of currently subscribed URIs (for testing)."""
42
+ return frozenset(_subscribed_uris)
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Helpers shared by resource handlers and the notification path
47
+ # ---------------------------------------------------------------------------
48
+
49
+ def _unreachable_error(exc: Exception) -> str:
50
+ return json.dumps({
51
+ "error": (
52
+ f"Collab server unreachable: {exc}. "
53
+ "Make sure the PixelWeaver server is running "
54
+ "(pixelweaver serve --port 7779)."
55
+ ),
56
+ })
57
+
58
+
59
+ async def _synced_state():
60
+ """Pull latest state and return it, or raise on failure."""
61
+ # Import here to avoid circular imports at module level.
62
+ from pixelweaver.mcp_server import _ensure_initialized, _state, _sync_from_collab
63
+
64
+ _ensure_initialized()
65
+ await _sync_from_collab()
66
+ return _state
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # Resource registration
71
+ # ---------------------------------------------------------------------------
72
+
73
+ def init_mcp_resources(mcp: FastMCP) -> None:
74
+ """Register all MCP resources on the FastMCP server instance.
75
+
76
+ Called from ``mcp_server.init_mcp_tools`` so resources and tools are
77
+ initialised together.
78
+ """
79
+
80
+ # -- project://state ------------------------------------------------
81
+ @mcp.resource(
82
+ "project://state",
83
+ name="project_state",
84
+ description=(
85
+ "Full project overview: project name, dimensions, canvas list "
86
+ "with layer trees, frame counts, and playback settings."
87
+ ),
88
+ mime_type="application/json",
89
+ )
90
+ async def project_state() -> str:
91
+ try:
92
+ state = await _synced_state()
93
+ except CollabServerUnreachableError as exc:
94
+ return _unreachable_error(exc)
95
+
96
+ project = state.get_active_project()
97
+ if project is None:
98
+ return json.dumps({"error": "No active project"})
99
+
100
+ canvases_info: dict[str, Any] = {}
101
+ for cname, canvas in project.canvases.items():
102
+ canvases_info[cname] = {
103
+ "name": canvas.name,
104
+ "width": canvas.width,
105
+ "height": canvas.height,
106
+ "layer_count": len(canvas.layers),
107
+ "layers": [
108
+ {
109
+ "id": layer["id"],
110
+ "name": layer.get("name", ""),
111
+ "visible": layer.get("visible", True),
112
+ "opacity": layer.get("opacity", 1.0),
113
+ }
114
+ for layer in canvas.layers
115
+ ],
116
+ "frame_count": canvas.frame_count,
117
+ "current_frame_index": canvas.current_frame_index,
118
+ "global_fps": canvas.global_fps,
119
+ }
120
+
121
+ return json.dumps(
122
+ {
123
+ "name": project.name,
124
+ "width": project.width,
125
+ "height": project.height,
126
+ "canvases": canvases_info,
127
+ },
128
+ indent=2,
129
+ )
130
+
131
+ # -- project://canvas/{name} ----------------------------------------
132
+ @mcp.resource(
133
+ "project://canvas/{name}",
134
+ name="canvas_info",
135
+ description=(
136
+ "Metadata for a single canvas: dimensions, layer tree, "
137
+ "frame count, playback state."
138
+ ),
139
+ mime_type="application/json",
140
+ )
141
+ async def canvas_info(name: str) -> str:
142
+ try:
143
+ state = await _synced_state()
144
+ except CollabServerUnreachableError as exc:
145
+ return _unreachable_error(exc)
146
+
147
+ project = state.get_active_project()
148
+ if project is None:
149
+ return json.dumps({"error": "No active project"})
150
+
151
+ canvas = project.canvases.get(name)
152
+ if canvas is None:
153
+ return json.dumps({"error": f"Canvas {name!r} not found"})
154
+
155
+ return json.dumps(
156
+ {
157
+ "name": canvas.name,
158
+ "width": canvas.width,
159
+ "height": canvas.height,
160
+ "layers": [
161
+ {
162
+ "id": layer["id"],
163
+ "name": layer.get("name", ""),
164
+ "visible": layer.get("visible", True),
165
+ "opacity": layer.get("opacity", 1.0),
166
+ "locked": layer.get("locked", False),
167
+ "blend_mode": layer.get("blendMode", "normal"),
168
+ }
169
+ for layer in canvas.layers
170
+ ],
171
+ "frame_count": canvas.frame_count,
172
+ "current_frame_index": canvas.current_frame_index,
173
+ "global_fps": canvas.global_fps,
174
+ },
175
+ indent=2,
176
+ )
177
+
178
+ # -- project://canvas/{name}/frame/{index} --------------------------
179
+ @mcp.resource(
180
+ "project://canvas/{name}/frame/{index}",
181
+ name="frame_png",
182
+ description="Composited frame pixel data as a base64-encoded PNG.",
183
+ mime_type="image/png",
184
+ )
185
+ async def frame_png(name: str, index: str) -> bytes:
186
+ """Return composited PNG bytes for the given canvas frame."""
187
+ from pixelweaver.storage import export_frame_png
188
+
189
+ try:
190
+ state = await _synced_state()
191
+ except CollabServerUnreachableError as exc:
192
+ # Resources returning bytes cannot easily embed error JSON;
193
+ # raise so the MCP framework surfaces the error to the client.
194
+ raise RuntimeError(_unreachable_error(exc)) from exc
195
+
196
+ project = state.get_active_project()
197
+ if project is None:
198
+ raise RuntimeError("No active project")
199
+
200
+ try:
201
+ frame_index = int(index)
202
+ except ValueError:
203
+ raise RuntimeError(f"Invalid frame index: {index!r}")
204
+
205
+ return export_frame_png(project, name, frame_index)
206
+
207
+ # -- project://palette -----------------------------------------------
208
+ @mcp.resource(
209
+ "project://palette",
210
+ name="palette",
211
+ description="Unique colours currently present on the active canvas.",
212
+ mime_type="application/json",
213
+ )
214
+ async def palette_resource() -> str:
215
+ try:
216
+ state = await _synced_state()
217
+ except CollabServerUnreachableError as exc:
218
+ return _unreachable_error(exc)
219
+
220
+ project = state.get_active_project()
221
+ if project is None:
222
+ return json.dumps({"error": "No active project"})
223
+
224
+ canvas = next(iter(project.canvases.values()), None)
225
+ if canvas is None:
226
+ return json.dumps({"error": "No canvas in project"})
227
+
228
+ max_colors = 256
229
+ colors: set[str] = set()
230
+
231
+ for layer in canvas.layers:
232
+ data = canvas.current_frame().pixel_data.get(layer["id"])
233
+ if data is None:
234
+ continue
235
+ for i in range(0, len(data), 4):
236
+ r, g, b, a = data[i], data[i + 1], data[i + 2], data[i + 3]
237
+ if a > 0:
238
+ colors.add(f"#{r:02x}{g:02x}{b:02x}{a:02x}")
239
+ if len(colors) >= max_colors:
240
+ break
241
+ if len(colors) >= max_colors:
242
+ break
243
+
244
+ return json.dumps({"colors": sorted(colors)}, indent=2)
245
+
246
+
247
+ # ---------------------------------------------------------------------------
248
+ # Subscription handlers and notification support
249
+ # ---------------------------------------------------------------------------
250
+
251
+ def init_mcp_subscriptions(mcp: FastMCP) -> None:
252
+ """Wire subscribe/unsubscribe handlers and enable the capability.
253
+
254
+ Must be called after ``init_mcp_resources`` but before the server
255
+ starts accepting connections.
256
+ """
257
+ low_level = mcp._mcp_server
258
+
259
+ @low_level.subscribe_resource()
260
+ async def handle_subscribe(uri: AnyUrl) -> None:
261
+ uri_str = str(uri)
262
+ _subscribed_uris.add(uri_str)
263
+ logger.info("Client subscribed to resource: %s", uri_str)
264
+
265
+ @low_level.unsubscribe_resource()
266
+ async def handle_unsubscribe(uri: AnyUrl) -> None:
267
+ uri_str = str(uri)
268
+ _subscribed_uris.discard(uri_str)
269
+ logger.info("Client unsubscribed from resource: %s", uri_str)
270
+
271
+ # Patch the capability so the server advertises subscribe=True.
272
+ # The low-level server hardcodes subscribe=False in get_capabilities;
273
+ # we wrap it to flip the flag after the fact.
274
+ _original_get_capabilities = low_level.get_capabilities
275
+
276
+ def _patched_get_capabilities(notification_options, experimental_capabilities):
277
+ caps = _original_get_capabilities(notification_options, experimental_capabilities)
278
+ if caps.resources is not None:
279
+ caps.resources.subscribe = True
280
+ return caps
281
+
282
+ low_level.get_capabilities = _patched_get_capabilities
283
+
284
+
285
+ async def notify_resource_subscribers(mcp: FastMCP) -> None:
286
+ """Send ``resource/updated`` for every currently-subscribed URI.
287
+
288
+ Called after a mutating tool pushes state back to the collab server,
289
+ so the MCP client knows to re-read any resources it is watching.
290
+
291
+ This must run inside a request context (i.e. while a tool handler is
292
+ executing) so we can access the session.
293
+ """
294
+ if not _subscribed_uris:
295
+ return
296
+
297
+ try:
298
+ session = mcp._mcp_server.request_context.session
299
+ except LookupError:
300
+ # No active request context -- nothing we can do.
301
+ logger.debug("No request context; skipping resource notifications")
302
+ return
303
+
304
+ for uri_str in _subscribed_uris:
305
+ try:
306
+ uri = AnyUrl(uri_str)
307
+ await session.send_resource_updated(uri=uri)
308
+ logger.debug("Sent resource/updated for %s", uri_str)
309
+ except Exception:
310
+ logger.warning(
311
+ "Failed to send resource/updated for %s", uri_str, exc_info=True
312
+ )