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,201 @@
1
+ /**
2
+ * Platform-adaptive file download helper.
3
+ *
4
+ * On desktop (pywebview): opens a native save dialog, then writes the file
5
+ * via the pywebview bridge. For batch exports, opens a directory picker
6
+ * and writes all files into the chosen folder.
7
+ *
8
+ * On browser: falls back to the classic <a download> trick with
9
+ * URL.createObjectURL.
10
+ */
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Base64 helper
14
+ // ---------------------------------------------------------------------------
15
+
16
+ function uint8ArrayToBase64(bytes: Uint8Array): string {
17
+ let binary = '';
18
+ for (let i = 0; i < bytes.length; i++) {
19
+ binary += String.fromCharCode(bytes[i]!);
20
+ }
21
+ return btoa(binary);
22
+ }
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Desktop detection
26
+ // ---------------------------------------------------------------------------
27
+
28
+ /** Check if running inside a pywebview desktop window. */
29
+ export function isDesktop(): boolean {
30
+ return !!window.pywebview;
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Single-file download
35
+ // ---------------------------------------------------------------------------
36
+
37
+ export interface DownloadFileOptions {
38
+ /** The blob to save. */
39
+ blob: Blob;
40
+ /** Suggested filename including extension (e.g. "sprite.png"). */
41
+ filename: string;
42
+ /** Filter label for the native dialog (e.g. "PNG Image"). */
43
+ filterName?: string;
44
+ /** Allowed extensions without dot (e.g. ["png"]). */
45
+ filterExtensions?: string[];
46
+ }
47
+
48
+ /**
49
+ * Download / save a single file. On desktop a native save dialog is shown;
50
+ * on browser the file is downloaded directly.
51
+ *
52
+ * Returns `true` if the file was saved, `false` if the user cancelled
53
+ * the native dialog (browser always returns `true`).
54
+ */
55
+ export async function downloadFile(opts: DownloadFileOptions): Promise<boolean> {
56
+ if (isDesktop()) {
57
+ return downloadFileDesktop(opts);
58
+ }
59
+ downloadFileBrowser(opts.blob, opts.filename);
60
+ return true;
61
+ }
62
+
63
+ async function downloadFileDesktop(opts: DownloadFileOptions): Promise<boolean> {
64
+ const ext = opts.filterExtensions
65
+ ?? [opts.filename.split('.').pop() ?? 'bin'];
66
+ const filterName = opts.filterName
67
+ ?? ext.map((e) => e.toUpperCase()).join('/') + ' File';
68
+
69
+ const buffer = new Uint8Array(await opts.blob.arrayBuffer());
70
+ const b64 = uint8ArrayToBase64(buffer);
71
+
72
+ const path = await window.pywebview!.api.save_file(
73
+ b64, opts.filename, filterName, ext,
74
+ );
75
+ return path !== null;
76
+ }
77
+
78
+ function downloadFileBrowser(blob: Blob, filename: string): void {
79
+ const url = URL.createObjectURL(blob);
80
+ const a = document.createElement('a');
81
+ a.href = url;
82
+ a.download = filename;
83
+ document.body.appendChild(a);
84
+ a.click();
85
+ setTimeout(() => {
86
+ document.body.removeChild(a);
87
+ URL.revokeObjectURL(url);
88
+ }, 100);
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Multi-file download (batch)
93
+ // ---------------------------------------------------------------------------
94
+
95
+ export interface BatchFileEntry {
96
+ blob: Blob;
97
+ filename: string;
98
+ }
99
+
100
+ /**
101
+ * Download multiple files. On desktop a directory picker is shown and all
102
+ * files are written into the chosen folder. On browser the files are
103
+ * downloaded sequentially with a small delay between each to avoid
104
+ * throttling.
105
+ *
106
+ * Returns `true` if files were saved, `false` if the user cancelled
107
+ * the directory picker (browser always returns `true`).
108
+ */
109
+ export async function downloadBatch(entries: BatchFileEntry[]): Promise<boolean> {
110
+ if (entries.length === 0) return true;
111
+ if (isDesktop()) {
112
+ return downloadBatchDesktop(entries);
113
+ }
114
+ downloadBatchBrowser(entries, 0);
115
+ return true;
116
+ }
117
+
118
+ async function downloadBatchDesktop(entries: BatchFileEntry[]): Promise<boolean> {
119
+ const dir = await window.pywebview!.api.pick_directory();
120
+ if (!dir) return false;
121
+
122
+ const files = await Promise.all(
123
+ entries.map(async (entry) => ({
124
+ name: entry.filename,
125
+ data: uint8ArrayToBase64(new Uint8Array(await entry.blob.arrayBuffer())),
126
+ })),
127
+ );
128
+
129
+ return window.pywebview!.api.write_files_to_directory(dir, files);
130
+ }
131
+
132
+ /**
133
+ * Trigger browser downloads one at a time with a small delay between
134
+ * each to prevent the browser from blocking rapid successive downloads.
135
+ */
136
+ function downloadBatchBrowser(entries: BatchFileEntry[], index: number): void {
137
+ if (index >= entries.length) return;
138
+ const entry = entries[index];
139
+ if (!entry) return;
140
+ downloadFileBrowser(entry.blob, entry.filename);
141
+ // 50ms gap so the browser does not throttle/block consecutive downloads.
142
+ setTimeout(() => { downloadBatchBrowser(entries, index + 1); }, 50);
143
+ }
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // Multi-file companion download (e.g. PNG + JSON from same export)
147
+ // ---------------------------------------------------------------------------
148
+
149
+ /**
150
+ * Download multiple companion files from a single export operation.
151
+ * On desktop the first file gets a save dialog; subsequent files are written
152
+ * to the same directory as the first. On browser each file is downloaded.
153
+ *
154
+ * Returns `true` if files were saved, `false` if the user cancelled.
155
+ */
156
+ export async function downloadCompanionFiles(
157
+ entries: DownloadFileOptions[],
158
+ ): Promise<boolean> {
159
+ if (entries.length === 0) return true;
160
+ if (isDesktop()) {
161
+ return downloadCompanionFilesDesktop(entries);
162
+ }
163
+ // Browser: download each file sequentially
164
+ for (const entry of entries) {
165
+ downloadFileBrowser(entry.blob, entry.filename);
166
+ }
167
+ return true;
168
+ }
169
+
170
+ async function downloadCompanionFilesDesktop(
171
+ entries: DownloadFileOptions[],
172
+ ): Promise<boolean> {
173
+ // Show save dialog for the first (primary) file
174
+ const first = entries[0]!;
175
+ const ext = first.filterExtensions
176
+ ?? [first.filename.split('.').pop() ?? 'bin'];
177
+ const filterName = first.filterName
178
+ ?? ext.map((e) => e.toUpperCase()).join('/') + ' File';
179
+
180
+ const buffer = new Uint8Array(await first.blob.arrayBuffer());
181
+ const b64 = uint8ArrayToBase64(buffer);
182
+
183
+ const path = await window.pywebview!.api.save_file(
184
+ b64, first.filename, filterName, ext,
185
+ );
186
+ if (!path) return false;
187
+
188
+ // Write companion files to the same directory
189
+ if (entries.length > 1) {
190
+ const dir = path.substring(0, path.lastIndexOf('/'));
191
+ const companionFiles = await Promise.all(
192
+ entries.slice(1).map(async (entry) => ({
193
+ name: entry.filename,
194
+ data: uint8ArrayToBase64(new Uint8Array(await entry.blob.arrayBuffer())),
195
+ })),
196
+ );
197
+ await window.pywebview!.api.write_files_to_directory(dir, companionFiles);
198
+ }
199
+
200
+ return true;
201
+ }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * PNG metadata injection -- embeds tEXt chunks into PNG files.
3
+ *
4
+ * PNG files have a well-defined chunk structure. After the 8-byte signature,
5
+ * the file is a sequence of chunks: [4-byte length][4-byte type][data][4-byte CRC].
6
+ * tEXt chunks store key-value pairs separated by a null byte.
7
+ *
8
+ * This module inserts tEXt chunks after IHDR and before the first IDAT chunk,
9
+ * which is the standard location for ancillary metadata chunks.
10
+ */
11
+
12
+ // --- CRC32 ---
13
+
14
+ /** Pre-computed CRC32 lookup table (IEEE polynomial 0xEDB88320). */
15
+ const crcTable = buildCrcTable();
16
+
17
+ function buildCrcTable(): Uint32Array {
18
+ const table = new Uint32Array(256);
19
+ for (let n = 0; n < 256; n++) {
20
+ let c = n;
21
+ for (let k = 0; k < 8; k++) {
22
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
23
+ }
24
+ table[n] = c;
25
+ }
26
+ return table;
27
+ }
28
+
29
+ /** Compute CRC32 over one or more byte arrays. */
30
+ function crc32(...buffers: Uint8Array[]): number {
31
+ let crc = 0xffffffff;
32
+ // Uint8Array indexing is `number | undefined` under noUncheckedIndexedAccess;
33
+ // ?? 0 fallbacks never trigger because i < buf.length and the table is dense.
34
+ for (const buf of buffers) {
35
+ for (let i = 0; i < buf.length; i++) {
36
+ crc = (crcTable[(crc ^ (buf[i] ?? 0)) & 0xff] ?? 0) ^ (crc >>> 8);
37
+ }
38
+ }
39
+ return (crc ^ 0xffffffff) >>> 0;
40
+ }
41
+
42
+ // --- Text encoding helpers ---
43
+
44
+ const textEncoder = new TextEncoder();
45
+
46
+ /** Encode a string as Latin-1 bytes (values > 255 are clamped). */
47
+ function toLatin1(str: string): Uint8Array {
48
+ const bytes = new Uint8Array(str.length);
49
+ for (let i = 0; i < str.length; i++) {
50
+ bytes[i] = str.charCodeAt(i) & 0xff;
51
+ }
52
+ return bytes;
53
+ }
54
+
55
+ // --- tEXt chunk construction ---
56
+
57
+ /** PNG chunk type identifier for tEXt. */
58
+ const TEXT_TYPE = textEncoder.encode('tEXt');
59
+
60
+ /**
61
+ * Build a complete PNG tEXt chunk (length + type + data + CRC).
62
+ *
63
+ * tEXt data format: keyword (1-79 Latin-1 chars) + null byte + value (Latin-1).
64
+ * The CRC covers the type field and data, but not the length field.
65
+ */
66
+ function buildTextChunk(key: string, value: string): Uint8Array {
67
+ // Clamp key to 79 characters per PNG spec
68
+ const clampedKey = key.slice(0, 79);
69
+ const keyBytes = toLatin1(clampedKey);
70
+ const valueBytes = toLatin1(value);
71
+
72
+ // data = key + \0 + value
73
+ const dataLength = keyBytes.length + 1 + valueBytes.length;
74
+ // chunk = 4 (length) + 4 (type) + dataLength + 4 (CRC)
75
+ const chunk = new Uint8Array(4 + 4 + dataLength + 4);
76
+ const view = new DataView(chunk.buffer);
77
+
78
+ // Write length (big-endian)
79
+ view.setUint32(0, dataLength);
80
+
81
+ // Write chunk type "tEXt"
82
+ chunk.set(TEXT_TYPE, 4);
83
+
84
+ // Write key + null separator + value
85
+ chunk.set(keyBytes, 8);
86
+ chunk[8 + keyBytes.length] = 0;
87
+ chunk.set(valueBytes, 8 + keyBytes.length + 1);
88
+
89
+ // Compute CRC over type + data
90
+ const crcData = chunk.slice(4, 8 + dataLength);
91
+ const crcValue = crc32(crcData);
92
+ view.setUint32(8 + dataLength, crcValue);
93
+
94
+ return chunk;
95
+ }
96
+
97
+ // --- PNG parsing helpers ---
98
+
99
+ /** PNG magic signature (8 bytes). */
100
+ const PNG_SIGNATURE_LENGTH = 8;
101
+
102
+ /**
103
+ * Read a 4-byte ASCII chunk type from a DataView at the given offset.
104
+ */
105
+ function readChunkType(view: DataView, offset: number): string {
106
+ return String.fromCharCode(
107
+ view.getUint8(offset),
108
+ view.getUint8(offset + 1),
109
+ view.getUint8(offset + 2),
110
+ view.getUint8(offset + 3),
111
+ );
112
+ }
113
+
114
+ /**
115
+ * Find the byte offset of the first IDAT chunk in a PNG buffer.
116
+ *
117
+ * Walks the chunk list starting after the 8-byte signature.
118
+ * Returns the offset of the IDAT chunk's length field, which is where
119
+ * we want to splice in tEXt chunks.
120
+ */
121
+ function findFirstIdatOffset(view: DataView): number {
122
+ let offset = PNG_SIGNATURE_LENGTH;
123
+
124
+ while (offset < view.byteLength) {
125
+ const chunkDataLength = view.getUint32(offset);
126
+ const chunkType = readChunkType(view, offset + 4);
127
+
128
+ if (chunkType === 'IDAT') {
129
+ return offset;
130
+ }
131
+
132
+ // Advance past this chunk: 4 (length) + 4 (type) + data + 4 (CRC)
133
+ offset += 4 + 4 + chunkDataLength + 4;
134
+ }
135
+
136
+ throw new Error('PNG has no IDAT chunk -- file is malformed.');
137
+ }
138
+
139
+ // --- Public API ---
140
+
141
+ /**
142
+ * Embed tEXt metadata key-value pairs into a PNG blob.
143
+ *
144
+ * Inserts tEXt chunks after IHDR and before the first IDAT chunk.
145
+ * Returns a new Blob; the original is not mutated.
146
+ */
147
+ export async function embedPngMetadata(
148
+ pngBlob: Blob,
149
+ metadata: Record<string, string>,
150
+ ): Promise<Blob> {
151
+ const buffer = await pngBlob.arrayBuffer();
152
+ const view = new DataView(buffer);
153
+
154
+ const insertOffset = findFirstIdatOffset(view);
155
+
156
+ // Build all tEXt chunks
157
+ const chunks: Uint8Array[] = [];
158
+ for (const [key, value] of Object.entries(metadata)) {
159
+ chunks.push(buildTextChunk(key, value));
160
+ }
161
+
162
+ // Total bytes to insert
163
+ const insertLength = chunks.reduce((sum, c) => sum + c.length, 0);
164
+
165
+ // Assemble: [before IDAT] + [tEXt chunks] + [IDAT onward]
166
+ const before = new Uint8Array(buffer, 0, insertOffset);
167
+ const after = new Uint8Array(buffer, insertOffset);
168
+
169
+ const result = new Uint8Array(before.length + insertLength + after.length);
170
+ result.set(before, 0);
171
+
172
+ let writeOffset = before.length;
173
+ for (const chunk of chunks) {
174
+ result.set(chunk, writeOffset);
175
+ writeOffset += chunk.length;
176
+ }
177
+
178
+ result.set(after, writeOffset);
179
+
180
+ return new Blob([result], { type: 'image/png' });
181
+ }