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,423 @@
1
+ """Frame tools for MCP registry.
2
+
3
+ Provides real server-side state mutations for animation frame operations,
4
+ plus read-only introspection tools. Each mutation handler:
5
+ 1. Validates the active project/canvas
6
+ 2. Mutates CanvasState.frames directly
7
+ 3. Dispatches the command to WebSocket clients (frontend undo stack)
8
+ 4. Broadcasts a full state patch so all clients see the change
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import uuid
14
+ from typing import TYPE_CHECKING, Any
15
+
16
+ from .mcp_registry import ToolDef
17
+ from .state import FrameState, build_full_state_patch
18
+
19
+ if TYPE_CHECKING:
20
+ from .mcp_registry import MCPCommandRegistry
21
+
22
+
23
+ def _get_canvas(registry: MCPCommandRegistry) -> Any:
24
+ """Return the first canvas of the active project, or an error dict."""
25
+ project = registry.state.get_active_project()
26
+ if project is None:
27
+ return {"success": False, "error": "No active project"}
28
+ canvas = next(iter(project.canvases.values()), None)
29
+ if canvas is None:
30
+ return {"success": False, "error": "No canvas in project"}
31
+ return canvas
32
+
33
+
34
+ async def _broadcast_state(registry: MCPCommandRegistry) -> None:
35
+ """Broadcast a full state patch to all WebSocket clients."""
36
+ project = registry.state.get_active_project()
37
+ if project is None:
38
+ return
39
+ patch = build_full_state_patch(project)
40
+ await registry.connections.broadcast({
41
+ "type": "state_patch",
42
+ "patches": [patch],
43
+ })
44
+
45
+
46
+ def register_frame_tools(registry: MCPCommandRegistry) -> None:
47
+ """Register all frame/animation-related MCP tools."""
48
+
49
+ # -- add_frame -----------------------------------------------------------
50
+
51
+ async def handle_add_frame(args: dict[str, Any]) -> dict[str, Any]:
52
+ err = registry._require_active_project()
53
+ if err:
54
+ return err
55
+ canvas = _get_canvas(registry)
56
+ if isinstance(canvas, dict):
57
+ return canvas
58
+
59
+ after_index = args.get("after_index", canvas.current_frame_index)
60
+ if after_index < -1 or after_index >= canvas.frame_count:
61
+ return {
62
+ "success": False,
63
+ "error": f"after_index {after_index} out of range [-1, {canvas.frame_count - 1}]",
64
+ }
65
+
66
+ new_frame = FrameState(id=str(uuid.uuid4()), pixel_data={})
67
+ insert_pos = after_index + 1
68
+ canvas.frames.insert(insert_pos, new_frame)
69
+
70
+ await registry._dispatch_command("add_frame", args)
71
+ await _broadcast_state(registry)
72
+
73
+ return {
74
+ "success": True,
75
+ "frame_id": new_frame.id,
76
+ "frame_count": canvas.frame_count,
77
+ }
78
+
79
+ registry._register(
80
+ ToolDef(
81
+ "add_frame",
82
+ "Add a new animation frame",
83
+ {
84
+ "type": "object",
85
+ "properties": {
86
+ "after_index": {
87
+ "type": "integer",
88
+ "description": "Insert after this frame index (default: current frame)",
89
+ },
90
+ },
91
+ },
92
+ mutates=True,
93
+ ),
94
+ handle_add_frame,
95
+ )
96
+
97
+ # -- remove_frame --------------------------------------------------------
98
+
99
+ async def handle_remove_frame(args: dict[str, Any]) -> dict[str, Any]:
100
+ err = registry._require_active_project()
101
+ if err:
102
+ return err
103
+ canvas = _get_canvas(registry)
104
+ if isinstance(canvas, dict):
105
+ return canvas
106
+
107
+ frame_index = args["frame_index"]
108
+ if canvas.frame_count <= 1:
109
+ return {"success": False, "error": "Cannot remove the last frame"}
110
+
111
+ if frame_index < 0 or frame_index >= canvas.frame_count:
112
+ return {
113
+ "success": False,
114
+ "error": f"frame_index {frame_index} out of range [0, {canvas.frame_count - 1}]",
115
+ }
116
+
117
+ canvas.frames.pop(frame_index)
118
+
119
+ # Adjust current_frame_index to stay valid
120
+ if canvas.current_frame_index >= canvas.frame_count:
121
+ canvas.current_frame_index = canvas.frame_count - 1
122
+
123
+ await registry._dispatch_command("remove_frame", args)
124
+ await _broadcast_state(registry)
125
+
126
+ return {"success": True, "frame_count": canvas.frame_count}
127
+
128
+ registry._register(
129
+ ToolDef(
130
+ "remove_frame",
131
+ "Remove an animation frame",
132
+ {
133
+ "type": "object",
134
+ "properties": {
135
+ "frame_index": {"type": "integer"},
136
+ },
137
+ "required": ["frame_index"],
138
+ },
139
+ mutates=True,
140
+ ),
141
+ handle_remove_frame,
142
+ )
143
+
144
+ # -- duplicate_frame -----------------------------------------------------
145
+
146
+ async def handle_duplicate_frame(args: dict[str, Any]) -> dict[str, Any]:
147
+ err = registry._require_active_project()
148
+ if err:
149
+ return err
150
+ canvas = _get_canvas(registry)
151
+ if isinstance(canvas, dict):
152
+ return canvas
153
+
154
+ frame_index = args["frame_index"]
155
+ if frame_index < 0 or frame_index >= canvas.frame_count:
156
+ return {
157
+ "success": False,
158
+ "error": f"frame_index {frame_index} out of range [0, {canvas.frame_count - 1}]",
159
+ }
160
+
161
+ source = canvas.frames[frame_index]
162
+ # Deep-copy pixel data via bytearray round-trip to guarantee a fresh
163
+ # object (CPython interns all-zero bytes, so bytes(data) may alias)
164
+ new_pixel_data = {
165
+ layer_id: bytes(bytearray(data))
166
+ for layer_id, data in source.pixel_data.items()
167
+ }
168
+ new_frame = FrameState(
169
+ id=str(uuid.uuid4()),
170
+ duration_ms=source.duration_ms,
171
+ pixel_data=new_pixel_data,
172
+ )
173
+ canvas.frames.insert(frame_index + 1, new_frame)
174
+
175
+ await registry._dispatch_command("duplicate_frame", args)
176
+ await _broadcast_state(registry)
177
+
178
+ return {
179
+ "success": True,
180
+ "frame_id": new_frame.id,
181
+ "source_frame_id": source.id,
182
+ "frame_count": canvas.frame_count,
183
+ }
184
+
185
+ registry._register(
186
+ ToolDef(
187
+ "duplicate_frame",
188
+ "Duplicate an existing frame",
189
+ {
190
+ "type": "object",
191
+ "properties": {
192
+ "frame_index": {"type": "integer"},
193
+ },
194
+ "required": ["frame_index"],
195
+ },
196
+ mutates=True,
197
+ ),
198
+ handle_duplicate_frame,
199
+ )
200
+
201
+ # -- reorder_frame -------------------------------------------------------
202
+
203
+ async def handle_reorder_frame(args: dict[str, Any]) -> dict[str, Any]:
204
+ err = registry._require_active_project()
205
+ if err:
206
+ return err
207
+ canvas = _get_canvas(registry)
208
+ if isinstance(canvas, dict):
209
+ return canvas
210
+
211
+ from_index = args["from_index"]
212
+ to_index = args["to_index"]
213
+
214
+ if from_index < 0 or from_index >= canvas.frame_count:
215
+ return {
216
+ "success": False,
217
+ "error": f"from_index {from_index} out of range [0, {canvas.frame_count - 1}]",
218
+ }
219
+ if to_index < 0 or to_index >= canvas.frame_count:
220
+ return {
221
+ "success": False,
222
+ "error": f"to_index {to_index} out of range [0, {canvas.frame_count - 1}]",
223
+ }
224
+
225
+ # Track the frame that current_frame_index points to
226
+ current_frame = canvas.frames[canvas.current_frame_index]
227
+ frame = canvas.frames.pop(from_index)
228
+ canvas.frames.insert(to_index, frame)
229
+ # Adjust current_frame_index to track the same frame
230
+ canvas.current_frame_index = canvas.frames.index(current_frame)
231
+
232
+ await registry._dispatch_command("reorder_frame", args)
233
+ await _broadcast_state(registry)
234
+
235
+ return {"success": True, "frame_count": canvas.frame_count}
236
+
237
+ registry._register(
238
+ ToolDef(
239
+ "reorder_frame",
240
+ "Move a frame to a new position",
241
+ {
242
+ "type": "object",
243
+ "properties": {
244
+ "from_index": {"type": "integer"},
245
+ "to_index": {"type": "integer"},
246
+ },
247
+ "required": ["from_index", "to_index"],
248
+ },
249
+ mutates=True,
250
+ ),
251
+ handle_reorder_frame,
252
+ )
253
+
254
+ # -- set_frame_duration --------------------------------------------------
255
+
256
+ async def handle_set_frame_duration(args: dict[str, Any]) -> dict[str, Any]:
257
+ err = registry._require_active_project()
258
+ if err:
259
+ return err
260
+ canvas = _get_canvas(registry)
261
+ if isinstance(canvas, dict):
262
+ return canvas
263
+
264
+ frame_index = args["frame_index"]
265
+ if frame_index < 0 or frame_index >= canvas.frame_count:
266
+ return {
267
+ "success": False,
268
+ "error": f"frame_index {frame_index} out of range [0, {canvas.frame_count - 1}]",
269
+ }
270
+
271
+ # duration_ms can be int or null (None = derive from global_fps)
272
+ duration_ms = args.get("duration_ms")
273
+ canvas.frames[frame_index].duration_ms = duration_ms
274
+
275
+ await registry._dispatch_command("set_frame_duration", args)
276
+ await _broadcast_state(registry)
277
+
278
+ return {"success": True, "frame_index": frame_index, "duration_ms": duration_ms}
279
+
280
+ registry._register(
281
+ ToolDef(
282
+ "set_frame_duration",
283
+ "Set duration for a specific frame in milliseconds",
284
+ {
285
+ "type": "object",
286
+ "properties": {
287
+ "frame_index": {"type": "integer"},
288
+ "duration_ms": {
289
+ "type": ["integer", "null"],
290
+ "minimum": 1,
291
+ "description": "Duration in ms, or null to use global FPS",
292
+ },
293
+ },
294
+ "required": ["frame_index", "duration_ms"],
295
+ },
296
+ mutates=True,
297
+ ),
298
+ handle_set_frame_duration,
299
+ )
300
+
301
+ # -- set_global_fps ------------------------------------------------------
302
+
303
+ async def handle_set_global_fps(args: dict[str, Any]) -> dict[str, Any]:
304
+ err = registry._require_active_project()
305
+ if err:
306
+ return err
307
+ canvas = _get_canvas(registry)
308
+ if isinstance(canvas, dict):
309
+ return canvas
310
+
311
+ fps = args["fps"]
312
+ canvas.global_fps = max(1.0, min(float(fps), 120.0))
313
+
314
+ await registry._dispatch_command("set_global_fps", args)
315
+ await _broadcast_state(registry)
316
+
317
+ return {"success": True, "global_fps": canvas.global_fps}
318
+
319
+ registry._register(
320
+ ToolDef(
321
+ "set_global_fps",
322
+ "Set the global frames per second for animation",
323
+ {
324
+ "type": "object",
325
+ "properties": {
326
+ "fps": {"type": "number", "minimum": 0.1, "maximum": 120},
327
+ },
328
+ "required": ["fps"],
329
+ },
330
+ mutates=True,
331
+ ),
332
+ handle_set_global_fps,
333
+ )
334
+
335
+ # -- get_frame_count (read-only) -----------------------------------------
336
+
337
+ async def handle_get_frame_count(_args: dict[str, Any]) -> dict[str, Any]:
338
+ canvas = _get_canvas(registry)
339
+ if isinstance(canvas, dict):
340
+ return canvas
341
+ return {
342
+ "success": True,
343
+ "count": canvas.frame_count,
344
+ "current_index": canvas.current_frame_index,
345
+ "global_fps": canvas.global_fps,
346
+ }
347
+
348
+ registry._register(
349
+ ToolDef(
350
+ "get_frame_count",
351
+ "Get the number of frames, current frame index, and global FPS",
352
+ {"type": "object", "properties": {}},
353
+ mutates=False,
354
+ ),
355
+ handle_get_frame_count,
356
+ )
357
+
358
+ # -- get_current_frame (read-only) ---------------------------------------
359
+
360
+ async def handle_get_current_frame(_args: dict[str, Any]) -> dict[str, Any]:
361
+ canvas = _get_canvas(registry)
362
+ if isinstance(canvas, dict):
363
+ return canvas
364
+ frame = canvas.current_frame()
365
+ return {
366
+ "success": True,
367
+ "index": canvas.current_frame_index,
368
+ "id": frame.id,
369
+ "duration_ms": frame.duration_ms,
370
+ }
371
+
372
+ registry._register(
373
+ ToolDef(
374
+ "get_current_frame",
375
+ "Get info about the currently selected frame",
376
+ {"type": "object", "properties": {}},
377
+ mutates=False,
378
+ ),
379
+ handle_get_current_frame,
380
+ )
381
+
382
+ # -- set_current_frame ---------------------------------------------------
383
+
384
+ async def handle_set_current_frame(args: dict[str, Any]) -> dict[str, Any]:
385
+ err = registry._require_active_project()
386
+ if err:
387
+ return err
388
+ canvas = _get_canvas(registry)
389
+ if isinstance(canvas, dict):
390
+ return canvas
391
+
392
+ frame_index = args["frame_index"]
393
+ if frame_index < 0 or frame_index >= canvas.frame_count:
394
+ return {
395
+ "success": False,
396
+ "error": f"frame_index {frame_index} out of range [0, {canvas.frame_count - 1}]",
397
+ }
398
+
399
+ canvas.current_frame_index = frame_index
400
+
401
+ await _broadcast_state(registry)
402
+
403
+ return {
404
+ "success": True,
405
+ "current_frame_index": canvas.current_frame_index,
406
+ "frame_id": canvas.current_frame().id,
407
+ }
408
+
409
+ registry._register(
410
+ ToolDef(
411
+ "set_current_frame",
412
+ "Set the currently active frame by index",
413
+ {
414
+ "type": "object",
415
+ "properties": {
416
+ "frame_index": {"type": "integer"},
417
+ },
418
+ "required": ["frame_index"],
419
+ },
420
+ mutates=True,
421
+ ),
422
+ handle_set_current_frame,
423
+ )
@@ -0,0 +1,106 @@
1
+ """History tools for MCP registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ if TYPE_CHECKING:
8
+ from .mcp_registry import MCPCommandRegistry
9
+
10
+ from .mcp_registry import ToolDef
11
+
12
+
13
+ def register_history_tools(registry: MCPCommandRegistry) -> None:
14
+ """Register all history-related MCP tools."""
15
+
16
+ # undo
17
+ async def handle_undo(_args: dict[str, Any]) -> dict[str, Any]:
18
+ project = registry.state.get_active_project()
19
+ if project is None:
20
+ return {"success": False, "error": "No active project"}
21
+ if not project.command_history:
22
+ return {"success": False, "error": "Nothing to undo"}
23
+
24
+ undone = project.command_history.pop()
25
+ project.redo_stack.append(undone)
26
+
27
+ await registry.connections.broadcast({
28
+ "type": "command_broadcast",
29
+ "command": {"type": "undo", "undone": undone},
30
+ "source": "mcp",
31
+ })
32
+
33
+ return {"success": True, "undone_command": undone.get("type", "unknown")}
34
+
35
+ registry._register(
36
+ ToolDef(
37
+ "undo",
38
+ "Undo the last command",
39
+ {"type": "object", "properties": {}},
40
+ mutates=True,
41
+ ),
42
+ handle_undo,
43
+ )
44
+
45
+ # redo
46
+ async def handle_redo(_args: dict[str, Any]) -> dict[str, Any]:
47
+ project = registry.state.get_active_project()
48
+ if project is None:
49
+ return {"success": False, "error": "No active project"}
50
+ if not project.redo_stack:
51
+ return {"success": False, "error": "Nothing to redo"}
52
+
53
+ redone = project.redo_stack.pop()
54
+ project.command_history.append(redone)
55
+
56
+ await registry.connections.broadcast({
57
+ "type": "command_broadcast",
58
+ "command": {"type": "redo", "redone": redone},
59
+ "source": "mcp",
60
+ })
61
+
62
+ return {"success": True, "redone_command": redone.get("type", "unknown")}
63
+
64
+ registry._register(
65
+ ToolDef(
66
+ "redo",
67
+ "Redo the last undone command",
68
+ {"type": "object", "properties": {}},
69
+ mutates=True,
70
+ ),
71
+ handle_redo,
72
+ )
73
+
74
+ # get_action_log
75
+ async def handle_get_action_log(args: dict[str, Any]) -> dict[str, Any]:
76
+ project = registry.state.get_active_project()
77
+ if project is None:
78
+ return {"success": False, "error": "No active project"}
79
+
80
+ limit = args.get("limit", 20)
81
+ history = project.command_history[-limit:]
82
+ entries = [
83
+ {"type": cmd.get("type"), "id": cmd.get("id"), "timestamp": cmd.get("timestamp")}
84
+ for cmd in history
85
+ ]
86
+ return {
87
+ "success": True,
88
+ "actions": entries,
89
+ "total": len(project.command_history),
90
+ "redo_available": len(project.redo_stack),
91
+ }
92
+
93
+ registry._register(
94
+ ToolDef(
95
+ "get_action_log",
96
+ "Get the recent command history",
97
+ {
98
+ "type": "object",
99
+ "properties": {
100
+ "limit": {"type": "integer", "default": 20},
101
+ },
102
+ },
103
+ mutates=False,
104
+ ),
105
+ handle_get_action_log,
106
+ )
@@ -0,0 +1,64 @@
1
+ """Layer 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_layer_tools(registry: MCPCommandRegistry) -> None:
12
+ """Register all layer-related MCP tools."""
13
+ layer_tools = [
14
+ ("add_layer", "Add a new layer to the active canvas", {
15
+ "type": "object",
16
+ "properties": {
17
+ "name": {"type": "string", "description": "Layer name"},
18
+ "above_layer_id": {"type": "string", "description": "Insert above this layer"},
19
+ },
20
+ "required": ["name"],
21
+ }),
22
+ ("remove_layer", "Remove a layer by ID", {
23
+ "type": "object",
24
+ "properties": {
25
+ "layer_id": {"type": "string"},
26
+ },
27
+ "required": ["layer_id"],
28
+ }),
29
+ ("rename_layer", "Rename a layer", {
30
+ "type": "object",
31
+ "properties": {
32
+ "layer_id": {"type": "string"},
33
+ "name": {"type": "string"},
34
+ },
35
+ "required": ["layer_id", "name"],
36
+ }),
37
+ ("move_layer", "Move a layer to a new position in the stack", {
38
+ "type": "object",
39
+ "properties": {
40
+ "layer_id": {"type": "string"},
41
+ "position": {"type": "integer", "description": "New index (0 = bottom)"},
42
+ },
43
+ "required": ["layer_id", "position"],
44
+ }),
45
+ ("set_layer_visibility", "Show or hide a layer", {
46
+ "type": "object",
47
+ "properties": {
48
+ "layer_id": {"type": "string"},
49
+ "visible": {"type": "boolean"},
50
+ },
51
+ "required": ["layer_id", "visible"],
52
+ }),
53
+ ("set_layer_opacity", "Set layer opacity (0.0 - 1.0)", {
54
+ "type": "object",
55
+ "properties": {
56
+ "layer_id": {"type": "string"},
57
+ "opacity": {"type": "number", "minimum": 0.0, "maximum": 1.0},
58
+ },
59
+ "required": ["layer_id", "opacity"],
60
+ }),
61
+ ]
62
+
63
+ for name, desc, schema in layer_tools:
64
+ registry._register_drawing_tool(name, desc, schema)
@@ -0,0 +1,37 @@
1
+ """Mutex for MCP command execution.
2
+
3
+ Prevents race conditions between concurrent MCP and UI mutations.
4
+ Use as an async context manager: ``async with mcp_lock: ...``
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ from typing import Any
11
+
12
+
13
+ class MCPLock:
14
+ """Async mutex for serializing MCP tool execution."""
15
+
16
+ def __init__(self) -> None:
17
+ self._lock = asyncio.Lock()
18
+
19
+ async def acquire(self) -> None:
20
+ """Acquire the lock (blocks until available)."""
21
+ await self._lock.acquire()
22
+
23
+ def release(self) -> None:
24
+ """Release the lock."""
25
+ self._lock.release()
26
+
27
+ @property
28
+ def is_locked(self) -> bool:
29
+ """Whether the lock is currently held."""
30
+ return self._lock.locked()
31
+
32
+ async def __aenter__(self) -> "MCPLock":
33
+ await self.acquire()
34
+ return self
35
+
36
+ async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
37
+ self.release()