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,136 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import * as ft from './frame-tags.svelte.js';
3
+
4
+ beforeEach(() => {
5
+ ft._resetForTesting();
6
+ });
7
+
8
+ describe('addTag', () => {
9
+ it('should add a tag to the root list', () => {
10
+ const tag = ft.addTag('Walk', 0, 3);
11
+ expect(ft.getTags()).toHaveLength(1);
12
+ expect(tag.name).toBe('Walk');
13
+ expect(tag.startFrame).toBe(0);
14
+ expect(tag.endFrame).toBe(3);
15
+ });
16
+
17
+ it('should assign an auto color when none provided', () => {
18
+ const tag = ft.addTag('Run', 0, 5);
19
+ expect(tag.color).toBeTruthy();
20
+ expect(tag.color.startsWith('#')).toBe(true);
21
+ });
22
+
23
+ it('should use a custom color when provided', () => {
24
+ const tag = ft.addTag('Idle', 0, 2, { color: '#AABBCC' });
25
+ expect(tag.color).toBe('#AABBCC');
26
+ });
27
+
28
+ it('should throw if startFrame > endFrame', () => {
29
+ expect(() => ft.addTag('Bad', 5, 2)).toThrow('startFrame');
30
+ });
31
+ });
32
+
33
+ describe('addTag with nesting', () => {
34
+ it('should add a child tag to a parent', () => {
35
+ const parent = ft.addTag('Walk Cycle', 0, 7);
36
+ const child = ft.addTag('Walk Step', 0, 3, { parentId: parent.id });
37
+ expect(parent.children).toHaveLength(1);
38
+ expect(child.parentId).toBe(parent.id);
39
+ });
40
+
41
+ it('should throw if parent does not exist', () => {
42
+ expect(() => ft.addTag('Orphan', 0, 1, { parentId: 'nonexistent' })).toThrow();
43
+ });
44
+ });
45
+
46
+ describe('removeTag', () => {
47
+ it('should remove a root tag', () => {
48
+ const tag = ft.addTag('Walk', 0, 3);
49
+ ft.removeTag(tag.id);
50
+ expect(ft.getTags()).toHaveLength(0);
51
+ });
52
+
53
+ it('should remove a nested tag', () => {
54
+ const parent = ft.addTag('Walk', 0, 7);
55
+ const child = ft.addTag('Step', 0, 3, { parentId: parent.id });
56
+ ft.removeTag(child.id);
57
+ expect(parent.children).toHaveLength(0);
58
+ });
59
+
60
+ it('should throw for nonexistent tag', () => {
61
+ expect(() => { ft.removeTag('nonexistent'); }).toThrow();
62
+ });
63
+ });
64
+
65
+ describe('updateTag', () => {
66
+ it('should update tag properties', () => {
67
+ const tag = ft.addTag('Walk', 0, 3);
68
+ ft.updateTag(tag.id, { name: 'Run', color: '#FF0000', startFrame: 1, endFrame: 5 });
69
+ expect(tag.name).toBe('Run');
70
+ expect(tag.color).toBe('#FF0000');
71
+ expect(tag.startFrame).toBe(1);
72
+ expect(tag.endFrame).toBe(5);
73
+ });
74
+
75
+ it('should throw if update makes startFrame > endFrame', () => {
76
+ const tag = ft.addTag('Walk', 0, 3);
77
+ expect(() => { ft.updateTag(tag.id, { startFrame: 10 }); }).toThrow();
78
+ });
79
+ });
80
+
81
+ describe('getTagsForFrame', () => {
82
+ it('should return tags spanning a given frame', () => {
83
+ ft.addTag('Walk', 0, 3);
84
+ ft.addTag('Run', 4, 7);
85
+ ft.addTag('All', 0, 7);
86
+
87
+ const tagsAt2 = ft.getTagsForFrame(2);
88
+ expect(tagsAt2).toHaveLength(2); // Walk + All
89
+ expect(tagsAt2.map((t) => t.name).sort()).toEqual(['All', 'Walk']);
90
+
91
+ const tagsAt5 = ft.getTagsForFrame(5);
92
+ expect(tagsAt5).toHaveLength(2); // Run + All
93
+ });
94
+
95
+ it('should include nested tags that span the frame', () => {
96
+ const parent = ft.addTag('Walk', 0, 7);
97
+ ft.addTag('Step', 2, 4, { parentId: parent.id });
98
+
99
+ const tagsAt3 = ft.getTagsForFrame(3);
100
+ expect(tagsAt3).toHaveLength(2); // Walk + Step
101
+ });
102
+ });
103
+
104
+ describe('getFrameRange', () => {
105
+ it('should return the tag range for a simple tag', () => {
106
+ const tag = ft.addTag('Walk', 2, 5);
107
+ expect(ft.getFrameRange(tag.id)).toEqual({ start: 2, end: 5 });
108
+ });
109
+
110
+ it('should expand range to include nested children', () => {
111
+ const parent = ft.addTag('Cycle', 2, 5);
112
+ ft.addTag('Intro', 0, 2, { parentId: parent.id });
113
+ ft.addTag('Outro', 5, 8, { parentId: parent.id });
114
+
115
+ const range = ft.getFrameRange(parent.id);
116
+ expect(range.start).toBe(0);
117
+ expect(range.end).toBe(8);
118
+ });
119
+ });
120
+
121
+ describe('serialize / deserialize', () => {
122
+ it('should roundtrip tag state', () => {
123
+ const parent = ft.addTag('Walk', 0, 7, { color: '#112233' });
124
+ ft.addTag('Step', 0, 3, { parentId: parent.id });
125
+
126
+ const serialized = ft.serialize();
127
+ ft._resetForTesting();
128
+ ft.deserialize(serialized);
129
+
130
+ expect(ft.getTags()).toHaveLength(1);
131
+ expect(ft.getTags()[0]?.name).toBe('Walk');
132
+ expect(ft.getTags()[0]?.color).toBe('#112233');
133
+ expect(ft.getTags()[0]?.children).toHaveLength(1);
134
+ expect(ft.getTags()[0]?.children[0]?.name).toBe('Step');
135
+ });
136
+ });
@@ -0,0 +1,141 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { importPng, importSpritesheet } from './import.js';
3
+
4
+ // --- Helpers ---
5
+
6
+ /** Create a fake ImageData (standard web API shape). */
7
+ function makeImageData(width: number, height: number, fill?: [number, number, number, number]): ImageData {
8
+ const data = new Uint8ClampedArray(width * height * 4);
9
+ if (fill) {
10
+ for (let i = 0; i < data.length; i += 4) {
11
+ data[i] = fill[0];
12
+ data[i + 1] = fill[1];
13
+ data[i + 2] = fill[2];
14
+ data[i + 3] = fill[3];
15
+ }
16
+ }
17
+ return { data, width, height, colorSpace: 'srgb' };
18
+ }
19
+
20
+ /** Create a horizontal strip ImageData where each frame column has a distinct color. */
21
+ function makeHorizontalStrip(
22
+ frameWidth: number,
23
+ frameHeight: number,
24
+ frameCount: number,
25
+ ): ImageData {
26
+ const totalWidth = frameWidth * frameCount;
27
+ const data = new Uint8ClampedArray(totalWidth * frameHeight * 4);
28
+
29
+ for (let f = 0; f < frameCount; f++) {
30
+ const gray = (f + 1) * 50; // 50, 100, 150, ...
31
+ for (let y = 0; y < frameHeight; y++) {
32
+ for (let x = 0; x < frameWidth; x++) {
33
+ const px = f * frameWidth + x;
34
+ const offset = (y * totalWidth + px) * 4;
35
+ data[offset] = gray;
36
+ data[offset + 1] = gray;
37
+ data[offset + 2] = gray;
38
+ data[offset + 3] = 255;
39
+ }
40
+ }
41
+ }
42
+
43
+ return { data, width: totalWidth, height: frameHeight, colorSpace: 'srgb' };
44
+ }
45
+
46
+ /** Create a vertical strip ImageData. */
47
+ function makeVerticalStrip(
48
+ frameWidth: number,
49
+ frameHeight: number,
50
+ frameCount: number,
51
+ ): ImageData {
52
+ const totalHeight = frameHeight * frameCount;
53
+ const data = new Uint8ClampedArray(frameWidth * totalHeight * 4);
54
+
55
+ for (let f = 0; f < frameCount; f++) {
56
+ const gray = (f + 1) * 50;
57
+ for (let y = 0; y < frameHeight; y++) {
58
+ const row = f * frameHeight + y;
59
+ for (let x = 0; x < frameWidth; x++) {
60
+ const offset = (row * frameWidth + x) * 4;
61
+ data[offset] = gray;
62
+ data[offset + 1] = gray;
63
+ data[offset + 2] = gray;
64
+ data[offset + 3] = 255;
65
+ }
66
+ }
67
+ }
68
+
69
+ return { data, width: frameWidth, height: totalHeight, colorSpace: 'srgb' };
70
+ }
71
+
72
+ // --- Tests ---
73
+
74
+ describe('importPng', () => {
75
+ it('should create a PixelBuffer at target dimensions', () => {
76
+ const img = makeImageData(16, 16, [255, 0, 0, 255]);
77
+ const buf = importPng(img, 8, 8);
78
+ expect(buf.width).toBe(8);
79
+ expect(buf.height).toBe(8);
80
+ });
81
+
82
+ it('should use nearest-neighbor sampling', () => {
83
+ // 2x2 image: top-left red, top-right green, bottom-left blue, bottom-right white
84
+ const img = makeImageData(2, 2);
85
+ img.data.set([255, 0, 0, 255], 0); // (0,0) red
86
+ img.data.set([0, 255, 0, 255], 4); // (1,0) green
87
+ img.data.set([0, 0, 255, 255], 8); // (0,1) blue
88
+ img.data.set([255, 255, 255, 255], 12); // (1,1) white
89
+
90
+ // Scale to 4x4: each source pixel should fill a 2x2 region
91
+ const buf = importPng(img, 4, 4);
92
+ expect(buf.getPixel(0, 0)).toEqual([255, 0, 0, 255]); // red
93
+ expect(buf.getPixel(1, 0)).toEqual([255, 0, 0, 255]); // still red (nearest)
94
+ expect(buf.getPixel(2, 0)).toEqual([0, 255, 0, 255]); // green
95
+ expect(buf.getPixel(0, 2)).toEqual([0, 0, 255, 255]); // blue
96
+ expect(buf.getPixel(3, 3)).toEqual([255, 255, 255, 255]); // white
97
+ });
98
+
99
+ it('should handle downscaling', () => {
100
+ const img = makeImageData(8, 8, [100, 200, 50, 255]);
101
+ const buf = importPng(img, 2, 2);
102
+ expect(buf.width).toBe(2);
103
+ expect(buf.height).toBe(2);
104
+ // All pixels should sample from the uniform source
105
+ expect(buf.getPixel(0, 0)).toEqual([100, 200, 50, 255]);
106
+ });
107
+ });
108
+
109
+ describe('importSpritesheet', () => {
110
+ it('should split a horizontal strip into frames', () => {
111
+ const strip = makeHorizontalStrip(8, 8, 4);
112
+ const frames = importSpritesheet(strip, 8, 8);
113
+
114
+ expect(frames).toHaveLength(4);
115
+ expect(frames[0]?.width).toBe(8);
116
+ expect(frames[0]?.height).toBe(8);
117
+
118
+ // Each frame should have its distinct gray value
119
+ expect(frames[0]?.getPixel(0, 0)[0]).toBe(50);
120
+ expect(frames[1]?.getPixel(0, 0)[0]).toBe(100);
121
+ expect(frames[2]?.getPixel(0, 0)[0]).toBe(150);
122
+ expect(frames[3]?.getPixel(0, 0)[0]).toBe(200);
123
+ });
124
+
125
+ it('should split a vertical strip into frames', () => {
126
+ const strip = makeVerticalStrip(8, 8, 3);
127
+ const frames = importSpritesheet(strip, 8, 8, { direction: 'vertical' });
128
+
129
+ expect(frames).toHaveLength(3);
130
+ expect(frames[0]?.getPixel(0, 0)[0]).toBe(50);
131
+ expect(frames[1]?.getPixel(0, 0)[0]).toBe(100);
132
+ expect(frames[2]?.getPixel(0, 0)[0]).toBe(150);
133
+ });
134
+
135
+ it('should ignore partial frames at the end', () => {
136
+ // 20px wide strip with 8px frames = 2 full frames + 4px remainder
137
+ const img = makeImageData(20, 8, [128, 128, 128, 255]);
138
+ const frames = importSpritesheet(img, 8, 8);
139
+ expect(frames).toHaveLength(2); // not 3
140
+ });
141
+ });
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Import -- convert external image data into PixelBuffers.
3
+ *
4
+ * Handles single PNG import (scaling to target size) and spritesheet
5
+ * splitting (dividing a strip into equal-sized frames).
6
+ */
7
+
8
+ import { PixelBuffer } from '../canvas/pixel-buffer.js';
9
+
10
+ /**
11
+ * Import a single image as a PixelBuffer, scaled to the target dimensions.
12
+ *
13
+ * Uses nearest-neighbor sampling (appropriate for pixel art).
14
+ * ImageData is the standard web API type from canvas getImageData().
15
+ */
16
+ export function importPng(
17
+ imageData: ImageData,
18
+ targetWidth: number,
19
+ targetHeight: number,
20
+ ): PixelBuffer {
21
+ const result = new PixelBuffer(targetWidth, targetHeight);
22
+ const srcW = imageData.width;
23
+ const srcH = imageData.height;
24
+
25
+ for (let ty = 0; ty < targetHeight; ty++) {
26
+ for (let tx = 0; tx < targetWidth; tx++) {
27
+ // Nearest-neighbor: map target pixel to source pixel
28
+ const sx = Math.floor((tx / targetWidth) * srcW);
29
+ const sy = Math.floor((ty / targetHeight) * srcH);
30
+
31
+ const srcOffset = (sy * srcW + sx) * 4;
32
+ const dstOffset = (ty * targetWidth + tx) * 4;
33
+
34
+ result.data[dstOffset] = imageData.data[srcOffset] ?? 0;
35
+ result.data[dstOffset + 1] = imageData.data[srcOffset + 1] ?? 0;
36
+ result.data[dstOffset + 2] = imageData.data[srcOffset + 2] ?? 0;
37
+ result.data[dstOffset + 3] = imageData.data[srcOffset + 3] ?? 0;
38
+ }
39
+ }
40
+
41
+ return result;
42
+ }
43
+
44
+ /**
45
+ * Import a spritesheet by splitting it into equal-sized frame buffers.
46
+ *
47
+ * @param imageData - the full spritesheet image
48
+ * @param frameWidth - width of each frame in pixels
49
+ * @param frameHeight - height of each frame in pixels
50
+ * @param options.direction - 'horizontal' (left-to-right) or 'vertical' (top-to-bottom)
51
+ * @returns array of PixelBuffers, one per frame
52
+ */
53
+ export function importSpritesheet(
54
+ imageData: ImageData,
55
+ frameWidth: number,
56
+ frameHeight: number,
57
+ options?: { direction?: 'horizontal' | 'vertical' },
58
+ ): PixelBuffer[] {
59
+ const direction = options?.direction ?? 'horizontal';
60
+ const frames: PixelBuffer[] = [];
61
+
62
+ if (direction === 'horizontal') {
63
+ const frameCount = Math.floor(imageData.width / frameWidth);
64
+ for (let i = 0; i < frameCount; i++) {
65
+ const frame = new PixelBuffer(frameWidth, frameHeight);
66
+ const offsetX = i * frameWidth;
67
+
68
+ for (let y = 0; y < frameHeight && y < imageData.height; y++) {
69
+ for (let x = 0; x < frameWidth; x++) {
70
+ const srcX = offsetX + x;
71
+ if (srcX >= imageData.width) continue;
72
+
73
+ const srcOffset = (y * imageData.width + srcX) * 4;
74
+ const dstOffset = (y * frameWidth + x) * 4;
75
+
76
+ frame.data[dstOffset] = imageData.data[srcOffset] ?? 0;
77
+ frame.data[dstOffset + 1] = imageData.data[srcOffset + 1] ?? 0;
78
+ frame.data[dstOffset + 2] = imageData.data[srcOffset + 2] ?? 0;
79
+ frame.data[dstOffset + 3] = imageData.data[srcOffset + 3] ?? 0;
80
+ }
81
+ }
82
+
83
+ frames.push(frame);
84
+ }
85
+ } else {
86
+ // vertical: frames are stacked top-to-bottom
87
+ const frameCount = Math.floor(imageData.height / frameHeight);
88
+ for (let i = 0; i < frameCount; i++) {
89
+ const frame = new PixelBuffer(frameWidth, frameHeight);
90
+ const offsetY = i * frameHeight;
91
+
92
+ for (let y = 0; y < frameHeight; y++) {
93
+ const srcY = offsetY + y;
94
+ if (srcY >= imageData.height) continue;
95
+
96
+ for (let x = 0; x < frameWidth && x < imageData.width; x++) {
97
+ const srcOffset = (srcY * imageData.width + x) * 4;
98
+ const dstOffset = (y * frameWidth + x) * 4;
99
+
100
+ frame.data[dstOffset] = imageData.data[srcOffset] ?? 0;
101
+ frame.data[dstOffset + 1] = imageData.data[srcOffset + 1] ?? 0;
102
+ frame.data[dstOffset + 2] = imageData.data[srcOffset + 2] ?? 0;
103
+ frame.data[dstOffset + 3] = imageData.data[srcOffset + 3] ?? 0;
104
+ }
105
+ }
106
+
107
+ frames.push(frame);
108
+ }
109
+ }
110
+
111
+ return frames;
112
+ }
@@ -0,0 +1,239 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { PixelBuffer } from '../canvas/pixel-buffer.js';
3
+
4
+ /** Shape of parsed metadata JSON produced by the various generators. */
5
+ interface ParsedMeta {
6
+ format?: string;
7
+ frames: Record<string, { frame: { w: number; h: number } }> | Array<{ x: number; y: number }>;
8
+ meta?: { app?: string; frameTags?: unknown[] };
9
+ }
10
+ function parseMeta(json: string): ParsedMeta {
11
+ return JSON.parse(json) as ParsedMeta;
12
+ }
13
+ import type { Frame } from './frame-model.svelte.js';
14
+ import type { Layer } from '../layers/layer-types.js';
15
+ import {
16
+ calculateHorizontalLayout,
17
+ calculateGridLayout,
18
+ generatePixelweaverMeta,
19
+ generateTexturePackerMeta,
20
+ generateAsepriteMeta,
21
+ generateCssMeta,
22
+ exportSpritesheet,
23
+ } from './spritesheet-export.js';
24
+
25
+ // --- Helpers ---
26
+
27
+ function makeFrame(index: number, layerId: string, width: number, height: number): Frame {
28
+ const buf = new PixelBuffer(width, height);
29
+ buf.fill(index * 40, index * 40, index * 40, 255); // distinct grayscale per frame
30
+ return {
31
+ id: `frame-${String(index)}`,
32
+ index,
33
+ durationMs: null,
34
+ pixelData: new Map([[layerId, buf]]),
35
+ };
36
+ }
37
+
38
+ function makeLayerTree(): Layer[] {
39
+ return [
40
+ {
41
+ id: 'layer-1',
42
+ name: 'Background',
43
+ type: 'pixel',
44
+ visible: true,
45
+ opacity: 100,
46
+ blendMode: 'normal',
47
+ locked: false,
48
+ },
49
+ ];
50
+ }
51
+
52
+ // --- Layout tests ---
53
+
54
+ describe('calculateHorizontalLayout', () => {
55
+ it('should arrange frames in a row', () => {
56
+ const layout = calculateHorizontalLayout(3, 16, 16, 0);
57
+ expect(layout.width).toBe(48);
58
+ expect(layout.height).toBe(16);
59
+ expect(layout.positions).toEqual([
60
+ { x: 0, y: 0 },
61
+ { x: 16, y: 0 },
62
+ { x: 32, y: 0 },
63
+ ]);
64
+ });
65
+
66
+ it('should include padding between frames', () => {
67
+ const layout = calculateHorizontalLayout(3, 16, 16, 2);
68
+ // Width: 3 * 16 + 2 * 2 = 52
69
+ expect(layout.width).toBe(52);
70
+ expect(layout.positions[1]?.x).toBe(18); // 16 + 2 padding
71
+ expect(layout.positions[2]?.x).toBe(36); // 16 + 2 + 16 + 2
72
+ });
73
+
74
+ it('should handle a single frame', () => {
75
+ const layout = calculateHorizontalLayout(1, 32, 32, 4);
76
+ expect(layout.width).toBe(32);
77
+ expect(layout.height).toBe(32);
78
+ expect(layout.positions).toEqual([{ x: 0, y: 0 }]);
79
+ });
80
+ });
81
+
82
+ describe('calculateGridLayout', () => {
83
+ it('should arrange frames in a grid', () => {
84
+ const layout = calculateGridLayout(4, 16, 16, 2, 0);
85
+ // 2 columns, 2 rows
86
+ expect(layout.width).toBe(32);
87
+ expect(layout.height).toBe(32);
88
+ expect(layout.positions).toEqual([
89
+ { x: 0, y: 0 },
90
+ { x: 16, y: 0 },
91
+ { x: 0, y: 16 },
92
+ { x: 16, y: 16 },
93
+ ]);
94
+ });
95
+
96
+ it('should handle non-even frame counts', () => {
97
+ const layout = calculateGridLayout(5, 8, 8, 3, 0);
98
+ // 3 columns, 2 rows (5 frames, last row has 2)
99
+ expect(layout.width).toBe(24);
100
+ expect(layout.height).toBe(16);
101
+ expect(layout.positions).toHaveLength(5);
102
+ });
103
+
104
+ it('should include padding', () => {
105
+ const layout = calculateGridLayout(4, 16, 16, 2, 4);
106
+ // Width: 2 * 16 + 1 * 4 = 36
107
+ // Height: 2 * 16 + 1 * 4 = 36
108
+ expect(layout.width).toBe(36);
109
+ expect(layout.height).toBe(36);
110
+ expect(layout.positions[1]).toEqual({ x: 20, y: 0 });
111
+ expect(layout.positions[2]).toEqual({ x: 0, y: 20 });
112
+ });
113
+ });
114
+
115
+ // --- Metadata tests ---
116
+
117
+ describe('generatePixelweaverMeta', () => {
118
+ it('should produce valid JSON with frame positions', () => {
119
+ const layout = calculateHorizontalLayout(2, 16, 16, 0);
120
+ const frames = [makeFrame(0, 'layer-1', 16, 16), makeFrame(1, 'layer-1', 16, 16)];
121
+ const meta = generatePixelweaverMeta(layout, frames, {
122
+ layout: 'horizontal',
123
+ padding: 0,
124
+ metadataFormat: 'pixelweaver',
125
+ });
126
+
127
+ const parsed = parseMeta(meta);
128
+ expect(parsed.format).toBe('pixelweaver');
129
+ const framesArr = parsed.frames as Array<{ x: number; y: number }>;
130
+ expect(framesArr).toHaveLength(2);
131
+ expect(framesArr[0]?.x).toBe(0);
132
+ expect(framesArr[1]?.x).toBe(16);
133
+ });
134
+ });
135
+
136
+ describe('generateTexturePackerMeta', () => {
137
+ it('should produce TexturePacker JSON hash format', () => {
138
+ const layout = calculateHorizontalLayout(2, 16, 16, 0);
139
+ const frames = [makeFrame(0, 'layer-1', 16, 16), makeFrame(1, 'layer-1', 16, 16)];
140
+ const meta = generateTexturePackerMeta(layout, frames, 16, 16);
141
+
142
+ const parsed = parseMeta(meta);
143
+ expect(parsed.frames).toBeDefined();
144
+ const framesMap = parsed.frames as Record<string, { frame: { w: number; h: number } }>;
145
+ expect(framesMap['frame_0']).toBeDefined();
146
+ expect(framesMap['frame_0']?.frame.w).toBe(16);
147
+ expect(parsed.meta?.app).toBe('PixelWeaver');
148
+ });
149
+ });
150
+
151
+ describe('generateAsepriteMeta', () => {
152
+ it('should produce Aseprite-compatible JSON', () => {
153
+ const layout = calculateHorizontalLayout(1, 16, 16, 0);
154
+ const frames = [makeFrame(0, 'layer-1', 16, 16)];
155
+ const meta = generateAsepriteMeta(layout, frames, 16, 16);
156
+
157
+ const parsed = parseMeta(meta);
158
+ expect(parsed.meta?.app).toContain('aseprite');
159
+ expect(parsed.meta?.frameTags).toEqual([]);
160
+ expect(Object.keys(parsed.frames)).toHaveLength(1);
161
+ });
162
+ });
163
+
164
+ describe('generateCssMeta', () => {
165
+ it('should produce CSS with sprite class and keyframes', () => {
166
+ const layout = calculateHorizontalLayout(2, 16, 16, 0);
167
+ const frames = [makeFrame(0, 'layer-1', 16, 16), makeFrame(1, 'layer-1', 16, 16)];
168
+ const css = generateCssMeta(layout, frames, 16, 16);
169
+
170
+ expect(css).toContain('.sprite {');
171
+ expect(css).toContain('.sprite-frame-0');
172
+ expect(css).toContain('.sprite-frame-1');
173
+ expect(css).toContain('@keyframes sprite-animation');
174
+ });
175
+ });
176
+
177
+ // --- Export integration test ---
178
+
179
+ describe('exportSpritesheet', () => {
180
+ it('should produce a spritesheet image and metadata', () => {
181
+ const frames = [
182
+ makeFrame(0, 'layer-1', 8, 8),
183
+ makeFrame(1, 'layer-1', 8, 8),
184
+ makeFrame(2, 'layer-1', 8, 8),
185
+ ];
186
+ const layerTree = makeLayerTree();
187
+
188
+ const result = exportSpritesheet(frames, layerTree, {
189
+ layout: 'horizontal',
190
+ padding: 0,
191
+ metadataFormat: 'pixelweaver',
192
+ });
193
+
194
+ expect(result.image.width).toBe(24); // 3 * 8
195
+ expect(result.image.height).toBe(8);
196
+ expect(result.metadata).toBeTruthy();
197
+ const parsed = parseMeta(result.metadata);
198
+ expect(parsed.frames).toHaveLength(3);
199
+ });
200
+
201
+ it('should use grid layout with specified columns', () => {
202
+ const frames = [
203
+ makeFrame(0, 'layer-1', 8, 8),
204
+ makeFrame(1, 'layer-1', 8, 8),
205
+ makeFrame(2, 'layer-1', 8, 8),
206
+ makeFrame(3, 'layer-1', 8, 8),
207
+ ];
208
+ const layerTree = makeLayerTree();
209
+
210
+ const result = exportSpritesheet(frames, layerTree, {
211
+ layout: 'grid',
212
+ columns: 2,
213
+ padding: 0,
214
+ metadataFormat: 'pixelweaver',
215
+ });
216
+
217
+ expect(result.image.width).toBe(16); // 2 * 8
218
+ expect(result.image.height).toBe(16); // 2 * 8
219
+ });
220
+
221
+ it('should apply padding in the spritesheet', () => {
222
+ const frames = [
223
+ makeFrame(0, 'layer-1', 8, 8),
224
+ makeFrame(1, 'layer-1', 8, 8),
225
+ ];
226
+ const layerTree = makeLayerTree();
227
+
228
+ const result = exportSpritesheet(frames, layerTree, {
229
+ layout: 'horizontal',
230
+ padding: 2,
231
+ metadataFormat: 'pixelweaver',
232
+ });
233
+
234
+ expect(result.image.width).toBe(18); // 8 + 2 + 8
235
+ // The padding area (pixels 8-9) should be transparent
236
+ expect(result.image.getPixel(8, 0)[3]).toBe(0);
237
+ expect(result.image.getPixel(9, 0)[3]).toBe(0);
238
+ });
239
+ });