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,125 @@
1
+ """Tests for the WebSocket JSON protocol message models."""
2
+
3
+ import pytest
4
+ from pydantic import ValidationError
5
+
6
+ from pixelweaver.protocol import (
7
+ CommandAckMessage,
8
+ CommandBroadcastMessage,
9
+ CommandMessage,
10
+ CommandRejectMessage,
11
+ ErrorMessage,
12
+ RedoMessage,
13
+ StateSyncMessage,
14
+ SyncRequestMessage,
15
+ UndoMessage,
16
+ parse_client_message,
17
+ )
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Client -> Server message parsing
21
+ # ---------------------------------------------------------------------------
22
+
23
+
24
+ class TestParseClientMessage:
25
+ def test_parse_command(self):
26
+ raw = {
27
+ "type": "command",
28
+ "id": "msg-1",
29
+ "command": {
30
+ "type": "draw_pixels",
31
+ "plugin": "builtin/pencil",
32
+ "version": "1.0.0",
33
+ "params": {"x": 10, "y": 20},
34
+ "id": "cmd-1",
35
+ "timestamp": 1234567890,
36
+ },
37
+ }
38
+ msg = parse_client_message(raw)
39
+ assert isinstance(msg, CommandMessage)
40
+ assert msg.id == "msg-1"
41
+ assert msg.command.type == "draw_pixels"
42
+ assert msg.command.plugin == "builtin/pencil"
43
+ assert msg.command.params == {"x": 10, "y": 20}
44
+
45
+ def test_parse_sync_request(self):
46
+ raw = {"type": "sync_request", "id": "msg-2"}
47
+ msg = parse_client_message(raw)
48
+ assert isinstance(msg, SyncRequestMessage)
49
+ assert msg.id == "msg-2"
50
+
51
+ def test_parse_undo(self):
52
+ raw = {"type": "undo", "id": "msg-3"}
53
+ msg = parse_client_message(raw)
54
+ assert isinstance(msg, UndoMessage)
55
+
56
+ def test_parse_redo(self):
57
+ raw = {"type": "redo", "id": "msg-4"}
58
+ msg = parse_client_message(raw)
59
+ assert isinstance(msg, RedoMessage)
60
+
61
+ def test_unknown_type_raises(self):
62
+ with pytest.raises(ValueError, match="Unknown message type"):
63
+ parse_client_message({"type": "bogus", "id": "x"})
64
+
65
+ def test_missing_type_raises(self):
66
+ with pytest.raises(ValueError, match="Unknown message type"):
67
+ parse_client_message({"id": "x"})
68
+
69
+ def test_malformed_command_raises(self):
70
+ """Command message missing required 'command' field."""
71
+ with pytest.raises(Exception):
72
+ parse_client_message({"type": "command", "id": "x"})
73
+
74
+ def test_command_missing_inner_fields_raises(self):
75
+ """Command payload missing required fields."""
76
+ with pytest.raises(Exception):
77
+ parse_client_message({
78
+ "type": "command",
79
+ "id": "msg-1",
80
+ "command": {"type": "draw"},
81
+ # missing plugin, version, id, timestamp
82
+ })
83
+
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # Server -> Client models (construction)
87
+ # ---------------------------------------------------------------------------
88
+
89
+
90
+ class TestServerMessages:
91
+ def test_command_ack(self):
92
+ msg = CommandAckMessage(id="msg-1", command_id="cmd-1")
93
+ d = msg.model_dump()
94
+ assert d["type"] == "command_ack"
95
+ assert d["success"] is True
96
+ assert d["id"] == "msg-1"
97
+
98
+ def test_command_reject(self):
99
+ msg = CommandRejectMessage(id="msg-1", command_id="cmd-1", error="Bad pixel")
100
+ d = msg.model_dump()
101
+ assert d["type"] == "command_reject"
102
+ assert d["error"] == "Bad pixel"
103
+
104
+ def test_state_sync(self):
105
+ msg = StateSyncMessage(id="msg-1", state={"project": None})
106
+ d = msg.model_dump()
107
+ assert d["type"] == "state_sync"
108
+ assert d["state"]["project"] is None
109
+
110
+ def test_command_broadcast(self):
111
+ msg = CommandBroadcastMessage(command={"type": "draw"}, source="mcp")
112
+ d = msg.model_dump()
113
+ assert d["type"] == "command_broadcast"
114
+ assert d["source"] == "mcp"
115
+
116
+ def test_error(self):
117
+ msg = ErrorMessage(id="msg-1", error="Something went wrong")
118
+ d = msg.model_dump()
119
+ assert d["type"] == "error"
120
+ assert d["error"] == "Something went wrong"
121
+
122
+ def test_ack_type_literal_enforced(self):
123
+ """The type field must be the literal value."""
124
+ with pytest.raises(ValidationError):
125
+ CommandAckMessage(type="wrong", id="x", command_id="y")
@@ -0,0 +1,87 @@
1
+ """Tests for FrameState and multi-frame CanvasState accessors."""
2
+
3
+ import pytest
4
+
5
+ from pixelweaver.state import CanvasState, FrameState, ServerState
6
+
7
+
8
+ class TestFrameState:
9
+ def test_creation_defaults(self):
10
+ f = FrameState(id="abc")
11
+ assert f.id == "abc"
12
+ assert f.duration_ms is None
13
+ assert f.pixel_data == {}
14
+
15
+ def test_creation_with_data(self):
16
+ data = {"layer-1": b"\x00" * 16}
17
+ f = FrameState(id="xyz", duration_ms=100, pixel_data=data)
18
+ assert f.duration_ms == 100
19
+ assert f.pixel_data["layer-1"] == b"\x00" * 16
20
+
21
+
22
+ class TestCanvasFrameAccessors:
23
+ def _make_canvas(self, n_frames: int = 3) -> CanvasState:
24
+ frames = [FrameState(id=f"frame-{i}") for i in range(n_frames)]
25
+ canvas = CanvasState(
26
+ name="test",
27
+ width=8,
28
+ height=8,
29
+ layers=[],
30
+ frames=frames,
31
+ )
32
+ return canvas
33
+
34
+ def test_current_frame(self):
35
+ canvas = self._make_canvas()
36
+ canvas.current_frame_index = 1
37
+ assert canvas.current_frame().id == "frame-1"
38
+
39
+ def test_current_frame_clamps(self):
40
+ canvas = self._make_canvas()
41
+ canvas.current_frame_index = 99
42
+ assert canvas.current_frame().id == "frame-2" # last frame
43
+
44
+ def test_current_frame_empty_raises(self):
45
+ canvas = self._make_canvas(0)
46
+ with pytest.raises(ValueError, match="no frames"):
47
+ canvas.current_frame()
48
+
49
+ def test_frame_at(self):
50
+ canvas = self._make_canvas()
51
+ assert canvas.frame_at(2).id == "frame-2"
52
+
53
+ def test_frame_at_out_of_range(self):
54
+ canvas = self._make_canvas()
55
+ with pytest.raises(IndexError):
56
+ canvas.frame_at(5)
57
+
58
+ def test_frame_count(self):
59
+ canvas = self._make_canvas()
60
+ assert canvas.frame_count == 3
61
+
62
+ def test_frame_count_empty(self):
63
+ canvas = self._make_canvas(0)
64
+ assert canvas.frame_count == 0
65
+
66
+
67
+ class TestCreateProjectSeedsFrames:
68
+ def test_new_project_has_one_frame(self):
69
+ state = ServerState()
70
+ state.create_project("test-proj", 16, 16)
71
+ project = state.projects["test-proj"]
72
+ canvas = list(project.canvases.values())[0]
73
+ assert canvas.frame_count == 1
74
+ frame = canvas.frames[0]
75
+ assert frame.id # non-empty UUID
76
+ assert frame.duration_ms is None
77
+
78
+ def test_frame_pixel_data_is_sole_owner(self):
79
+ """Pixel data lives only in FrameState, not on CanvasState."""
80
+ state = ServerState()
81
+ state.create_project("test-proj", 4, 4)
82
+ canvas = list(state.projects["test-proj"].canvases.values())[0]
83
+ assert not hasattr(canvas, "pixel_data") or "pixel_data" not in canvas.__dataclass_fields__
84
+ # Pixel data is accessible through the frame
85
+ layer_id = canvas.layers[0]["id"]
86
+ assert layer_id in canvas.frames[0].pixel_data
87
+ assert len(canvas.frames[0].pixel_data[layer_id]) == 4 * 4 * 4
@@ -0,0 +1,306 @@
1
+ """Tests for project file I/O (save, load, list, export)."""
2
+
3
+ import json
4
+ import uuid
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ from pixelweaver.state import CanvasState, FrameState, ProjectState, ServerState
10
+ from pixelweaver.storage import export_frame_png, list_projects, load_project, save_project
11
+
12
+
13
+ @pytest.fixture()
14
+ def tmp_data_dir(tmp_path: Path) -> str:
15
+ """Provide a temporary data directory path as a string."""
16
+ return str(tmp_path)
17
+
18
+
19
+ @pytest.fixture()
20
+ def sample_project() -> ProjectState:
21
+ """Create a sample project with one canvas and one layer."""
22
+ state = ServerState()
23
+ project = state.create_project("test-project", 16, 16)
24
+ return project
25
+
26
+
27
+ def _make_multi_frame_project() -> ProjectState:
28
+ """Create a project with one canvas containing two frames and two layers.
29
+
30
+ Frame 0: layer-a has red pixels, layer-b has green pixels.
31
+ Frame 1: layer-a has blue pixels, layer-b has white pixels.
32
+ Each frame has duration_ms set (None for frame 0, 200 for frame 1).
33
+ """
34
+ w, h = 4, 4
35
+ layer_a_id = "layer-a"
36
+ layer_b_id = "layer-b"
37
+
38
+ # 4x4 RGBA = 64 bytes per layer
39
+ red_px = (b"\xff\x00\x00\xff") * (w * h)
40
+ green_px = (b"\x00\xff\x00\xff") * (w * h)
41
+ blue_px = (b"\x00\x00\xff\xff") * (w * h)
42
+ white_px = (b"\xff\xff\xff\xff") * (w * h)
43
+
44
+ frame0 = FrameState(
45
+ id=str(uuid.uuid4()),
46
+ duration_ms=None,
47
+ pixel_data={layer_a_id: red_px, layer_b_id: green_px},
48
+ )
49
+ frame1 = FrameState(
50
+ id=str(uuid.uuid4()),
51
+ duration_ms=200,
52
+ pixel_data={layer_a_id: blue_px, layer_b_id: white_px},
53
+ )
54
+
55
+ canvas = CanvasState(
56
+ name="canvas-0",
57
+ width=w,
58
+ height=h,
59
+ layers=[
60
+ {"id": layer_a_id, "name": "Layer A", "visible": True, "opacity": 1.0},
61
+ {"id": layer_b_id, "name": "Layer B", "visible": True, "opacity": 1.0},
62
+ ],
63
+ frames=[frame0, frame1],
64
+ current_frame_index=1,
65
+ global_fps=24.0,
66
+ origin_x=5,
67
+ origin_y=10,
68
+ )
69
+
70
+ project = ProjectState(name="multi-frame-project", width=w, height=h)
71
+ project.canvases["canvas-0"] = canvas
72
+ return project
73
+
74
+
75
+ class TestSaveAndLoad:
76
+ def test_save_creates_expected_files(self, tmp_data_dir: str, sample_project: ProjectState):
77
+ save_project(sample_project, tmp_data_dir)
78
+
79
+ project_dir = Path(tmp_data_dir) / "test-project"
80
+ assert project_dir.is_dir()
81
+ assert (project_dir / "project.json").is_file()
82
+ assert (project_dir / "history.json").is_file()
83
+ assert (project_dir / "canvases" / "canvas-0" / "canvas.json").is_file()
84
+ assert (project_dir / "canvases" / "canvas-0" / "frames.json").is_file()
85
+
86
+ # Verify project.json content
87
+ meta = json.loads((project_dir / "project.json").read_text())
88
+ assert meta["name"] == "test-project"
89
+ assert meta["width"] == 16
90
+ assert meta["height"] == 16
91
+ assert "canvas-0" in meta["canvases"]
92
+
93
+ def test_save_creates_frames_json(self, tmp_data_dir: str, sample_project: ProjectState):
94
+ save_project(sample_project, tmp_data_dir)
95
+
96
+ frames_json_path = (
97
+ Path(tmp_data_dir) / "test-project" / "canvases" / "canvas-0" / "frames.json"
98
+ )
99
+ manifest = json.loads(frames_json_path.read_text())
100
+
101
+ assert "frames" in manifest
102
+ assert len(manifest["frames"]) == 1
103
+ assert "id" in manifest["frames"][0]
104
+ assert manifest["current_frame_index"] == 0
105
+ assert manifest["global_fps"] == 12.0
106
+ assert manifest["origin_x"] == 0
107
+ assert manifest["origin_y"] == 0
108
+
109
+ def test_save_creates_uuid_frame_dirs(self, tmp_data_dir: str, sample_project: ProjectState):
110
+ save_project(sample_project, tmp_data_dir)
111
+
112
+ canvas_dir = Path(tmp_data_dir) / "test-project" / "canvases" / "canvas-0"
113
+ manifest = json.loads((canvas_dir / "frames.json").read_text())
114
+ frame_id = manifest["frames"][0]["id"]
115
+
116
+ # Frame directory should be named by UUID, not "0"
117
+ frame_dir = canvas_dir / "frames" / frame_id
118
+ assert frame_dir.is_dir()
119
+ assert not (canvas_dir / "frames" / "0").exists()
120
+
121
+ # There should be at least one layer PNG
122
+ pngs = list(frame_dir.glob("layer-*.png"))
123
+ assert len(pngs) == 1
124
+
125
+ def test_save_cleans_up_old_frame_dirs(self, tmp_data_dir: str, sample_project: ProjectState):
126
+ """Old frames/0/ directory should be removed after save."""
127
+ # Create a fake old-format directory
128
+ canvas_dir = Path(tmp_data_dir) / "test-project" / "canvases" / "canvas-0"
129
+ old_frame_dir = canvas_dir / "frames" / "0"
130
+ old_frame_dir.mkdir(parents=True, exist_ok=True)
131
+ (old_frame_dir / "layer-fake.png").write_bytes(b"fake")
132
+
133
+ save_project(sample_project, tmp_data_dir)
134
+
135
+ # Old "0" directory should be gone
136
+ assert not old_frame_dir.exists()
137
+
138
+ def test_round_trip(self, tmp_data_dir: str, sample_project: ProjectState):
139
+ """Save then load should produce an equivalent project."""
140
+ # Add a command to history before saving
141
+ sample_project.command_history.append({"type": "draw_pixels", "id": "cmd-1"})
142
+
143
+ save_project(sample_project, tmp_data_dir)
144
+ loaded = load_project(f"{tmp_data_dir}/test-project")
145
+
146
+ assert loaded.name == sample_project.name
147
+ assert loaded.width == sample_project.width
148
+ assert loaded.height == sample_project.height
149
+ assert len(loaded.canvases) == len(sample_project.canvases)
150
+ assert "canvas-0" in loaded.canvases
151
+ assert len(loaded.command_history) == 1
152
+ assert loaded.command_history[0]["id"] == "cmd-1"
153
+
154
+ # Pixel data should round-trip through PNG
155
+ orig_canvas = sample_project.canvases["canvas-0"]
156
+ loaded_canvas = loaded.canvases["canvas-0"]
157
+ assert len(loaded_canvas.layers) == len(orig_canvas.layers)
158
+ orig_pd = orig_canvas.current_frame().pixel_data
159
+ loaded_pd = loaded_canvas.current_frame().pixel_data
160
+ assert len(loaded_pd) == len(orig_pd)
161
+
162
+ for layer_id in orig_pd:
163
+ assert layer_id in loaded_pd
164
+ assert loaded_pd[layer_id] == orig_pd[layer_id]
165
+
166
+ def test_round_trip_preserves_frame_ids(self, tmp_data_dir: str, sample_project: ProjectState):
167
+ """Frame UUIDs should survive a save/load round-trip."""
168
+ orig_frame_id = sample_project.canvases["canvas-0"].frames[0].id
169
+
170
+ save_project(sample_project, tmp_data_dir)
171
+ loaded = load_project(f"{tmp_data_dir}/test-project")
172
+
173
+ loaded_frame_id = loaded.canvases["canvas-0"].frames[0].id
174
+ assert loaded_frame_id == orig_frame_id
175
+
176
+ def test_round_trip_preserves_frame_metadata(self, tmp_data_dir: str):
177
+ """current_frame_index, global_fps, origin should round-trip."""
178
+ project = _make_multi_frame_project()
179
+
180
+ save_project(project, tmp_data_dir)
181
+ loaded = load_project(f"{tmp_data_dir}/multi-frame-project")
182
+
183
+ loaded_canvas = loaded.canvases["canvas-0"]
184
+ assert loaded_canvas.current_frame_index == 1
185
+ assert loaded_canvas.global_fps == 24.0
186
+ assert loaded_canvas.origin_x == 5
187
+ assert loaded_canvas.origin_y == 10
188
+
189
+ def test_multi_frame_round_trip(self, tmp_data_dir: str):
190
+ """Two frames with different pixel data should round-trip correctly."""
191
+ project = _make_multi_frame_project()
192
+ orig_canvas = project.canvases["canvas-0"]
193
+
194
+ save_project(project, tmp_data_dir)
195
+ loaded = load_project(f"{tmp_data_dir}/multi-frame-project")
196
+ loaded_canvas = loaded.canvases["canvas-0"]
197
+
198
+ # Same number of frames
199
+ assert len(loaded_canvas.frames) == 2
200
+
201
+ # Frame IDs preserved
202
+ assert loaded_canvas.frames[0].id == orig_canvas.frames[0].id
203
+ assert loaded_canvas.frames[1].id == orig_canvas.frames[1].id
204
+
205
+ # duration_ms preserved
206
+ assert loaded_canvas.frames[0].duration_ms is None
207
+ assert loaded_canvas.frames[1].duration_ms == 200
208
+
209
+ # Pixel data preserved for all layers in both frames
210
+ for i in range(2):
211
+ orig_pd = orig_canvas.frames[i].pixel_data
212
+ loaded_pd = loaded_canvas.frames[i].pixel_data
213
+ assert set(loaded_pd.keys()) == set(orig_pd.keys())
214
+ for layer_id in orig_pd:
215
+ assert loaded_pd[layer_id] == orig_pd[layer_id]
216
+
217
+ def test_multi_frame_creates_separate_dirs(self, tmp_data_dir: str):
218
+ """Each frame should get its own UUID-named directory on disk."""
219
+ project = _make_multi_frame_project()
220
+ orig_canvas = project.canvases["canvas-0"]
221
+
222
+ save_project(project, tmp_data_dir)
223
+
224
+ canvas_dir = Path(tmp_data_dir) / "multi-frame-project" / "canvases" / "canvas-0"
225
+ for frame in orig_canvas.frames:
226
+ frame_dir = canvas_dir / "frames" / frame.id
227
+ assert frame_dir.is_dir()
228
+ # Each frame should have PNGs for both layers
229
+ pngs = list(frame_dir.glob("layer-*.png"))
230
+ assert len(pngs) == 2
231
+
232
+ def test_load_missing_frames_json_raises(self, tmp_data_dir: str):
233
+ """Loading a canvas without frames.json should raise FileNotFoundError."""
234
+ # Create minimal project structure without frames.json
235
+ project_dir = Path(tmp_data_dir) / "old-project"
236
+ canvas_dir = project_dir / "canvases" / "canvas-0"
237
+ canvas_dir.mkdir(parents=True)
238
+
239
+ (project_dir / "project.json").write_text(json.dumps({
240
+ "name": "old-project", "width": 16, "height": 16, "canvases": ["canvas-0"],
241
+ }))
242
+ (canvas_dir / "canvas.json").write_text(json.dumps({
243
+ "name": "canvas-0", "width": 16, "height": 16, "layers": [],
244
+ }))
245
+
246
+ with pytest.raises(FileNotFoundError, match="frames.json"):
247
+ load_project(str(project_dir))
248
+
249
+
250
+ class TestListProjects:
251
+ def test_empty_dir(self, tmp_data_dir: str):
252
+ assert list_projects(tmp_data_dir) == []
253
+
254
+ def test_nonexistent_dir(self):
255
+ assert list_projects("/nonexistent/path") == []
256
+
257
+ def test_lists_saved_projects(self, tmp_data_dir: str, sample_project: ProjectState):
258
+ save_project(sample_project, tmp_data_dir)
259
+ names = list_projects(tmp_data_dir)
260
+ assert names == ["test-project"]
261
+
262
+ def test_ignores_non_project_dirs(self, tmp_data_dir: str):
263
+ """Directories without project.json should not be listed."""
264
+ (Path(tmp_data_dir) / "not-a-project").mkdir()
265
+ assert list_projects(tmp_data_dir) == []
266
+
267
+
268
+ class TestExportFramePng:
269
+ def test_export_produces_png_bytes(self, sample_project: ProjectState):
270
+ png_bytes = export_frame_png(sample_project, "canvas-0", 0)
271
+ # PNG files start with the PNG magic bytes
272
+ assert png_bytes[:8] == b"\x89PNG\r\n\x1a\n"
273
+
274
+ def test_export_nonexistent_canvas_raises(self, sample_project: ProjectState):
275
+ with pytest.raises(ValueError, match="not found"):
276
+ export_frame_png(sample_project, "no-such-canvas", 0)
277
+
278
+ def test_export_nonzero_frame(self):
279
+ """Exporting frame 1 should produce valid PNG with that frame's pixel data."""
280
+ project = _make_multi_frame_project()
281
+ png_bytes = export_frame_png(project, "canvas-0", 1)
282
+ assert png_bytes[:8] == b"\x89PNG\r\n\x1a\n"
283
+
284
+ # Verify the composited image contains frame 1's data (blue + white)
285
+ from io import BytesIO
286
+ from PIL import Image
287
+
288
+ img = Image.open(BytesIO(png_bytes)).convert("RGBA")
289
+ assert img.size == (4, 4)
290
+
291
+ # Frame 1: layer-a is blue, layer-b is white (composited on top)
292
+ # Alpha composite of blue over transparent, then white over that = white
293
+ pixel = img.getpixel((0, 0))
294
+ assert pixel == (255, 255, 255, 255)
295
+
296
+ def test_export_each_frame_differs(self):
297
+ """Each frame should export different pixel content."""
298
+ project = _make_multi_frame_project()
299
+ png0 = export_frame_png(project, "canvas-0", 0)
300
+ png1 = export_frame_png(project, "canvas-0", 1)
301
+ assert png0 != png1
302
+
303
+ def test_export_out_of_range_frame_raises(self, sample_project: ProjectState):
304
+ """Out-of-range frame index should raise IndexError."""
305
+ with pytest.raises(IndexError, match="out of range"):
306
+ export_frame_png(sample_project, "canvas-0", 5)