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,361 @@
1
+ /**
2
+ * Command Dispatcher -- the central routing system of PixelWeaver.
3
+ *
4
+ * When a command is dispatched:
5
+ * 1. Validate: ensure the command type is registered
6
+ * 2. Execute: call the command definition's execute()
7
+ * 3. Record: push onto the undo stack (with tier + parent pointer) and notify onExecute subscribers
8
+ * 4. Log: call describe() and append to the semantic log
9
+ * 5. Serialize: notify onSpecExport subscribers
10
+ *
11
+ * Two-tier undo: each command is tagged 'frame' (drawing ops) or 'project'
12
+ * (structural ops). Ctrl+Z always undoes the most recent command regardless
13
+ * of tier. The tier tag exists for future UI grouping and tier-filtered
14
+ * undo operations.
15
+ *
16
+ * Module-level singleton -- one dispatcher per app instance.
17
+ */
18
+
19
+ import type {
20
+ Command,
21
+ CommandContext,
22
+ CommandDefinition,
23
+ SemanticLogEntry,
24
+ UndoEntry,
25
+ } from './commands.js';
26
+ import type { CommandType, ParamsOf } from './command-params.js';
27
+ import { commandRegistry } from './registries.svelte.js';
28
+
29
+ // --- Sync hook ---
30
+ // Called after every successful command dispatch so the sync layer can
31
+ // forward commands to the collaboration server without tight coupling.
32
+
33
+ type DispatchHook = (command: Command) => void;
34
+ let dispatchHook: DispatchHook | null = null;
35
+
36
+ /** Register (or clear) the post-dispatch hook used by the sync layer. */
37
+ export function setDispatchHook(hook: DispatchHook | null): void {
38
+ dispatchHook = hook;
39
+ }
40
+
41
+ /**
42
+ * Notify subscribers using a defensive copy so callbacks that add/remove
43
+ * other subscribers during iteration don't corrupt the loop.
44
+ */
45
+ function notify<T>(callbacks: ReadonlyArray<(arg: T) => void>, arg: T): void {
46
+ for (const cb of [...callbacks]) cb(arg);
47
+ }
48
+
49
+ // --- Consumer hook types ---
50
+
51
+ type ExecuteCallback = (command: Command) => void;
52
+ type UndoCallback = (command: Command) => void;
53
+ type LogCallback = (entry: SemanticLogEntry) => void;
54
+ type SpecExportCallback = (command: Command) => void;
55
+
56
+ // --- Dispatcher state ---
57
+
58
+ /** The shared command context; later phases will populate with canvas, layers, etc. */
59
+ let context: CommandContext = {};
60
+
61
+ /** Undo stack with tier and parent pointers (most recent entry last) */
62
+ const undoStack: UndoEntry[] = [];
63
+
64
+ /** Redo stack with tier and parent pointers (most recently undone entry last) */
65
+ const redoStack: UndoEntry[] = [];
66
+
67
+ /** Semantic log of all executed commands */
68
+ const semanticLog: SemanticLogEntry[] = [];
69
+
70
+ /** Maximum number of entries in the undo stack; oldest entries are dropped when exceeded */
71
+ let maxUndoSize = 500;
72
+
73
+ /**
74
+ * Save checkpoint: the ID of the undo stack's top entry at the time of the
75
+ * last save. Comparing the current top-of-stack ID against this value tells
76
+ * us whether the project has unsaved changes -- correctly handling undo past
77
+ * the save point, redo back to it, and new commands after undo.
78
+ *
79
+ * null means "saved at empty state" (fresh/new project).
80
+ */
81
+ let saveCheckpointId: string | null = null;
82
+
83
+ // --- Consumer hook subscriptions ---
84
+
85
+ const executeCallbacks: ExecuteCallback[] = [];
86
+ const undoCallbacks: UndoCallback[] = [];
87
+ const logCallbacks: LogCallback[] = [];
88
+ const specExportCallbacks: SpecExportCallback[] = [];
89
+
90
+ // --- Internal helpers ---
91
+
92
+ /** Resolve the tier for a command from its definition, defaulting to 'frame'. */
93
+ function resolveTier(commandType: string): UndoEntry['tier'] {
94
+ const def = commandRegistry.get(commandType);
95
+ return def?.tier ?? 'frame';
96
+ }
97
+
98
+ /** Get the ID of the topmost undo entry, or null if the stack is empty. */
99
+ function topUndoId(): string | null {
100
+ return undoStack[undoStack.length - 1]?.id ?? null;
101
+ }
102
+
103
+ // --- Public API ---
104
+
105
+ /** Subscribe to command execution events. Returns an unsubscribe function. */
106
+ export function onExecute(callback: ExecuteCallback): () => void {
107
+ executeCallbacks.push(callback);
108
+ return () => {
109
+ const idx = executeCallbacks.indexOf(callback);
110
+ if (idx !== -1) executeCallbacks.splice(idx, 1);
111
+ };
112
+ }
113
+
114
+ /** Subscribe to command undo events. Returns an unsubscribe function. */
115
+ export function onUndo(callback: UndoCallback): () => void {
116
+ undoCallbacks.push(callback);
117
+ return () => {
118
+ const idx = undoCallbacks.indexOf(callback);
119
+ if (idx !== -1) undoCallbacks.splice(idx, 1);
120
+ };
121
+ }
122
+
123
+ /** Subscribe to semantic log events. Returns an unsubscribe function. */
124
+ export function onLog(callback: LogCallback): () => void {
125
+ logCallbacks.push(callback);
126
+ return () => {
127
+ const idx = logCallbacks.indexOf(callback);
128
+ if (idx !== -1) logCallbacks.splice(idx, 1);
129
+ };
130
+ }
131
+
132
+ /** Subscribe to spec-export events. Returns an unsubscribe function. */
133
+ export function onSpecExport(callback: SpecExportCallback): () => void {
134
+ specExportCallbacks.push(callback);
135
+ return () => {
136
+ const idx = specExportCallbacks.indexOf(callback);
137
+ if (idx !== -1) specExportCallbacks.splice(idx, 1);
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Dispatch a command: validate, execute, record, log, and notify consumers.
143
+ * Throws if the command type is not registered.
144
+ */
145
+ // Typed overload: compile-time param validation for known commands
146
+ export function dispatch<T extends CommandType>(command: Command & { type: T; params: ParamsOf<T> }): void;
147
+ // String fallback: for dynamic dispatch (import_spec replay, etc.)
148
+ export function dispatch(command: Command): void;
149
+ // Implementation
150
+ export function dispatch(command: Command): void {
151
+ // Registry stores CommandDefinition<any> as a type-erasure seam;
152
+ // narrow through unknown to the default-P shape so unsafe-assignment
153
+ // does not fire on the UndoEntry.definition assignment below.
154
+ const definition = commandRegistry.get(command.type) as unknown as
155
+ | CommandDefinition
156
+ | undefined;
157
+ if (!definition) {
158
+ throw new Error(
159
+ `Unknown command type "${command.type}". ` +
160
+ `Has the plugin that provides it been loaded?`,
161
+ );
162
+ }
163
+
164
+ // Execute and capture snapshot for undo
165
+ const snapshot = definition.execute(command.params, context);
166
+
167
+ // Build undo entry with tier, parent pointer, definition, and snapshot
168
+ const entry: UndoEntry = {
169
+ command,
170
+ definition,
171
+ tier: resolveTier(command.type),
172
+ id: command.id,
173
+ parentId: topUndoId(),
174
+ snapshot,
175
+ };
176
+
177
+ // Record for undo; new dispatch clears the redo stack
178
+ undoStack.push(entry);
179
+ redoStack.length = 0;
180
+
181
+ // Enforce max undo size by dropping oldest entries
182
+ while (undoStack.length > maxUndoSize) {
183
+ undoStack.shift();
184
+ }
185
+
186
+ // Semantic log
187
+ const logEntry: SemanticLogEntry = {
188
+ description: definition.describe(command.params),
189
+ timestamp: command.timestamp,
190
+ commandId: command.id,
191
+ };
192
+ semanticLog.push(logEntry);
193
+
194
+ // Notify consumers
195
+ notify(executeCallbacks, command);
196
+ notify(logCallbacks, logEntry);
197
+ notify(specExportCallbacks, command);
198
+
199
+ // Forward to the sync layer (best-effort, won't block local dispatch)
200
+ dispatchHook?.(command);
201
+ }
202
+
203
+ /**
204
+ * Undo the most recent command. Returns the undone command, or undefined if
205
+ * the undo stack is empty.
206
+ */
207
+ export function undoLast(): Command | undefined {
208
+ const entry = undoStack.pop();
209
+ if (!entry) return undefined;
210
+
211
+ entry.definition.undo(entry.command.params, context, entry.snapshot);
212
+ redoStack.push(entry);
213
+
214
+ notify(undoCallbacks, entry.command);
215
+ return entry.command;
216
+ }
217
+
218
+ /**
219
+ * Redo the most recently undone command. Returns the redone command, or
220
+ * undefined if the redo stack is empty.
221
+ */
222
+ export function redoLast(): Command | undefined {
223
+ const entry = redoStack.pop();
224
+ if (!entry) return undefined;
225
+
226
+ // Re-execute and capture a fresh snapshot (buffer state may differ from original)
227
+ const newSnapshot = entry.definition.execute(entry.command.params, context);
228
+ entry.snapshot = newSnapshot;
229
+
230
+ // Update parent pointer to current top of undo stack before pushing
231
+ entry.parentId = topUndoId();
232
+ undoStack.push(entry);
233
+
234
+ // Log the redo as a new semantic entry
235
+ const logEntry: SemanticLogEntry = {
236
+ description: entry.definition.describe(entry.command.params),
237
+ timestamp: Date.now(),
238
+ commandId: entry.command.id,
239
+ };
240
+ semanticLog.push(logEntry);
241
+
242
+ notify(executeCallbacks, entry.command);
243
+ notify(logCallbacks, logEntry);
244
+ notify(specExportCallbacks, entry.command);
245
+
246
+ return entry.command;
247
+ }
248
+
249
+ // --- Two-tier undo query API ---
250
+
251
+ /** Read-only access to the undo stack (entries with tier + parent pointers). */
252
+ export function getUndoStack(): readonly UndoEntry[] {
253
+ return undoStack;
254
+ }
255
+
256
+ /** Read-only access to the redo stack (entries with tier + parent pointers). */
257
+ export function getRedoStack(): readonly UndoEntry[] {
258
+ return redoStack;
259
+ }
260
+
261
+ /** Whether there is at least one command to undo. */
262
+ export function canUndo(): boolean {
263
+ return undoStack.length > 0;
264
+ }
265
+
266
+ /** Whether there is at least one command to redo. */
267
+ export function canRedo(): boolean {
268
+ return redoStack.length > 0;
269
+ }
270
+
271
+ // --- Save checkpoint API ---
272
+
273
+ /**
274
+ * Mark the current undo position as the save checkpoint. Call this after a
275
+ * successful save so that hasUnsavedChanges() returns false until the next
276
+ * dispatch/undo/redo changes the position.
277
+ */
278
+ export function markSaveCheckpoint(): void {
279
+ saveCheckpointId = topUndoId();
280
+ }
281
+
282
+ /**
283
+ * Whether the project has been modified since the last save (or since
284
+ * creation, if never saved). Compares the current top-of-stack entry ID
285
+ * against the checkpoint recorded at save time.
286
+ *
287
+ * Correctly handles:
288
+ * - Undo past the save point (dirty again)
289
+ * - Redo back to the save point (clean again)
290
+ * - New commands diverging from the save point (dirty)
291
+ */
292
+ export function hasUnsavedChanges(): boolean {
293
+ return topUndoId() !== saveCheckpointId;
294
+ }
295
+
296
+ // --- Undo stack size configuration ---
297
+
298
+ /** Set the maximum undo stack size. Oldest entries are dropped when exceeded. */
299
+ export function setMaxUndoSize(size: number): void {
300
+ if (size < 1) throw new Error('Max undo size must be at least 1');
301
+ maxUndoSize = size;
302
+ // Trim if current stack exceeds the new limit
303
+ while (undoStack.length > maxUndoSize) {
304
+ undoStack.shift();
305
+ }
306
+ }
307
+
308
+ /** Get the current maximum undo stack size. */
309
+ export function getMaxUndoSize(): number {
310
+ return maxUndoSize;
311
+ }
312
+
313
+ // --- History management ---
314
+
315
+ /**
316
+ * Clear all undo/redo history and the semantic log.
317
+ * Call this when creating a new project so stale commands from the
318
+ * previous project cannot trigger false unsaved-changes warnings or
319
+ * corrupt state via undo replay.
320
+ */
321
+ export function clearHistory(): void {
322
+ undoStack.length = 0;
323
+ redoStack.length = 0;
324
+ semanticLog.length = 0;
325
+ // Reset checkpoint so a fresh project is not considered dirty
326
+ saveCheckpointId = null;
327
+ }
328
+
329
+ // --- Existing API ---
330
+
331
+ /** Read-only access to the semantic log */
332
+ export function getSemanticLog(): readonly SemanticLogEntry[] {
333
+ return semanticLog;
334
+ }
335
+
336
+ /** Set (or merge into) the shared command context */
337
+ export function setContext(partial: CommandContext): void {
338
+ Object.assign(context, partial);
339
+ }
340
+
341
+ /** Get the shared command context */
342
+ export function getContext(): CommandContext {
343
+ return context;
344
+ }
345
+
346
+ /**
347
+ * Reset all dispatcher state. Intended for tests only.
348
+ */
349
+ export function _resetForTesting(): void {
350
+ undoStack.length = 0;
351
+ redoStack.length = 0;
352
+ semanticLog.length = 0;
353
+ executeCallbacks.length = 0;
354
+ undoCallbacks.length = 0;
355
+ logCallbacks.length = 0;
356
+ specExportCallbacks.length = 0;
357
+ context = {};
358
+ maxUndoSize = 500;
359
+ dispatchHook = null;
360
+ saveCheckpointId = null;
361
+ }
@@ -0,0 +1,7 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ describe('PixelWeaver scaffolding', () => {
4
+ it('should pass a basic sanity check', () => {
5
+ expect(1 + 1).toBe(2);
6
+ });
7
+ });
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Notification State -- reactive singleton for the in-app notification banner.
3
+ * Manages a priority-ordered queue of notifications with auto-dismiss support.
4
+ *
5
+ * Lives in core so that plugin-api.ts and plugin-types.ts can depend on it
6
+ * without a core-to-UI layer violation. The UI component (NotificationBanner)
7
+ * imports via the re-export barrel in ui/notifications/.
8
+ */
9
+
10
+ // --- Types ---
11
+
12
+ export interface AppNotification {
13
+ id: string;
14
+ message: string;
15
+ type: 'info' | 'warning' | 'success';
16
+ /** Priority for ordering (higher = shown first). Default: 0 */
17
+ priority?: number;
18
+ /** Optional action button displayed alongside the message */
19
+ action?: {
20
+ label: string;
21
+ callback: () => void;
22
+ };
23
+ /** Called when the notification is dismissed (manually or via auto-dismiss) */
24
+ onDismiss?: () => void;
25
+ /** Auto-dismiss after this many ms. If absent, stays until manually dismissed. */
26
+ autoDismissMs?: number;
27
+ }
28
+
29
+ /** Union of allowed notification severity values */
30
+ export type NotificationLevel = AppNotification['type'];
31
+
32
+ // --- Reactive state ---
33
+
34
+ let notifications: AppNotification[] = $state([]);
35
+
36
+ /** Active auto-dismiss timers keyed by notification id (not reactive). */
37
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity -- internal timer bookkeeping
38
+ const timers = new Map<string, ReturnType<typeof setTimeout>>();
39
+
40
+ // --- Internal helpers ---
41
+
42
+ /** Sort by priority descending (higher priority first) */
43
+ function sorted(list: AppNotification[]): AppNotification[] {
44
+ return [...list].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
45
+ }
46
+
47
+ function clearTimer(id: string): void {
48
+ const timer = timers.get(id);
49
+ if (timer !== undefined) {
50
+ clearTimeout(timer);
51
+ timers.delete(id);
52
+ }
53
+ }
54
+
55
+ function startAutoDismiss(notification: AppNotification): void {
56
+ if (notification.autoDismissMs == null || notification.autoDismissMs <= 0) return;
57
+ clearTimer(notification.id);
58
+ timers.set(
59
+ notification.id,
60
+ setTimeout(() => {
61
+ notificationState.dismiss(notification.id);
62
+ }, notification.autoDismissMs),
63
+ );
64
+ }
65
+
66
+ // --- Public API ---
67
+
68
+ export const notificationState = {
69
+ /** Add a notification. If one with the same id exists, it is replaced. */
70
+ push(notification: AppNotification): void {
71
+ // Replace existing notification with same id
72
+ const idx = notifications.findIndex((n) => n.id === notification.id);
73
+ if (idx !== -1) {
74
+ clearTimer(notification.id);
75
+ notifications[idx] = notification;
76
+ // Trigger reactivity by reassigning the array
77
+ notifications = [...notifications];
78
+ } else {
79
+ notifications = [...notifications, notification];
80
+ }
81
+ startAutoDismiss(notification);
82
+ },
83
+
84
+ /** Remove a notification by id and invoke its onDismiss callback. */
85
+ dismiss(id: string): void {
86
+ const target = notifications.find((n) => n.id === id);
87
+ if (!target) return;
88
+ clearTimer(id);
89
+ notifications = notifications.filter((n) => n.id !== id);
90
+ target.onDismiss?.();
91
+ },
92
+
93
+ /** Remove all notifications, invoking each onDismiss callback. */
94
+ dismissAll(): void {
95
+ const copy = [...notifications];
96
+ for (const n of copy) {
97
+ clearTimer(n.id);
98
+ }
99
+ notifications = [];
100
+ for (const n of copy) {
101
+ n.onDismiss?.();
102
+ }
103
+ },
104
+
105
+ /** Sorted notification list (highest priority first). Reactive. */
106
+ get notifications(): AppNotification[] {
107
+ return sorted(notifications);
108
+ },
109
+
110
+ /** The highest-priority notification, or undefined if empty. Reactive. */
111
+ get current(): AppNotification | undefined {
112
+ return sorted(notifications)[0];
113
+ },
114
+
115
+ /** Total number of active notifications. Reactive. */
116
+ get count(): number {
117
+ return notifications.length;
118
+ },
119
+ };
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Plugin API -- the object passed to each plugin's register() function.
3
+ *
4
+ * Provides methods to register commands, tools, panels, exporters, importers,
5
+ * and shortcuts, plus helpers to dispatch commands and query app state.
6
+ */
7
+
8
+ import type { Command } from './commands.js';
9
+ import type { PluginAPI } from './plugin-types.js';
10
+ import {
11
+ commandRegistry,
12
+ toolRegistry,
13
+ panelRegistry,
14
+ exporterRegistry,
15
+ importerRegistry,
16
+ shortcutRegistry,
17
+ menuRegistry,
18
+ toolbarRegistry,
19
+ } from './registries.svelte.js';
20
+ import { dispatch as dispatchCommand, onExecute } from './dispatcher.js';
21
+ import { notificationState } from './notification-state.svelte.js';
22
+ import { canvasState } from '../canvas/canvas-state.svelte.js';
23
+ import { PixelBuffer } from '../canvas/pixel-buffer.js';
24
+ import {
25
+ getCurrentFrame,
26
+ getFrames as getFramesInternal,
27
+ addFrame as addFrameInternal,
28
+ setFrameDuration as setFrameDurationInternal,
29
+ setCurrentFrame as setCurrentFrameInternal,
30
+ setGlobalFps as setGlobalFpsInternal,
31
+ deserialize as deserializeFramesInternal,
32
+ } from '../animation/frame-model.svelte.js';
33
+ import {
34
+ getLayers,
35
+ getActiveLayer,
36
+ addLayer as addLayerInternal,
37
+ addGroup as addGroupInternal,
38
+ setVisibility as setVisibilityInternal,
39
+ setOpacity as setOpacityInternal,
40
+ deserialize as deserializeLayersInternal,
41
+ } from '../layers/layer-tree.svelte.js';
42
+ import { setProjectPalette as setProjectPaletteInternal } from '../color/palette-state.svelte.js';
43
+ import {
44
+ hasSelection as selectionHasSelection,
45
+ getSelectedPixels as selectionGetSelectedPixels,
46
+ } from '../../../plugins/builtin/selection-tool.js';
47
+
48
+ // Re-export so existing consumers can import PluginAPI from either location
49
+ export type { PluginAPI } from './plugin-types.js';
50
+
51
+ /**
52
+ * Create a PluginAPI instance scoped to a specific plugin.
53
+ * The pluginName is used to namespace registrations and fill command metadata.
54
+ */
55
+ export function createPluginAPI(pluginName: string): PluginAPI {
56
+ // Method parameter types are inferred from the PluginAPI return type
57
+ return {
58
+ addCommand(name, definition) {
59
+ // Stamp the plugin name onto the definition so executeOrDispatch() can
60
+ // construct well-formed Command objects when routing through dispatch().
61
+ commandRegistry.set(name, { ...definition, _plugin: pluginName });
62
+ },
63
+
64
+ addTool(name, definition) {
65
+ toolRegistry.set(name, definition);
66
+ },
67
+
68
+ addPanel(name, definition) {
69
+ panelRegistry.set(name, definition);
70
+ },
71
+
72
+ addExporter(name, definition) {
73
+ exporterRegistry.set(name, definition);
74
+ },
75
+
76
+ addImporter(name, definition) {
77
+ importerRegistry.set(name, definition);
78
+ },
79
+
80
+ addShortcut(name, definition) {
81
+ shortcutRegistry.set(name, definition);
82
+ },
83
+
84
+ addMenuItem(name, contribution) {
85
+ menuRegistry.set(name, contribution);
86
+ },
87
+
88
+ addToolbarItem(name, contribution) {
89
+ toolbarRegistry.set(name, contribution);
90
+ },
91
+
92
+ notify(notification) {
93
+ notificationState.push(notification);
94
+ },
95
+
96
+ dismissNotification(id) {
97
+ notificationState.dismiss(id);
98
+ },
99
+
100
+ dispatch(partial: Omit<Command, 'id' | 'timestamp'>) {
101
+ const command: Command = {
102
+ ...partial,
103
+ id: crypto.randomUUID(),
104
+ timestamp: Date.now(),
105
+ };
106
+ dispatchCommand(command);
107
+ },
108
+
109
+ onCommand(callback) {
110
+ return onExecute(callback);
111
+ },
112
+
113
+ getCanvas() {
114
+ return canvasState;
115
+ },
116
+ getProject() {
117
+ return {
118
+ name: 'default',
119
+ width: canvasState.canvasWidth,
120
+ height: canvasState.canvasHeight,
121
+ };
122
+ },
123
+ getActiveFrame() {
124
+ return getCurrentFrame();
125
+ },
126
+ getActiveLayers() {
127
+ return { all: getLayers(), active: getActiveLayer() };
128
+ },
129
+ getSelection() {
130
+ // Return a stable object of bound methods so callers can capture it once
131
+ // at register time; each method invocation reads live selection state.
132
+ return {
133
+ hasSelection: selectionHasSelection,
134
+ getSelectedPixels: selectionGetSelectedPixels,
135
+ };
136
+ },
137
+
138
+ // --- Mutators ---
139
+
140
+ setCanvasSize(width, height) {
141
+ canvasState.canvasWidth = width;
142
+ canvasState.canvasHeight = height;
143
+ },
144
+
145
+ addLayer(name, options) {
146
+ return addLayerInternal(name, options).id;
147
+ },
148
+
149
+ addGroup(name, options) {
150
+ return addGroupInternal(name, options).id;
151
+ },
152
+
153
+ setLayerVisibility(id, visible) {
154
+ setVisibilityInternal(id, visible);
155
+ },
156
+
157
+ setLayerOpacity(id, opacity) {
158
+ setOpacityInternal(id, opacity);
159
+ },
160
+
161
+ deserializeLayers(state) {
162
+ deserializeLayersInternal(state);
163
+ },
164
+
165
+ addFrame(options) {
166
+ return addFrameInternal(options).index;
167
+ },
168
+
169
+ getFrames() {
170
+ return getFramesInternal();
171
+ },
172
+
173
+ setFrameDuration(index, durationMs) {
174
+ setFrameDurationInternal(index, durationMs);
175
+ },
176
+
177
+ setCurrentFrame(index) {
178
+ setCurrentFrameInternal(index);
179
+ },
180
+
181
+ setGlobalFps(fps) {
182
+ setGlobalFpsInternal(fps);
183
+ },
184
+
185
+ deserializeFrames(data) {
186
+ deserializeFramesInternal(data);
187
+ },
188
+
189
+ createPixelBuffer(width, height) {
190
+ return new PixelBuffer(width, height);
191
+ },
192
+
193
+ setFramePixelData(frameIndex, layerId, buffer) {
194
+ // Write pixel data directly into the frame's per-layer map. This is the
195
+ // single supported way for plugins to populate raw pixels for a given
196
+ // (frame, layer) pair -- importers rely on this.
197
+ const frames = getFramesInternal();
198
+ if (frameIndex < 0 || frameIndex >= frames.length) {
199
+ throw new Error(`Frame index ${String(frameIndex)} out of range (0..${String(frames.length - 1)}).`);
200
+ }
201
+ const frame = frames[frameIndex];
202
+ if (!frame) throw new Error(`Frame ${String(frameIndex)} missing`);
203
+ frame.pixelData.set(layerId, buffer);
204
+ },
205
+
206
+ setProjectPalette(palette) {
207
+ setProjectPaletteInternal(palette);
208
+ },
209
+ };
210
+ }