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,275 @@
1
+ /**
2
+ * File Commands -- New Project, Open, Save, Export.
3
+ *
4
+ * Most execute() functions delegate to dialogState to open the relevant
5
+ * dialog; the heavy lifting lives in those dialog components.
6
+ */
7
+
8
+ import type { PluginModule } from '../core/plugin-loader.js';
9
+ import { dialogState } from './dialog-state.svelte.js';
10
+ import { openFile } from './file-open.js';
11
+ import { gatherSnapshot, applySnapshot } from '../save/project-snapshot.js';
12
+ import { saveAs, saveToPath, openProject } from '../save/storage.js';
13
+ import { saveState } from '../save/save-state.svelte.js';
14
+ import { addRecentProject } from '../save/recent-projects.js';
15
+ import { markSaveCheckpoint } from '../core/dispatcher.js';
16
+ import { composite } from '../layers/compositor.js';
17
+ import { getLayers } from '../layers/layer-tree.svelte.js';
18
+ import { getCurrentFrame } from '../animation/frame-model.svelte.js';
19
+ import { canvasState } from '../canvas/canvas-state.svelte.js';
20
+ import { setRenderBuffer } from '../canvas/render-state.svelte.js';
21
+ import FilePlus from '~icons/lucide/file-plus';
22
+ import FolderOpen from '~icons/lucide/folder-open';
23
+ import Save from '~icons/lucide/save';
24
+ import SaveAll from '~icons/lucide/save-all';
25
+ import Download from '~icons/lucide/download';
26
+ import FileArchive from '~icons/lucide/file-archive';
27
+
28
+ /**
29
+ * Flatten all visible layers into the render buffer after loading a project.
30
+ * Mirrors the recomposite logic in canvas-init-plugin and file-open.
31
+ */
32
+ function recompositeAfterLoad(): void {
33
+ const currentFrame = getCurrentFrame();
34
+ const layers = getLayers();
35
+ const result = composite(
36
+ layers,
37
+ currentFrame.pixelData,
38
+ canvasState.canvasWidth,
39
+ canvasState.canvasHeight,
40
+ );
41
+ setRenderBuffer(result);
42
+ }
43
+
44
+ export const fileCommandsPlugin: PluginModule = {
45
+ name: 'ui/file-commands',
46
+ version: '1.0.0',
47
+ dependencies: [],
48
+ register(api) {
49
+ // New Project command (actual logic handled by dialog, dispatched via event)
50
+ api.addCommand('new_project_dialog', {
51
+ tier: 'project',
52
+ execute() { dialogState.openNewProject(); },
53
+ undo() { /* Not undoable */ },
54
+ describe() { return 'Opened new project dialog'; },
55
+ label: 'New Project',
56
+ category: 'File',
57
+ icon: FilePlus,
58
+ });
59
+ api.addMenuItem('menu:file:new', {
60
+ commandId: 'new_project_dialog',
61
+ menuPath: 'file',
62
+ group: 'new',
63
+ order: 10,
64
+ label: 'New Project',
65
+ });
66
+
67
+ // Open Project (.pwv file)
68
+ api.addCommand('open_project', {
69
+ tier: 'project',
70
+ async execute() {
71
+ try {
72
+ const result = await openProject();
73
+ if (!result) return; // user cancelled
74
+
75
+ applySnapshot(result.snapshot);
76
+ saveState.savePath = result.path;
77
+ saveState.saveFormat = result.format;
78
+ saveState.projectName = result.snapshot.name || 'Untitled';
79
+
80
+ recompositeAfterLoad();
81
+
82
+ const snap = result.snapshot;
83
+ addRecentProject({
84
+ name: snap.name || 'Untitled',
85
+ path: result.path,
86
+ timestamp: Date.now(),
87
+ dimensions: `${(snap.canvas as { width: number; height: number }).width}x${(snap.canvas as { width: number; height: number }).height}`,
88
+ });
89
+
90
+ api.notify({
91
+ id: 'project-opened',
92
+ message: `Opened "${result.snapshot.name || result.path}"`,
93
+ type: 'success',
94
+ autoDismissMs: 3000,
95
+ });
96
+ } catch (err) {
97
+ console.error('Failed to open project:', err);
98
+ api.notify({
99
+ id: 'project-open-error',
100
+ message: `Failed to open project: ${err instanceof Error ? err.message : String(err)}`,
101
+ type: 'warning',
102
+ autoDismissMs: 5000,
103
+ });
104
+ }
105
+ },
106
+ undo() {},
107
+ describe() { return 'Opened project'; },
108
+ label: 'Open Project...',
109
+ category: 'File',
110
+ icon: FileArchive,
111
+ });
112
+ api.addMenuItem('menu:file:open-project', {
113
+ commandId: 'open_project',
114
+ menuPath: 'file',
115
+ group: 'open',
116
+ order: 5,
117
+ label: 'Open Project...',
118
+ });
119
+
120
+ // Open file (image import into current project)
121
+ api.addCommand('open_file_dialog', {
122
+ tier: 'project',
123
+ execute() { void openFile(); },
124
+ undo() {},
125
+ describe() { return 'Opened file dialog'; },
126
+ label: 'Open File...',
127
+ category: 'File',
128
+ defaultShortcut: 'Ctrl+O',
129
+ icon: FolderOpen,
130
+ });
131
+ api.addMenuItem('menu:file:open', {
132
+ commandId: 'open_file_dialog',
133
+ menuPath: 'file',
134
+ group: 'open',
135
+ order: 10,
136
+ label: 'Open File...',
137
+ });
138
+
139
+ // Save (re-save to known path, or Save As if no path yet)
140
+ api.addCommand('save_project', {
141
+ tier: 'project',
142
+ async execute() {
143
+ try {
144
+ const snapshot = gatherSnapshot(saveState.projectName);
145
+
146
+ if (saveState.hasSavePath && saveState.savePath && saveState.saveFormat) {
147
+ // Re-save silently to existing location
148
+ await saveToPath(snapshot, saveState.savePath, saveState.saveFormat);
149
+ addRecentProject({
150
+ name: saveState.projectName,
151
+ path: saveState.savePath,
152
+ timestamp: Date.now(),
153
+ dimensions: `${canvasState.canvasWidth}x${canvasState.canvasHeight}`,
154
+ });
155
+ markSaveCheckpoint();
156
+ api.notify({
157
+ id: 'project-saved',
158
+ message: `Saved "${saveState.projectName}"`,
159
+ type: 'success',
160
+ autoDismissMs: 2000,
161
+ });
162
+ } else {
163
+ // First save -- show Save As dialog
164
+ const result = await saveAs(snapshot);
165
+ if (!result) return; // user cancelled
166
+
167
+ saveState.savePath = result.path;
168
+ saveState.saveFormat = result.format;
169
+ addRecentProject({
170
+ name: saveState.projectName,
171
+ path: result.path,
172
+ timestamp: Date.now(),
173
+ dimensions: `${canvasState.canvasWidth}x${canvasState.canvasHeight}`,
174
+ });
175
+ markSaveCheckpoint();
176
+ api.notify({
177
+ id: 'project-saved',
178
+ message: `Saved "${saveState.projectName}"`,
179
+ type: 'success',
180
+ autoDismissMs: 2000,
181
+ });
182
+ }
183
+ } catch (err) {
184
+ console.error('Failed to save project:', err);
185
+ api.notify({
186
+ id: 'project-save-error',
187
+ message: `Failed to save: ${err instanceof Error ? err.message : String(err)}`,
188
+ type: 'warning',
189
+ autoDismissMs: 5000,
190
+ });
191
+ }
192
+ },
193
+ undo() {},
194
+ describe() { return 'Saved project'; },
195
+ label: 'Save',
196
+ category: 'File',
197
+ defaultShortcut: 'Ctrl+S',
198
+ icon: Save,
199
+ });
200
+ api.addMenuItem('menu:file:save', {
201
+ commandId: 'save_project',
202
+ menuPath: 'file',
203
+ group: 'save',
204
+ order: 10,
205
+ label: 'Save',
206
+ });
207
+
208
+ // Save As (always show dialog)
209
+ api.addCommand('save_project_as', {
210
+ tier: 'project',
211
+ async execute() {
212
+ try {
213
+ const snapshot = gatherSnapshot(saveState.projectName);
214
+ const result = await saveAs(snapshot);
215
+ if (!result) return; // user cancelled
216
+
217
+ saveState.savePath = result.path;
218
+ saveState.saveFormat = result.format;
219
+ addRecentProject({
220
+ name: saveState.projectName,
221
+ path: result.path,
222
+ timestamp: Date.now(),
223
+ dimensions: `${canvasState.canvasWidth}x${canvasState.canvasHeight}`,
224
+ });
225
+ markSaveCheckpoint();
226
+ api.notify({
227
+ id: 'project-saved',
228
+ message: `Saved "${saveState.projectName}" to ${result.path}`,
229
+ type: 'success',
230
+ autoDismissMs: 3000,
231
+ });
232
+ } catch (err) {
233
+ console.error('Failed to save project:', err);
234
+ api.notify({
235
+ id: 'project-save-error',
236
+ message: `Failed to save: ${err instanceof Error ? err.message : String(err)}`,
237
+ type: 'warning',
238
+ autoDismissMs: 5000,
239
+ });
240
+ }
241
+ },
242
+ undo() {},
243
+ describe() { return 'Saved project as...'; },
244
+ label: 'Save As...',
245
+ category: 'File',
246
+ defaultShortcut: 'Ctrl+Shift+S',
247
+ icon: SaveAll,
248
+ });
249
+ api.addMenuItem('menu:file:save-as', {
250
+ commandId: 'save_project_as',
251
+ menuPath: 'file',
252
+ group: 'save',
253
+ order: 20,
254
+ label: 'Save As...',
255
+ });
256
+
257
+ // Export dialog
258
+ api.addCommand('export_dialog', {
259
+ tier: 'project',
260
+ execute() { dialogState.openExport(); },
261
+ undo() {},
262
+ describe() { return 'Opened export dialog'; },
263
+ label: 'Export...',
264
+ category: 'File',
265
+ icon: Download,
266
+ });
267
+ api.addMenuItem('menu:file:export-dialog', {
268
+ commandId: 'export_dialog',
269
+ menuPath: 'file/export',
270
+ group: 'export',
271
+ order: 10,
272
+ label: 'Export...',
273
+ });
274
+ },
275
+ };
@@ -0,0 +1,99 @@
1
+ /**
2
+ * File Open handler -- opens a browser file picker filtered to supported
3
+ * import formats, then routes the selected file to the matching importer plugin.
4
+ *
5
+ * After a successful import, triggers a recomposite so the canvas displays
6
+ * the imported content.
7
+ */
8
+
9
+ import { importerRegistry } from '../core/registries.svelte.js';
10
+ import { composite } from '../layers/compositor.js';
11
+ import { getLayers } from '../layers/layer-tree.svelte.js';
12
+ import { getCurrentFrame } from '../animation/frame-model.svelte.js';
13
+ import { canvasState } from '../canvas/canvas-state.svelte.js';
14
+ import { setRenderBuffer } from '../canvas/render-state.svelte.js';
15
+
16
+ /**
17
+ * Opens a file picker dialog filtered to supported import formats,
18
+ * then routes the selected file to the matching importer plugin.
19
+ */
20
+ export async function openFile(): Promise<void> {
21
+ // 1. Build accept string from all registered importers
22
+ const importers = importerRegistry.getAll();
23
+ if (importers.size === 0) {
24
+ console.warn('No importers registered');
25
+ return;
26
+ }
27
+
28
+ // Collect all extensions from every registered importer
29
+ const allExtensions: string[] = [];
30
+ for (const importer of importers.values()) {
31
+ allExtensions.push(...importer.extensions);
32
+ }
33
+
34
+ // Build the accept attribute: ".ase,.aseprite,.piskel,.png"
35
+ const accept = allExtensions.map((ext) => `.${ext}`).join(',');
36
+
37
+ // 2. Create a hidden file input and trigger the picker
38
+ const input = document.createElement('input');
39
+ input.type = 'file';
40
+ input.accept = accept;
41
+
42
+ const file = await new Promise<File | null>((resolve) => {
43
+ input.onchange = () => { resolve(input.files?.[0] ?? null); };
44
+ // Fallback: if the user cancels, the change event may not fire in all browsers.
45
+ // Listen for window re-focus and resolve null after a short delay.
46
+ const onFocus = () => {
47
+ setTimeout(() => {
48
+ if (!input.files?.length) resolve(null);
49
+ window.removeEventListener('focus', onFocus);
50
+ }, 300);
51
+ };
52
+ window.addEventListener('focus', onFocus);
53
+ input.click();
54
+ });
55
+
56
+ if (!file) return;
57
+
58
+ // 3. Match file extension to a registered importer
59
+ const ext = file.name.split('.').pop()?.toLowerCase() ?? '';
60
+ let matchedImporter = null;
61
+ for (const importer of importers.values()) {
62
+ if (importer.extensions.includes(ext)) {
63
+ matchedImporter = importer;
64
+ break;
65
+ }
66
+ }
67
+
68
+ if (!matchedImporter) {
69
+ console.warn(`No importer found for .${ext} files`);
70
+ return;
71
+ }
72
+
73
+ // 4. Run the importer
74
+ try {
75
+ await matchedImporter.import(file, {});
76
+ } catch (err) {
77
+ console.error(`Import failed for ${file.name}:`, err);
78
+ return;
79
+ }
80
+
81
+ // 5. Recomposite so the canvas shows the imported data
82
+ recompositeAfterImport();
83
+ }
84
+
85
+ /**
86
+ * Flatten all visible layers into the render buffer.
87
+ * Mirrors the recomposite logic in canvas-init-plugin.
88
+ */
89
+ function recompositeAfterImport(): void {
90
+ const currentFrame = getCurrentFrame();
91
+ const layers = getLayers();
92
+ const result = composite(
93
+ layers,
94
+ currentFrame.pixelData,
95
+ canvasState.canvasWidth,
96
+ canvasState.canvasHeight,
97
+ );
98
+ setRenderBuffer(result);
99
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Help Commands -- About, Keyboard Shortcuts, Report a Bug.
3
+ *
4
+ * The About command mounts AboutDialog.svelte imperatively into a fresh
5
+ * div on document.body; the dialog unmounts itself via its onClose prop.
6
+ */
7
+
8
+ import type { PluginModule } from '../core/plugin-loader.js';
9
+ import { mount, unmount } from 'svelte';
10
+ import AboutDialog from './AboutDialog.svelte';
11
+ import { notificationState } from './notifications/notification-state.svelte.js';
12
+ import Info from '~icons/lucide/info';
13
+ import Keyboard from '~icons/lucide/keyboard';
14
+ import Bug from '~icons/lucide/bug';
15
+
16
+ export const helpCommandsPlugin: PluginModule = {
17
+ name: 'ui/help-commands',
18
+ version: '1.0.0',
19
+ dependencies: [],
20
+ register(api) {
21
+ api.addCommand('about', {
22
+ tier: 'project',
23
+ execute() {
24
+ const target = document.createElement('div');
25
+ document.body.appendChild(target);
26
+ const instance = mount(AboutDialog, {
27
+ target,
28
+ props: {
29
+ onClose: () => {
30
+ void unmount(instance);
31
+ target.remove();
32
+ },
33
+ },
34
+ });
35
+ },
36
+ undo() {},
37
+ describe() { return 'Opened About dialog'; },
38
+ label: 'About PixelWeaver',
39
+ category: 'Help',
40
+ icon: Info,
41
+ });
42
+ api.addMenuItem('menu:help:about', {
43
+ commandId: 'about',
44
+ menuPath: 'help',
45
+ group: 'info',
46
+ order: 10,
47
+ label: 'About PixelWeaver',
48
+ });
49
+
50
+ api.addCommand('keyboard_shortcuts', {
51
+ tier: 'project',
52
+ execute() {
53
+ notificationState.push({
54
+ id: 'shortcuts',
55
+ message: 'Ctrl+Z Undo | Ctrl+Y Redo | Ctrl+A Select All | Space Play | B Pencil | E Eraser | G Bucket | = Zoom In | - Zoom Out',
56
+ type: 'info',
57
+ });
58
+ },
59
+ undo() {},
60
+ describe() { return 'Opened keyboard shortcuts'; },
61
+ label: 'Keyboard Shortcuts',
62
+ category: 'Help',
63
+ icon: Keyboard,
64
+ });
65
+ api.addMenuItem('menu:help:keyboard-shortcuts', {
66
+ commandId: 'keyboard_shortcuts',
67
+ menuPath: 'help',
68
+ group: 'info',
69
+ order: 20,
70
+ label: 'Keyboard Shortcuts',
71
+ });
72
+
73
+ api.addCommand('report_bug', {
74
+ tier: 'project',
75
+ execute() {
76
+ // Placeholder URL -- update when the real repository is published
77
+ window.open('https://github.com/user/pixelweaver/issues', '_blank');
78
+ },
79
+ undo() {},
80
+ describe() { return 'Opened bug report'; },
81
+ label: 'Report a Bug',
82
+ category: 'Help',
83
+ icon: Bug,
84
+ });
85
+ api.addMenuItem('menu:help:report-bug', {
86
+ commandId: 'report_bug',
87
+ menuPath: 'help',
88
+ group: 'info',
89
+ order: 30,
90
+ label: 'Report a Bug',
91
+ });
92
+ },
93
+ };
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Image Commands -- Resize Canvas, Crop to Selection, Flatten Image,
3
+ * Canvas Size (info).
4
+ *
5
+ * Resize opens ResizeDialog via dialogState.openResizeCanvas(). Crop and
6
+ * Flatten have partial implementations -- actual pixel remapping /
7
+ * compositing is still a TODO in layer-tree.
8
+ */
9
+
10
+ import type { PluginModule } from '../core/plugin-loader.js';
11
+ import { canvasState } from '../canvas/canvas-state.svelte.js';
12
+ import { dialogState } from './dialog-state.svelte.js';
13
+ import * as layerTree from '../layers/layer-tree.svelte.js';
14
+ import type { LayerTreeState } from '../layers/layer-types.js';
15
+ import { notificationState } from './notifications/notification-state.svelte.js';
16
+ import Move from '~icons/lucide/move';
17
+ import Crop from '~icons/lucide/crop';
18
+ import Ruler from '~icons/lucide/ruler';
19
+ import Layers from '~icons/lucide/layers';
20
+
21
+ // Snapshot only the canvas dimensions. Pixel buffers are stored per-layer
22
+ // per-frame and are NOT resized when canvasState.canvasWidth/Height change,
23
+ // so restoring the dimensions is sufficient to revert these ops.
24
+ interface CanvasSizeSnapshot {
25
+ width: number;
26
+ height: number;
27
+ }
28
+
29
+ // For flatten_image we snapshot the entire serialized layer tree. Since
30
+ // deepSnapshotLayer preserves layer IDs, frame pixel data (keyed by layer id)
31
+ // remains correctly associated after deserialize.
32
+ interface LayerTreeSnapshot {
33
+ tree: LayerTreeState;
34
+ }
35
+
36
+ export const imageCommandsPlugin: PluginModule = {
37
+ name: 'ui/image-commands',
38
+ version: '1.0.0',
39
+ dependencies: [],
40
+ register(api) {
41
+ // Capture once: stable object whose methods read live selection state.
42
+ const selection = api.getSelection();
43
+
44
+ api.addCommand('resize_canvas', {
45
+ tier: 'project',
46
+ execute() { dialogState.openResizeCanvas(); },
47
+ undo() { /* Not undoable -- resize is handled inside the dialog */ },
48
+ describe() { return 'Opened resize canvas dialog'; },
49
+ label: 'Resize Canvas...',
50
+ category: 'Image',
51
+ icon: Move,
52
+ });
53
+ api.addMenuItem('menu:image:resize-canvas', {
54
+ commandId: 'resize_canvas',
55
+ menuPath: 'image',
56
+ group: 'size',
57
+ order: 10,
58
+ label: 'Resize Canvas...',
59
+ });
60
+
61
+ api.addCommand('crop_to_selection', {
62
+ tier: 'project',
63
+ undoable: true,
64
+ execute(): CanvasSizeSnapshot | undefined {
65
+ if (!selection.hasSelection()) {
66
+ console.warn('crop_to_selection: no selection exists -- select an area first');
67
+ return;
68
+ }
69
+ // Capture pre-crop dimensions so undo can restore them. Pixel data
70
+ // is not remapped by this operation (still TODO in compositor), so
71
+ // restoring only width/height is sufficient to revert.
72
+ const snapshot: CanvasSizeSnapshot = {
73
+ width: canvasState.canvasWidth,
74
+ height: canvasState.canvasHeight,
75
+ };
76
+ // Compute bounding box of the selection
77
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
78
+ for (const key of selection.getSelectedPixels()) {
79
+ const [x, y] = key.split(',').map(Number) as [number, number];
80
+ if (x < minX) minX = x;
81
+ if (y < minY) minY = y;
82
+ if (x > maxX) maxX = x;
83
+ if (y > maxY) maxY = y;
84
+ }
85
+ const w = maxX - minX + 1;
86
+ const h = maxY - minY + 1;
87
+ // Resize canvas to selection bounds (pixel data remapping requires compositor support)
88
+ canvasState.canvasWidth = w;
89
+ canvasState.canvasHeight = h;
90
+ console.warn('crop_to_selection: canvas resized to selection bounds, but pixel data remapping is not yet implemented');
91
+ return snapshot;
92
+ },
93
+ undo(_params, _ctx, snapshot) {
94
+ const snap = snapshot as CanvasSizeSnapshot | undefined;
95
+ if (!snap) return;
96
+ canvasState.canvasWidth = snap.width;
97
+ canvasState.canvasHeight = snap.height;
98
+ },
99
+ describe() { return 'Cropped canvas to selection'; },
100
+ label: 'Crop to Selection',
101
+ category: 'Image',
102
+ icon: Crop,
103
+ });
104
+ api.addMenuItem('menu:image:crop-to-selection', {
105
+ commandId: 'crop_to_selection',
106
+ menuPath: 'image',
107
+ group: 'size',
108
+ order: 20,
109
+ label: 'Crop to Selection',
110
+ });
111
+
112
+ api.addCommand('flatten_image', {
113
+ tier: 'project',
114
+ undoable: true,
115
+ execute(): LayerTreeSnapshot | undefined {
116
+ // Merge all pixel layers into the bottom-most one by repeatedly merging the top layer down.
117
+ // Note: actual pixel compositing is still placeholder in layer-tree's mergeDown.
118
+ const pixelLayers = layerTree.getAllPixelLayers();
119
+ if (pixelLayers.length <= 1) return;
120
+ // Snapshot the pre-flatten tree. deepSnapshotLayer preserves layer
121
+ // IDs so frame pixel data (keyed by layer id) stays linked after
122
+ // restore. Must be taken BEFORE mutations.
123
+ const snapshot: LayerTreeSnapshot = { tree: layerTree.serialize() };
124
+ // Merge from top to bottom: repeatedly merge the last pixel layer down
125
+ for (let i = pixelLayers.length - 1; i > 0; i--) {
126
+ const layer = pixelLayers[i];
127
+ if (!layer) continue;
128
+ try {
129
+ layerTree.mergeDown(layer.id);
130
+ } catch {
131
+ // Skip layers that can't merge (e.g. no pixel layer below)
132
+ break;
133
+ }
134
+ }
135
+ return snapshot;
136
+ },
137
+ undo(_params, _ctx, snapshot) {
138
+ const snap = snapshot as LayerTreeSnapshot | undefined;
139
+ if (!snap) return;
140
+ layerTree.deserialize(snap.tree);
141
+ },
142
+ describe() { return 'Flattened image'; },
143
+ label: 'Flatten Image',
144
+ category: 'Image',
145
+ icon: Layers,
146
+ });
147
+ api.addMenuItem('menu:image:flatten', {
148
+ commandId: 'flatten_image',
149
+ menuPath: 'image',
150
+ group: 'flatten',
151
+ order: 30,
152
+ label: 'Flatten Image',
153
+ });
154
+
155
+ api.addCommand('canvas_size', {
156
+ tier: 'project',
157
+ execute() {
158
+ const w = canvasState.canvasWidth;
159
+ const h = canvasState.canvasHeight;
160
+ notificationState.push({
161
+ id: 'canvas-size',
162
+ message: `Canvas: ${String(w)} x ${String(h)}`,
163
+ type: 'info',
164
+ autoDismissMs: 3000,
165
+ });
166
+ },
167
+ undo() {},
168
+ describe() { return 'Opened canvas size dialog'; },
169
+ label: 'Canvas Size...',
170
+ category: 'Image',
171
+ icon: Ruler,
172
+ });
173
+ api.addMenuItem('menu:image:canvas-size', {
174
+ commandId: 'canvas_size',
175
+ menuPath: 'image',
176
+ group: 'size',
177
+ order: 15,
178
+ label: 'Canvas Size...',
179
+ });
180
+ },
181
+ };