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,540 @@
1
+ /**
2
+ * Animation Commands Plugin -- registers animation operations as undoable commands.
3
+ *
4
+ * All structural frame/tag operations are tier: 'project'.
5
+ * Drawing on a frame is tier: 'frame' (handled by existing drawing tools).
6
+ */
7
+
8
+ import type { PluginModule } from '../core/plugin-loader.js';
9
+ import type { Frame } from './frame-model.svelte.js';
10
+ import { PixelBuffer } from '../canvas/pixel-buffer.js';
11
+ import * as frameModel from './frame-model.svelte.js';
12
+ import * as frameTags from './frame-tags.svelte.js';
13
+ import { notificationState } from '../core/notification-state.svelte.js';
14
+ import { frameSelection } from './frame-selection.svelte.js';
15
+
16
+ // --- Frame clipboard ---
17
+
18
+ /**
19
+ * Module-level clipboard for frame copy-paste. Stores a deep-copied map of
20
+ * layerId -> PixelBuffer from the copied frame. Separate from the pixel-level
21
+ * clipboard in edit/clipboard-state.js.
22
+ */
23
+ let copiedFrames: Array<Map<string, PixelBuffer>> | null = null;
24
+
25
+ /** Whether the frame clipboard has content. Used by UI to conditionally enable paste. */
26
+ export function hasFrameClipboard(): boolean {
27
+ return copiedFrames !== null && copiedFrames.length > 0;
28
+ }
29
+
30
+ // --- Snapshot helpers ---
31
+
32
+ /** Snapshot a frame for undo restore (deep copy pixel data). */
33
+ function snapshotFrame(frame: Frame): {
34
+ id: string;
35
+ index: number;
36
+ durationMs: number | null;
37
+ pixelDataEntries: { layerId: string; width: number; height: number; data: Uint8ClampedArray }[];
38
+ } {
39
+ const pixelDataEntries: { layerId: string; width: number; height: number; data: Uint8ClampedArray }[] = [];
40
+ for (const [layerId, buffer] of frame.pixelData) {
41
+ pixelDataEntries.push({
42
+ layerId,
43
+ width: buffer.width,
44
+ height: buffer.height,
45
+ data: new Uint8ClampedArray(buffer.data),
46
+ });
47
+ }
48
+ return {
49
+ id: frame.id,
50
+ index: frame.index,
51
+ durationMs: frame.durationMs,
52
+ pixelDataEntries,
53
+ };
54
+ }
55
+
56
+ /** Restore a frame from a snapshot. */
57
+ function restoreFrameFromSnapshot(snapshot: ReturnType<typeof snapshotFrame>): void {
58
+ const pixelData = new Map<string, PixelBuffer>();
59
+ for (const entry of snapshot.pixelDataEntries) {
60
+ pixelData.set(entry.layerId, new PixelBuffer(entry.width, entry.height, entry.data));
61
+ }
62
+ const frame: Frame = {
63
+ id: snapshot.id,
64
+ index: snapshot.index,
65
+ durationMs: snapshot.durationMs,
66
+ pixelData,
67
+ };
68
+ frameModel._insertFrameAt(snapshot.index, frame);
69
+ }
70
+
71
+ // --- Snapshot types for each command ---
72
+
73
+ type AddFrameSnapshot = { frameIndex: number; previousFrameIndex: number };
74
+ type RemoveFrameSnapshot = {
75
+ frame: ReturnType<typeof snapshotFrame>;
76
+ previousFrameIndex: number;
77
+ };
78
+ type DuplicateFrameSnapshot = { newFrameIndex: number };
79
+ type SetFrameDurationSnapshot = { oldDurationMs: number | null };
80
+ type SetGlobalFpsSnapshot = { oldFps: number };
81
+ type AddTagSnapshot = { tagId: string };
82
+ type RemoveTagSnapshot = {
83
+ name: string;
84
+ color: string;
85
+ startFrame: number;
86
+ endFrame: number;
87
+ parentId: string | null;
88
+ };
89
+ type SetOriginSnapshot = { oldX: number; oldY: number };
90
+ type PasteFrameSnapshot = { pastedFrameIndices: number[] };
91
+ type ReorderFramesSnapshot = {
92
+ previousOrder: string[];
93
+ };
94
+ type DeleteSelectedFramesSnapshot = {
95
+ removedFrames: ReturnType<typeof snapshotFrame>[];
96
+ previousFrameIndex: number;
97
+ };
98
+
99
+ export const animationCommandsPlugin: PluginModule = {
100
+ name: 'builtin/animation',
101
+ version: '1.0.0',
102
+ description: 'Animation frame and tag management commands',
103
+ dependencies: ['builtin/layers'],
104
+
105
+ register(api) {
106
+ // --- add_frame ---
107
+ api.addCommand('add_frame', {
108
+ tier: 'project',
109
+ execute(params) {
110
+ const afterIndex = params["afterIndex"];
111
+ const duplicate = params["duplicate"];
112
+ const previousFrameIndex = frameModel.getCurrentFrameIndex();
113
+ // Build options object without undefined values (exactOptionalPropertyTypes)
114
+ const addFrameOptions: { afterIndex?: number; duplicate?: boolean } = {};
115
+ if (afterIndex !== undefined) addFrameOptions.afterIndex = afterIndex;
116
+ if (duplicate !== undefined) addFrameOptions.duplicate = duplicate;
117
+ const frame = frameModel.addFrame(addFrameOptions);
118
+ const snapshot: AddFrameSnapshot = {
119
+ frameIndex: frame.index,
120
+ previousFrameIndex,
121
+ };
122
+ return snapshot;
123
+ },
124
+ undo(_params, _ctx, snapshot) {
125
+ const typed = snapshot as AddFrameSnapshot | undefined;
126
+ if (!typed) return;
127
+ frameModel._removeFrameAt(typed.frameIndex);
128
+ // Restore previous current frame
129
+ const frames = frameModel.getFrames();
130
+ if (frames.length > 0 && typed.previousFrameIndex < frames.length) {
131
+ frameModel.setCurrentFrame(typed.previousFrameIndex);
132
+ }
133
+ },
134
+ describe(params) {
135
+ return params["duplicate"] ? 'Duplicated frame' : 'Added frame';
136
+ },
137
+ });
138
+
139
+ // --- remove_frame ---
140
+ api.addCommand('remove_frame', {
141
+ tier: 'project',
142
+ execute(params) {
143
+ const index = params["index"];
144
+ const frames = frameModel.getFrames();
145
+ const target = frames[index];
146
+ if (!target) return;
147
+ // Snapshot for undo
148
+ const snapshot: RemoveFrameSnapshot = {
149
+ frame: snapshotFrame(target),
150
+ previousFrameIndex: frameModel.getCurrentFrameIndex(),
151
+ };
152
+ frameModel.removeFrame(index);
153
+ return snapshot;
154
+ },
155
+ undo(_params, _ctx, snapshot) {
156
+ const typed = snapshot as RemoveFrameSnapshot | undefined;
157
+ if (!typed) return;
158
+ restoreFrameFromSnapshot(typed.frame);
159
+ const frames = frameModel.getFrames();
160
+ if (frames.length > 0 && typed.previousFrameIndex < frames.length) {
161
+ frameModel.setCurrentFrame(typed.previousFrameIndex);
162
+ }
163
+ },
164
+ describe() {
165
+ return 'Removed frame';
166
+ },
167
+ });
168
+
169
+ // --- duplicate_frame ---
170
+ api.addCommand('duplicate_frame', {
171
+ tier: 'project',
172
+ execute(params) {
173
+ const index = params["index"];
174
+ const frame = frameModel.duplicateFrame(index);
175
+ const snapshot: DuplicateFrameSnapshot = { newFrameIndex: frame.index };
176
+ return snapshot;
177
+ },
178
+ undo(_params, _ctx, snapshot) {
179
+ const typed = snapshot as DuplicateFrameSnapshot | undefined;
180
+ if (!typed) return;
181
+ frameModel._removeFrameAt(typed.newFrameIndex);
182
+ },
183
+ describe() {
184
+ return 'Duplicated frame';
185
+ },
186
+ });
187
+
188
+ // --- reorder_frame ---
189
+ api.addCommand('reorder_frame', {
190
+ tier: 'project',
191
+ execute(params) {
192
+ const fromIndex = params["fromIndex"];
193
+ const toIndex = params["toIndex"];
194
+ frameModel.reorderFrame(fromIndex, toIndex);
195
+ },
196
+ undo(params) {
197
+ // Reverse the reorder
198
+ const fromIndex = params["fromIndex"];
199
+ const toIndex = params["toIndex"];
200
+ frameModel.reorderFrame(toIndex, fromIndex);
201
+ },
202
+ describe(params) {
203
+ return `Reordered frame ${String(params["fromIndex"])} to ${String(params["toIndex"])}`;
204
+ },
205
+ });
206
+
207
+ // --- reorder_frames (bulk) ---
208
+ api.addCommand('reorder_frames', {
209
+ tier: 'project',
210
+ execute(params) {
211
+ const fromIndices: number[] = params["fromIndices"];
212
+ const toIndex: number = params["toIndex"];
213
+ // Snapshot the full frame ID order before reordering so undo can restore it
214
+ const previousOrder = frameModel.getFrames().map((f) => f.id);
215
+ frameModel.reorderFrames(fromIndices, toIndex);
216
+ return { previousOrder } as ReorderFramesSnapshot;
217
+ },
218
+ undo(_params, _ctx, snapshot) {
219
+ const typed = snapshot as ReorderFramesSnapshot | undefined;
220
+ if (!typed) return;
221
+ // Restore frames to their original order by ID
222
+ const currentFrames = frameModel.getFrames();
223
+ const byId = new Map(currentFrames.map((f) => [f.id, f]));
224
+ const restored = typed.previousOrder.map((id) => byId.get(id)).filter(Boolean) as Frame[];
225
+ // Replace in-place: splice everything out and back in
226
+ currentFrames.splice(0, currentFrames.length, ...restored);
227
+ // Re-index to fix .index fields
228
+ for (let i = 0; i < currentFrames.length; i++) {
229
+ const f = currentFrames[i];
230
+ if (f) f.index = i;
231
+ }
232
+ },
233
+ describe(params) {
234
+ const count = (params["fromIndices"] as number[]).length;
235
+ return `Reordered ${String(count)} frames`;
236
+ },
237
+ });
238
+
239
+ // --- set_frame_duration ---
240
+ api.addCommand('set_frame_duration', {
241
+ tier: 'project',
242
+ execute(params) {
243
+ const index = params["index"];
244
+ const durationMs = params["durationMs"];
245
+ const frames = frameModel.getFrames();
246
+ const target = frames[index];
247
+ if (!target) return;
248
+ const snapshot: SetFrameDurationSnapshot = {
249
+ oldDurationMs: target.durationMs,
250
+ };
251
+ frameModel.setFrameDuration(index, durationMs);
252
+ return snapshot;
253
+ },
254
+ undo(params, _ctx, snapshot) {
255
+ const typed = snapshot as SetFrameDurationSnapshot | undefined;
256
+ if (!typed) return;
257
+ const index = params["index"];
258
+ frameModel.setFrameDuration(index, typed.oldDurationMs);
259
+ },
260
+ describe(params) {
261
+ const ms = params["durationMs"];
262
+ return ms !== null ? `Set frame duration to ${String(ms)}ms` : 'Reset frame duration to global FPS';
263
+ },
264
+ });
265
+
266
+ // --- set_global_fps ---
267
+ api.addCommand('set_global_fps', {
268
+ tier: 'project',
269
+ execute(params) {
270
+ const snapshot: SetGlobalFpsSnapshot = { oldFps: frameModel.getGlobalFps() };
271
+ frameModel.setGlobalFps(params["fps"]);
272
+ return snapshot;
273
+ },
274
+ undo(_params, _ctx, snapshot) {
275
+ const typed = snapshot as SetGlobalFpsSnapshot | undefined;
276
+ if (!typed) return;
277
+ frameModel.setGlobalFps(typed.oldFps);
278
+ },
279
+ describe(params) {
280
+ return `Set global FPS to ${String(params["fps"])}`;
281
+ },
282
+ });
283
+
284
+ // --- add_tag ---
285
+ api.addCommand('add_tag', {
286
+ tier: 'project',
287
+ execute(params) {
288
+ // Build options object without undefined values (exactOptionalPropertyTypes)
289
+ const tagOptions: { color?: string; parentId?: string } = {};
290
+ if (params["color"] !== undefined) tagOptions.color = params["color"];
291
+ if (params["parentId"] !== undefined) tagOptions.parentId = params["parentId"];
292
+ const tag = frameTags.addTag(
293
+ params["name"],
294
+ params["startFrame"],
295
+ params["endFrame"],
296
+ tagOptions,
297
+ );
298
+ const snapshot: AddTagSnapshot = { tagId: tag.id };
299
+ return snapshot;
300
+ },
301
+ undo(_params, _ctx, snapshot) {
302
+ const typed = snapshot as AddTagSnapshot | undefined;
303
+ if (!typed) return;
304
+ frameTags.removeTag(typed.tagId);
305
+ },
306
+ describe(params) {
307
+ return `Added tag "${String(params["name"])}"`;
308
+ },
309
+ });
310
+
311
+ // --- remove_tag ---
312
+ api.addCommand('remove_tag', {
313
+ tier: 'project',
314
+ execute(params) {
315
+ const tag = frameTags.getTag(params["id"]);
316
+ if (!tag) return;
317
+ // Stash the name on params so describe() can use it (describe only gets params)
318
+ params["_removedName"] = tag.name;
319
+ // Snapshot for undo
320
+ const snapshot: RemoveTagSnapshot = {
321
+ name: tag.name,
322
+ color: tag.color,
323
+ startFrame: tag.startFrame,
324
+ endFrame: tag.endFrame,
325
+ parentId: tag.parentId,
326
+ };
327
+ frameTags.removeTag(params["id"]);
328
+ return snapshot;
329
+ },
330
+ undo(_params, _ctx, snapshot) {
331
+ const typed = snapshot as RemoveTagSnapshot | undefined;
332
+ if (typed) {
333
+ // Build options object without undefined values (exactOptionalPropertyTypes)
334
+ const restoreOptions: { color?: string; parentId?: string } = { color: typed.color };
335
+ if (typed.parentId !== null) restoreOptions.parentId = typed.parentId;
336
+ frameTags.addTag(typed.name, typed.startFrame, typed.endFrame, restoreOptions);
337
+ }
338
+ },
339
+ describe(params) {
340
+ return `Removed tag "${String(params["_removedName"] ?? params["id"])}"`;
341
+ },
342
+ });
343
+
344
+ // --- set_origin ---
345
+ api.addCommand('set_origin', {
346
+ tier: 'project',
347
+ execute(params) {
348
+ const snapshot: SetOriginSnapshot = {
349
+ oldX: frameModel.getOriginX(),
350
+ oldY: frameModel.getOriginY(),
351
+ };
352
+ frameModel.setOrigin(params["x"], params["y"]);
353
+ return snapshot;
354
+ },
355
+ undo(_params, _ctx, snapshot) {
356
+ const typed = snapshot as SetOriginSnapshot | undefined;
357
+ if (!typed) return;
358
+ frameModel.setOrigin(typed.oldX, typed.oldY);
359
+ },
360
+ describe(params) {
361
+ return `Set origin to (${String(params["x"])}, ${String(params["y"])})`;
362
+ },
363
+ });
364
+
365
+ // --- copy_frame ---
366
+ api.addCommand('copy_frame', {
367
+ tier: 'project',
368
+ execute() {
369
+ const allFrames = frameModel.getFrames();
370
+
371
+ // If multiple frames selected, copy them all (sorted by index);
372
+ // otherwise fall back to copying the current frame.
373
+ const indicesToCopy =
374
+ frameSelection.count > 1
375
+ ? frameSelection.sortedIndices
376
+ : [frameModel.getCurrentFrameIndex()];
377
+
378
+ copiedFrames = indicesToCopy.map((idx) => {
379
+ const cloned = new Map<string, PixelBuffer>();
380
+ const frame = allFrames[idx];
381
+ if (!frame) return cloned;
382
+ for (const [layerId, buffer] of frame.pixelData) {
383
+ cloned.set(layerId, buffer.clone());
384
+ }
385
+ return cloned;
386
+ });
387
+
388
+ notificationState.push({
389
+ id: 'frame-copied',
390
+ message: `Copied ${String(copiedFrames.length)} frame(s)`,
391
+ type: 'info',
392
+ autoDismissMs: 1500,
393
+ });
394
+ },
395
+ undo() {},
396
+ describe() {
397
+ return 'Copied frame(s)';
398
+ },
399
+ label: 'Copy Frame',
400
+ category: 'Animation',
401
+ });
402
+
403
+ // --- paste_frame ---
404
+ api.addCommand('paste_frame', {
405
+ tier: 'project',
406
+ undoable: true,
407
+ execute() {
408
+ if (!copiedFrames || copiedFrames.length === 0) {
409
+ notificationState.push({
410
+ id: 'no-frame-copied',
411
+ message: 'No frame copied',
412
+ type: 'warning',
413
+ autoDismissMs: 3000,
414
+ });
415
+ return;
416
+ }
417
+
418
+ const currentIdx = frameModel.getCurrentFrameIndex();
419
+ const pastedIndices: number[] = [];
420
+
421
+ // Insert each copied frame sequentially after the current frame
422
+ for (let i = 0; i < copiedFrames.length; i++) {
423
+ const insertAfter = currentIdx + i;
424
+ const newFrame = frameModel.addFrame({ afterIndex: insertAfter });
425
+ // Copy clipboard pixel data into the new frame
426
+ const src = copiedFrames[i];
427
+ if (src) {
428
+ for (const [layerId, buffer] of src) {
429
+ newFrame.pixelData.set(layerId, buffer.clone());
430
+ }
431
+ }
432
+ pastedIndices.push(newFrame.index);
433
+ }
434
+
435
+ notificationState.push({
436
+ id: 'frame-pasted',
437
+ message: `Pasted ${String(pastedIndices.length)} frame(s)`,
438
+ type: 'info',
439
+ autoDismissMs: 1500,
440
+ });
441
+
442
+ const snapshot: PasteFrameSnapshot = { pastedFrameIndices: pastedIndices };
443
+ return snapshot;
444
+ },
445
+ undo(_params, _ctx, snapshot) {
446
+ const typed = snapshot as PasteFrameSnapshot | undefined;
447
+ if (!typed) return;
448
+ // Remove pasted frames in reverse order to preserve indices
449
+ const sorted = [...typed.pastedFrameIndices].sort((a, b) => b - a);
450
+ for (const idx of sorted) {
451
+ frameModel._removeFrameAt(idx);
452
+ }
453
+ },
454
+ describe() {
455
+ return 'Pasted frame(s)';
456
+ },
457
+ label: 'Paste Frame',
458
+ category: 'Animation',
459
+ });
460
+
461
+ // --- delete_selected_frames ---
462
+ api.addCommand('delete_selected_frames', {
463
+ tier: 'project',
464
+ undoable: true,
465
+ execute() {
466
+ const allFrames = frameModel.getFrames();
467
+
468
+ // Use multi-selection if available, otherwise fall back to current frame
469
+ const indicesToDelete =
470
+ frameSelection.count > 1
471
+ ? frameSelection.sortedIndices
472
+ : [frameModel.getCurrentFrameIndex()];
473
+
474
+ if (allFrames.length <= 1) {
475
+ notificationState.push({
476
+ id: 'cannot-delete-last-frame',
477
+ message: 'Cannot delete the last frame',
478
+ type: 'warning',
479
+ autoDismissMs: 3000,
480
+ });
481
+ return;
482
+ }
483
+
484
+ const previousFrameIndex = frameModel.getCurrentFrameIndex();
485
+ // Snapshot frames before removal (sorted ascending)
486
+ const snapshots = indicesToDelete.flatMap((idx) => {
487
+ const f = allFrames[idx];
488
+ return f ? [snapshotFrame(f)] : [];
489
+ });
490
+
491
+ frameModel.removeFrames(indicesToDelete);
492
+ frameSelection.reset();
493
+
494
+ const count = snapshots.length;
495
+ notificationState.push({
496
+ id: 'frames-deleted',
497
+ message: `Deleted ${String(count)} frame(s)`,
498
+ type: 'info',
499
+ autoDismissMs: 1500,
500
+ });
501
+
502
+ const snapshot: DeleteSelectedFramesSnapshot = {
503
+ removedFrames: snapshots,
504
+ previousFrameIndex,
505
+ };
506
+ return snapshot;
507
+ },
508
+ undo(_params, _ctx, snapshot) {
509
+ const typed = snapshot as DeleteSelectedFramesSnapshot | undefined;
510
+ if (!typed) return;
511
+ // Rebuild Frame objects from snapshots and insert each at its
512
+ // original position (ascending order so each insertion correctly
513
+ // shifts subsequent indices)
514
+ for (const snap of typed.removedFrames) {
515
+ const pixelData = new Map<string, PixelBuffer>();
516
+ for (const entry of snap.pixelDataEntries) {
517
+ pixelData.set(entry.layerId, new PixelBuffer(entry.width, entry.height, entry.data));
518
+ }
519
+ const frame: Frame = {
520
+ id: snap.id,
521
+ index: snap.index,
522
+ durationMs: snap.durationMs,
523
+ pixelData,
524
+ };
525
+ frameModel._insertFrameAt(snap.index, frame);
526
+ }
527
+ // Restore previous current frame if valid
528
+ const allFrames = frameModel.getFrames();
529
+ if (typed.previousFrameIndex < allFrames.length) {
530
+ frameModel.setCurrentFrame(typed.previousFrameIndex);
531
+ }
532
+ },
533
+ describe() {
534
+ return 'Deleted frame(s)';
535
+ },
536
+ label: 'Delete Selected Frames',
537
+ category: 'Animation',
538
+ });
539
+ },
540
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Animation Preview Panel Plugin -- registers AnimationPreviewPanel
3
+ * as a dockable panel. The panel plays back the current animation using
4
+ * the animation-preview state machine and composites frames on demand.
5
+ *
6
+ * Discovered by bootstrap via the `*-plugin.ts` glob.
7
+ */
8
+
9
+ import type { PluginModule } from '../core/plugin-loader.js';
10
+ import AnimationPreviewPanel from './AnimationPreviewPanel.svelte';
11
+
12
+ export const animationPreviewPanelPlugin: PluginModule = {
13
+ name: 'ui/animation-preview-panel',
14
+ version: '1.0.0',
15
+ dependencies: [],
16
+ register(api) {
17
+ api.addPanel('animation-preview', {
18
+ title: 'Animation Preview',
19
+ component: AnimationPreviewPanel,
20
+ position: 'right',
21
+ minWidth: 180,
22
+ maxWidth: 400,
23
+ });
24
+ },
25
+ };