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,171 @@
1
+ #!/usr/bin/env python3
2
+ """Migrate PixelWeaver projects from old single-frame format to multi-frame format.
3
+
4
+ Old format (per canvas):
5
+ canvases/<canvas_name>/
6
+ canvas.json
7
+ frames/0/
8
+ layer-<id>.png
9
+
10
+ New format (per canvas):
11
+ canvases/<canvas_name>/
12
+ canvas.json
13
+ frames.json -- frame manifest with IDs, durations, playback state
14
+ frames/<uuid>/ -- UUID-named directory instead of "0"
15
+ layer-<id>.png
16
+
17
+ This script is idempotent: canvases that already have a frames.json are skipped.
18
+ It can safely be run multiple times against the same data directory.
19
+
20
+ The default PixelWeaver data directory is "./projects" (relative to the server
21
+ working directory), configurable via the --data-dir CLI flag when running the
22
+ PixelWeaver server.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import json
29
+ import sys
30
+ import uuid
31
+ from pathlib import Path
32
+
33
+
34
+ def find_projects(data_dir: Path) -> list[Path]:
35
+ """Return paths to all project directories (those containing project.json)."""
36
+ if not data_dir.is_dir():
37
+ return []
38
+ return sorted(
39
+ d for d in data_dir.iterdir()
40
+ if d.is_dir() and (d / "project.json").exists()
41
+ )
42
+
43
+
44
+ def find_canvases(project_dir: Path) -> list[Path]:
45
+ """Return paths to all canvas directories within a project."""
46
+ canvases_dir = project_dir / "canvases"
47
+ if not canvases_dir.is_dir():
48
+ return []
49
+ return sorted(
50
+ d for d in canvases_dir.iterdir() if d.is_dir()
51
+ )
52
+
53
+
54
+ def migrate_canvas(canvas_dir: Path, *, dry_run: bool) -> str | None:
55
+ """Migrate a single canvas from old format to new format.
56
+
57
+ Returns a description of what was done, or None if the canvas was skipped.
58
+ """
59
+ frames_json_path = canvas_dir / "frames.json"
60
+ old_frame_dir = canvas_dir / "frames" / "0"
61
+
62
+ # Already migrated -- frames.json exists
63
+ if frames_json_path.exists():
64
+ return None
65
+
66
+ # No old-format data to migrate
67
+ if not old_frame_dir.is_dir():
68
+ return None
69
+
70
+ frame_id = str(uuid.uuid4())
71
+ new_frame_dir = canvas_dir / "frames" / frame_id
72
+
73
+ frames_manifest = {
74
+ "frames": [{"id": frame_id, "duration_ms": None}],
75
+ "current_frame_index": 0,
76
+ "global_fps": 12.0,
77
+ "origin_x": 0,
78
+ "origin_y": 0,
79
+ }
80
+
81
+ if dry_run:
82
+ return (
83
+ f" Would rename frames/0/ -> frames/{frame_id}/\n"
84
+ f" Would create frames.json"
85
+ )
86
+
87
+ # Rename the directory first, then write the manifest.
88
+ # If the rename succeeds but the write fails, re-running the script
89
+ # won't find frames/0/ and will skip -- no data loss.
90
+ old_frame_dir.rename(new_frame_dir)
91
+ frames_json_path.write_text(json.dumps(frames_manifest, indent=2))
92
+
93
+ return (
94
+ f" Renamed frames/0/ -> frames/{frame_id}/\n"
95
+ f" Created frames.json"
96
+ )
97
+
98
+
99
+ def main(argv: list[str] | None = None) -> int:
100
+ parser = argparse.ArgumentParser(
101
+ description=(
102
+ "Migrate PixelWeaver projects from old single-frame (frames/0/) format "
103
+ "to the new multi-frame (frames/<uuid>/ + frames.json) format."
104
+ ),
105
+ epilog=(
106
+ "The default PixelWeaver data directory is './projects' (relative to the "
107
+ "server working directory), configurable via the --data-dir flag on the "
108
+ "PixelWeaver CLI."
109
+ ),
110
+ )
111
+ parser.add_argument(
112
+ "--data-dir",
113
+ type=Path,
114
+ required=True,
115
+ help="Base directory containing PixelWeaver projects (e.g. ./projects)",
116
+ )
117
+ parser.add_argument(
118
+ "--dry-run",
119
+ action="store_true",
120
+ help="Show what would be done without making any changes",
121
+ )
122
+ args = parser.parse_args(argv)
123
+
124
+ data_dir: Path = args.data_dir.expanduser().resolve()
125
+ dry_run: bool = args.dry_run
126
+
127
+ if not data_dir.is_dir():
128
+ print(f"Error: data directory does not exist: {data_dir}", file=sys.stderr)
129
+ return 1
130
+
131
+ if dry_run:
132
+ print("[DRY RUN] No changes will be made.\n")
133
+
134
+ projects = find_projects(data_dir)
135
+ if not projects:
136
+ print(f"No projects found in {data_dir}")
137
+ return 0
138
+
139
+ total_migrated = 0
140
+ total_skipped = 0
141
+
142
+ for project_dir in projects:
143
+ project_name = project_dir.name
144
+ canvases = find_canvases(project_dir)
145
+
146
+ if not canvases:
147
+ continue
148
+
149
+ print(f"Project: {project_name}")
150
+
151
+ for canvas_dir in canvases:
152
+ canvas_name = canvas_dir.name
153
+ result = migrate_canvas(canvas_dir, dry_run=dry_run)
154
+
155
+ if result is None:
156
+ total_skipped += 1
157
+ else:
158
+ total_migrated += 1
159
+ print(f" Canvas: {canvas_name}")
160
+ print(result)
161
+
162
+ # Summary
163
+ print()
164
+ verb = "would be" if dry_run else "were"
165
+ print(f"Done. {total_migrated} canvas(es) {verb} migrated, {total_skipped} skipped.")
166
+
167
+ return 0
168
+
169
+
170
+ if __name__ == "__main__":
171
+ raise SystemExit(main())
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Smoke test: start server, run CRUD checks, verify SPA serving
5
+ # Usage: ./scripts/smoke-test.sh [--with-frontend]
6
+ #
7
+ # --with-frontend: also build the frontend and verify SPA/static serving
8
+
9
+ PORT=7799
10
+ BASE="http://127.0.0.1:$PORT"
11
+ SERVER_PID=""
12
+
13
+ cleanup() {
14
+ if [ -n "$SERVER_PID" ]; then
15
+ kill "$SERVER_PID" 2>/dev/null || true
16
+ wait "$SERVER_PID" 2>/dev/null || true
17
+ fi
18
+ }
19
+ trap cleanup EXIT
20
+
21
+ WITH_FRONTEND=false
22
+ if [ "${1:-}" = "--with-frontend" ]; then
23
+ WITH_FRONTEND=true
24
+ fi
25
+
26
+ echo "=== Building frontend ==="
27
+ if $WITH_FRONTEND; then
28
+ npm run build
29
+ fi
30
+
31
+ echo "=== Starting server on port $PORT ==="
32
+ uv run pixelweaver serve --port "$PORT" &
33
+ SERVER_PID=$!
34
+
35
+ # Wait for server
36
+ for i in $(seq 1 30); do
37
+ if curl -sf "$BASE/health" > /dev/null 2>&1; then
38
+ break
39
+ fi
40
+ sleep 0.5
41
+ done
42
+
43
+ echo "=== Health check ==="
44
+ curl -sf "$BASE/health" | python3 -c "import sys,json; d=json.load(sys.stdin); assert d['status']=='ok'; print('PASS')"
45
+
46
+ echo "=== Create project ==="
47
+ curl -sf -X POST "$BASE/api/projects" \
48
+ -H "Content-Type: application/json" \
49
+ -d '{"name":"smoke","width":32,"height":32}' > /dev/null
50
+ echo "PASS"
51
+
52
+ echo "=== List projects ==="
53
+ curl -sf "$BASE/api/projects" | python3 -c "import sys,json; d=json.load(sys.stdin); assert 'smoke' in d['projects']; print('PASS')"
54
+
55
+ echo "=== Get project ==="
56
+ curl -sf "$BASE/api/projects/smoke" | python3 -c "import sys,json; d=json.load(sys.stdin); assert 'canvases' in d; print('PASS')"
57
+
58
+ echo "=== Export PNG ==="
59
+ STATUS=$(curl -sf -o /dev/null -w "%{http_code}" "$BASE/api/projects/smoke/export/png/canvas-0/0")
60
+ [ "$STATUS" = "200" ] && echo "PASS" || (echo "FAIL: $STATUS"; exit 1)
61
+
62
+ echo "=== Delete project ==="
63
+ STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE "$BASE/api/projects/smoke")
64
+ [ "$STATUS" = "204" ] && echo "PASS" || (echo "FAIL: $STATUS"; exit 1)
65
+
66
+ if $WITH_FRONTEND && [ -f dist/index.html ]; then
67
+ echo "=== SPA root ==="
68
+ STATUS=$(curl -sf -o /dev/null -w "%{http_code}" "$BASE/")
69
+ [ "$STATUS" = "200" ] && echo "PASS" || (echo "FAIL: $STATUS"; exit 1)
70
+
71
+ echo "=== SPA fallback ==="
72
+ STATUS=$(curl -sf -o /dev/null -w "%{http_code}" "$BASE/unknown/path")
73
+ [ "$STATUS" = "200" ] && echo "PASS" || (echo "FAIL: $STATUS"; exit 1)
74
+ fi
75
+
76
+ echo ""
77
+ echo "=== ALL SMOKE TESTS PASSED ==="
package/selfdoc.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "language": "python",
3
+ "source": [
4
+ "server/src/"
5
+ ],
6
+ "docs": "docs/",
7
+ "output": "docs/_build/",
8
+ "base_url": "https://pixelweaver.smmh.dev",
9
+ "root_files": ["docs/_README.md"]
10
+ }
File without changes
@@ -0,0 +1 @@
1
+ """PixelWeaver collaboration server."""
@@ -0,0 +1,4 @@
1
+ """Enable python -m pixelweaver."""
2
+ from pixelweaver.cli import main
3
+
4
+ main()
@@ -0,0 +1,114 @@
1
+ """Auto-save: periodic flush of in-memory state to disk.
2
+
3
+ The auto-saver runs as an asyncio background task. State is saved when:
4
+ - A debounce timer expires after the last state mutation, OR
5
+ - A fixed interval elapses since the last save
6
+
7
+ Whichever fires first triggers a save, preventing both data loss and
8
+ excessive I/O.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ import logging
15
+ import time
16
+
17
+ from pixelweaver.state import ServerState
18
+ from pixelweaver.storage import save_project
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class AutoSaver:
24
+ """Periodically flushes server state to disk."""
25
+
26
+ def __init__(
27
+ self,
28
+ state: ServerState,
29
+ save_dir: str,
30
+ debounce_ms: int = 500,
31
+ interval_s: int = 30,
32
+ ) -> None:
33
+ self.state = state
34
+ self.save_dir = save_dir
35
+ self.debounce_ms = debounce_ms
36
+ self.interval_s = interval_s
37
+ self._dirty = False
38
+ self._last_save: float = 0
39
+ self._task: asyncio.Task[None] | None = None
40
+ self._debounce_task: asyncio.Task[None] | None = None
41
+
42
+ def mark_dirty(self) -> None:
43
+ """Called after any state change to schedule a debounced save."""
44
+ self._dirty = True
45
+ # Reset the debounce timer
46
+ if self._debounce_task is not None and not self._debounce_task.done():
47
+ self._debounce_task.cancel()
48
+ self._debounce_task = asyncio.ensure_future(self._debounce_save())
49
+
50
+ async def _debounce_save(self) -> None:
51
+ """Wait for the debounce period, then save if still dirty."""
52
+ try:
53
+ await asyncio.sleep(self.debounce_ms / 1000.0)
54
+ if self._dirty:
55
+ await self.save_now()
56
+ except asyncio.CancelledError:
57
+ pass # Debounce was reset by a newer mutation
58
+
59
+ async def start(self) -> None:
60
+ """Start the auto-save background task (periodic interval saves)."""
61
+ self._task = asyncio.ensure_future(self._periodic_loop())
62
+ logger.info(
63
+ "Auto-saver started (debounce=%dms, interval=%ds, dir=%s)",
64
+ self.debounce_ms,
65
+ self.interval_s,
66
+ self.save_dir,
67
+ )
68
+
69
+ async def stop(self) -> None:
70
+ """Stop the background task gracefully, saving any pending changes."""
71
+ if self._task is not None:
72
+ self._task.cancel()
73
+ try:
74
+ await self._task
75
+ except asyncio.CancelledError:
76
+ pass
77
+ if self._debounce_task is not None:
78
+ self._debounce_task.cancel()
79
+ # Final save if dirty
80
+ if self._dirty:
81
+ await self.save_now()
82
+
83
+ async def _periodic_loop(self) -> None:
84
+ """Run periodic saves on a fixed interval."""
85
+ try:
86
+ while True:
87
+ await asyncio.sleep(self.interval_s)
88
+ if self._dirty:
89
+ await self.save_now()
90
+ except asyncio.CancelledError:
91
+ pass
92
+
93
+ async def save_now(self) -> None:
94
+ """Flush all project state to disk immediately."""
95
+ if not self.state.projects:
96
+ self._dirty = False
97
+ return
98
+
99
+ # Track per-loop failures so a single failed project doesn't lose
100
+ # the dirty marker for the rest. _dirty is a single flag here, so
101
+ # if anything failed we leave it set and the next debounce/interval
102
+ # tick will retry the whole batch.
103
+ any_failed = False
104
+ for project in self.state.projects.values():
105
+ try:
106
+ await asyncio.to_thread(save_project, project, self.save_dir)
107
+ except Exception:
108
+ any_failed = True
109
+ logger.error("Failed to save project %s", project.name, exc_info=True)
110
+
111
+ if not any_failed:
112
+ self._dirty = False
113
+ self._last_save = time.monotonic()
114
+ logger.debug("Auto-saved %d project(s) to %s", len(self.state.projects), self.save_dir)
@@ -0,0 +1,127 @@
1
+ """Desktop file I/O bridge for pywebview.
2
+
3
+ Provides native file dialogs and filesystem access callable from JavaScript
4
+ via pywebview's js_api mechanism. All binary data is transferred as
5
+ base64-encoded strings since the JS bridge serializes return values as JSON.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+ import logging
12
+ from pathlib import Path
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class DesktopBridge:
18
+ """File I/O bridge exposed to the frontend via pywebview js_api.
19
+
20
+ Methods on this class become callable from JavaScript as:
21
+ window.pywebview.api.method_name(args)
22
+ """
23
+
24
+ def save_file(
25
+ self,
26
+ data_b64: str,
27
+ default_name: str,
28
+ filter_name: str,
29
+ extensions: list[str],
30
+ ) -> str | None:
31
+ """Show a native Save dialog and write the file.
32
+
33
+ Returns the saved file path, or None if the user cancelled.
34
+ """
35
+ import webview
36
+
37
+ window = webview.windows[0]
38
+ file_types = (f"{filter_name} ({' '.join('*.' + e for e in extensions)})",)
39
+ result = window.create_file_dialog(
40
+ webview.SAVE_DIALOG,
41
+ save_filename=default_name,
42
+ file_types=file_types,
43
+ )
44
+ if not result:
45
+ return None
46
+ path = result if isinstance(result, str) else result[0]
47
+ try:
48
+ data = base64.b64decode(data_b64)
49
+ Path(path).parent.mkdir(parents=True, exist_ok=True)
50
+ Path(path).write_bytes(data)
51
+ return path
52
+ except Exception:
53
+ logger.exception("Failed to write file %s", path)
54
+ return None
55
+
56
+ def open_file(
57
+ self,
58
+ filter_name: str,
59
+ extensions: list[str],
60
+ ) -> dict | None:
61
+ """Show a native Open dialog and read the file.
62
+
63
+ Returns {"path": str, "data": base64_string} or None if cancelled.
64
+ """
65
+ import webview
66
+
67
+ window = webview.windows[0]
68
+ file_types = (f"{filter_name} ({' '.join('*.' + e for e in extensions)})",)
69
+ result = window.create_file_dialog(
70
+ webview.OPEN_DIALOG,
71
+ file_types=file_types,
72
+ )
73
+ if not result:
74
+ return None
75
+ path = result[0] if isinstance(result, (tuple, list)) else result
76
+ try:
77
+ data = Path(path).read_bytes()
78
+ return {
79
+ "path": str(path),
80
+ "data": base64.b64encode(data).decode("ascii"),
81
+ }
82
+ except Exception:
83
+ logger.exception("Failed to read file %s", path)
84
+ return None
85
+
86
+ def write_file(self, path: str, data_b64: str) -> bool:
87
+ """Write binary data to a specific path (no dialog)."""
88
+ try:
89
+ data = base64.b64decode(data_b64)
90
+ Path(path).parent.mkdir(parents=True, exist_ok=True)
91
+ Path(path).write_bytes(data)
92
+ return True
93
+ except Exception:
94
+ logger.exception("Failed to write file %s", path)
95
+ return False
96
+
97
+ def pick_directory(self) -> str | None:
98
+ """Show a native directory picker. Returns path or None if cancelled."""
99
+ import webview
100
+
101
+ window = webview.windows[0]
102
+ result = window.create_file_dialog(webview.FOLDER_DIALOG)
103
+ if not result:
104
+ return None
105
+ return result[0] if isinstance(result, (tuple, list)) else str(result)
106
+
107
+ def write_files_to_directory(
108
+ self,
109
+ dir_path: str,
110
+ files: list[dict],
111
+ ) -> bool:
112
+ """Write multiple files to a directory.
113
+
114
+ Each entry in files should be {"name": str, "data": base64_string}.
115
+ """
116
+ try:
117
+ base = Path(dir_path)
118
+ base.mkdir(parents=True, exist_ok=True)
119
+ for entry in files:
120
+ file_path = base / entry["name"]
121
+ file_path.parent.mkdir(parents=True, exist_ok=True)
122
+ data = base64.b64decode(entry["data"])
123
+ file_path.write_bytes(data)
124
+ return True
125
+ except Exception:
126
+ logger.exception("Failed to write files to %s", dir_path)
127
+ return False