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,388 @@
1
+ /**
2
+ * Selection Tool Plugin -- rectangle and lasso (polygon) selection.
3
+ *
4
+ * Registers:
5
+ * - Command: `select_rect` (tier: 'frame') -- not pixel-modifying, but recorded for undo
6
+ * - Command: `select_lasso` (tier: 'frame')
7
+ * - Command: `move_selection` (tier: 'frame') -- moves selected pixels
8
+ * - Command: `delete_selection` (tier: 'frame') -- clears selected pixels to transparent
9
+ * - Command: `deselect` (tier: 'frame')
10
+ * - Tool: `rect-select`
11
+ * - Tool: `lasso-select`
12
+ *
13
+ * Selection state is a module-level Set<string> of "x,y" keys.
14
+ * Only operations that modify pixels (move, delete) are true undoable commands.
15
+ * Selecting/deselecting is non-destructive but still recorded for undo convenience.
16
+ */
17
+
18
+ import type { PluginModule } from '../../src/lib/core/plugin-loader.js';
19
+ import type { PixelData, Point } from './drawing-utils.js';
20
+ import { snapshotPixels, applyPixels } from './drawing-utils.js';
21
+ import RectSelectIcon from '~icons/lucide/square-dashed';
22
+ import SelectIcon from '~icons/lucide/lasso';
23
+
24
+ // --- Selection state (module-level singleton) ---
25
+
26
+ /** The current set of selected pixel coordinates as "x,y" strings. */
27
+ let selectedPixels = new Set<string>();
28
+
29
+ /** Whether any pixels are currently selected. */
30
+ export function hasSelection(): boolean {
31
+ return selectedPixels.size > 0;
32
+ }
33
+
34
+ /** Get the current selection set (read-only reference). */
35
+ export function getSelectedPixels(): ReadonlySet<string> {
36
+ return selectedPixels;
37
+ }
38
+
39
+ /** Check if a specific pixel is selected. */
40
+ export function isSelected(x: number, y: number): boolean {
41
+ return selectedPixels.has(`${String(x)},${String(y)}`);
42
+ }
43
+
44
+ /** Select all pixels inside a rectangle. */
45
+ export function selectRect(x: number, y: number, w: number, h: number): void {
46
+ selectedPixels = new Set<string>();
47
+ for (let py = y; py < y + h; py++) {
48
+ for (let px = x; px < x + w; px++) {
49
+ selectedPixels.add(`${String(px)},${String(py)}`);
50
+ }
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Select all pixels inside a polygon defined by the given vertices.
56
+ * Uses the ray-casting algorithm for point-in-polygon testing.
57
+ */
58
+ export function selectLasso(points: Point[]): void {
59
+ if (points.length < 3) {
60
+ selectedPixels = new Set<string>();
61
+ return;
62
+ }
63
+
64
+ // Compute bounding box of the polygon
65
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
66
+ for (const p of points) {
67
+ if (p.x < minX) minX = p.x;
68
+ if (p.y < minY) minY = p.y;
69
+ if (p.x > maxX) maxX = p.x;
70
+ if (p.y > maxY) maxY = p.y;
71
+ }
72
+
73
+ selectedPixels = new Set<string>();
74
+ // Test each pixel in the bounding box
75
+ for (let py = Math.floor(minY); py <= Math.ceil(maxY); py++) {
76
+ for (let px = Math.floor(minX); px <= Math.ceil(maxX); px++) {
77
+ if (pointInPolygon(px, py, points)) {
78
+ selectedPixels.add(`${String(px)},${String(py)}`);
79
+ }
80
+ }
81
+ }
82
+ }
83
+
84
+ /** Clear the current selection. */
85
+ export function deselect(): void {
86
+ selectedPixels = new Set<string>();
87
+ }
88
+
89
+ /**
90
+ * Ray-casting point-in-polygon test.
91
+ * Casts a ray from (px, py) in the +X direction and counts edge crossings.
92
+ * An odd count means the point is inside.
93
+ */
94
+ export function pointInPolygon(px: number, py: number, polygon: Point[]): boolean {
95
+ let inside = false;
96
+ const n = polygon.length;
97
+
98
+ for (let i = 0, j = n - 1; i < n; j = i++) {
99
+ const pi = polygon[i];
100
+ const pj = polygon[j];
101
+ if (!pi || !pj) continue;
102
+ const xi = pi.x, yi = pi.y;
103
+ const xj = pj.x, yj = pj.y;
104
+
105
+ // Check if the ray from (px, py) in +X direction crosses the edge (i, j)
106
+ const intersect =
107
+ ((yi > py) !== (yj > py)) &&
108
+ (px < (xj - xi) * (py - yi) / (yj - yi) + xi);
109
+
110
+ if (intersect) inside = !inside;
111
+ }
112
+
113
+ return inside;
114
+ }
115
+
116
+ // --- Plugin ---
117
+
118
+ export const selectionToolPlugin: PluginModule = {
119
+ name: 'builtin/selection',
120
+ version: '1.0.0',
121
+ dependencies: [],
122
+ register(api) {
123
+ // --- Commands ---
124
+
125
+ api.addCommand('select_rect', {
126
+ tier: 'frame',
127
+
128
+ execute(params) {
129
+ // Capture previous selection as snapshot for undo
130
+ const prevSelection = Array.from(selectedPixels);
131
+ selectRect(
132
+ params["x"],
133
+ params["y"],
134
+ params["w"],
135
+ params["h"],
136
+ );
137
+ return prevSelection;
138
+ },
139
+
140
+ undo(_params, _ctx, snapshot) {
141
+ selectedPixels = new Set((snapshot ?? []) as string[]);
142
+ },
143
+
144
+ describe(params) {
145
+ return `Selected rect (${String(params["x"])},${String(params["y"])}) ${String(params["w"])}x${String(params["h"])}`;
146
+ },
147
+ });
148
+
149
+ api.addCommand('select_lasso', {
150
+ tier: 'frame',
151
+
152
+ execute(params) {
153
+ const prevSelection = Array.from(selectedPixels);
154
+ selectLasso(params["points"]);
155
+ return prevSelection;
156
+ },
157
+
158
+ undo(_params, _ctx, snapshot) {
159
+ selectedPixels = new Set((snapshot ?? []) as string[]);
160
+ },
161
+
162
+ describe(params) {
163
+ const pts = params["points"];
164
+ return `Selected lasso with ${String(pts.length)} vertices`;
165
+ },
166
+ });
167
+
168
+ api.addCommand('move_selection', {
169
+ tier: 'frame',
170
+
171
+ execute(params, ctx) {
172
+ const buffer = ctx.getActiveBuffer?.();
173
+ if (!buffer) return;
174
+
175
+ const dx = params["dx"];
176
+ const dy = params["dy"];
177
+
178
+ // Build snapshot of all affected pixels (source + destination) before modifying
179
+ const allPoints: Point[] = [];
180
+ for (const key of selectedPixels) {
181
+ const [sx, sy] = key.split(',').map(Number) as [number, number];
182
+ allPoints.push({ x: sx, y: sy });
183
+ allPoints.push({ x: sx + dx, y: sy + dy });
184
+ }
185
+ const snapshot = snapshotPixels(buffer, allPoints);
186
+
187
+ // Read selected pixel colors, clear originals, write at offset
188
+ const pixelColors: { x: number; y: number; r: number; g: number; b: number; a: number }[] = [];
189
+ for (const key of selectedPixels) {
190
+ const [sx, sy] = key.split(',').map(Number) as [number, number];
191
+ const [r, g, b, a] = buffer.getPixel(sx, sy);
192
+ pixelColors.push({ x: sx, y: sy, r, g, b, a });
193
+ }
194
+
195
+ // Clear original positions
196
+ for (const pc of pixelColors) {
197
+ buffer.setPixel(pc.x, pc.y, 0, 0, 0, 0);
198
+ }
199
+
200
+ // Write at new positions
201
+ for (const pc of pixelColors) {
202
+ const nx = pc.x + dx;
203
+ const ny = pc.y + dy;
204
+ if (buffer.inBounds(nx, ny)) {
205
+ buffer.setPixel(nx, ny, pc.r, pc.g, pc.b, pc.a);
206
+ }
207
+ }
208
+
209
+ // Update selection coordinates
210
+ const newSelection = new Set<string>();
211
+ for (const key of selectedPixels) {
212
+ const [sx, sy] = key.split(',').map(Number) as [number, number];
213
+ newSelection.add(`${String(sx + dx)},${String(sy + dy)}`);
214
+ }
215
+ selectedPixels = newSelection;
216
+
217
+ return snapshot;
218
+ },
219
+
220
+ undo(params, ctx, snapshot) {
221
+ const buffer = ctx.getActiveBuffer?.();
222
+ if (!buffer) return;
223
+
224
+ applyPixels(buffer, snapshot as PixelData[]);
225
+
226
+ // Reverse the selection coordinate shift
227
+ const dx = params["dx"];
228
+ const dy = params["dy"];
229
+ const restoredSelection = new Set<string>();
230
+ for (const key of selectedPixels) {
231
+ const [sx, sy] = key.split(',').map(Number) as [number, number];
232
+ restoredSelection.add(`${String(sx - dx)},${String(sy - dy)}`);
233
+ }
234
+ selectedPixels = restoredSelection;
235
+ },
236
+
237
+ describe(params) {
238
+ return `Moved selection by (${String(params["dx"])}, ${String(params["dy"])})`;
239
+ },
240
+ });
241
+
242
+ api.addCommand('delete_selection', {
243
+ tier: 'frame',
244
+
245
+ execute(_params, ctx) {
246
+ const buffer = ctx.getActiveBuffer?.();
247
+ if (!buffer) return;
248
+
249
+ // Snapshot selected pixels for undo
250
+ const points: Point[] = [];
251
+ for (const key of selectedPixels) {
252
+ const [x, y] = key.split(',').map(Number) as [number, number];
253
+ points.push({ x, y });
254
+ }
255
+ const snapshot = snapshotPixels(buffer, points);
256
+
257
+ // Clear selected pixels to transparent
258
+ for (const key of selectedPixels) {
259
+ const [x, y] = key.split(',').map(Number) as [number, number];
260
+ buffer.setPixel(x, y, 0, 0, 0, 0);
261
+ }
262
+
263
+ return snapshot;
264
+ },
265
+
266
+ undo(_params, ctx, snapshot) {
267
+ const buffer = ctx.getActiveBuffer?.();
268
+ if (!buffer) return;
269
+
270
+ applyPixels(buffer, snapshot as PixelData[]);
271
+ },
272
+
273
+ describe() {
274
+ return `Deleted selected pixels`;
275
+ },
276
+ });
277
+
278
+ api.addCommand('deselect', {
279
+ tier: 'frame',
280
+
281
+ execute() {
282
+ const prevSelection = Array.from(selectedPixels);
283
+ deselect();
284
+ return prevSelection;
285
+ },
286
+
287
+ undo(_params, _ctx, snapshot) {
288
+ selectedPixels = new Set((snapshot ?? []) as string[]);
289
+ },
290
+
291
+ describe() {
292
+ return 'Deselected';
293
+ },
294
+ });
295
+
296
+ // --- Toolbar contributions ---
297
+
298
+ api.addToolbarItem('toolbar:drawing-tools:rect-select', {
299
+ toolbarId: 'drawing-tools',
300
+ kind: 'tool',
301
+ targetId: 'rect-select',
302
+ group: 'select',
303
+ order: 10,
304
+ });
305
+
306
+ api.addToolbarItem('toolbar:drawing-tools:lasso-select', {
307
+ toolbarId: 'drawing-tools',
308
+ kind: 'tool',
309
+ targetId: 'lasso-select',
310
+ group: 'select',
311
+ order: 20,
312
+ });
313
+
314
+ // --- Tools ---
315
+
316
+ // Rectangle select tool
317
+ let rectDrawing = false;
318
+ let rectStartX = 0;
319
+ let rectStartY = 0;
320
+
321
+ api.addTool('rect-select', {
322
+ icon: RectSelectIcon,
323
+ cursor: 'crosshair',
324
+
325
+ onPointerDown(_e, ctx) {
326
+ rectDrawing = true;
327
+ rectStartX = ctx.canvasX;
328
+ rectStartY = ctx.canvasY;
329
+ },
330
+
331
+ onPointerMove(_e, _ctx) {
332
+ // Preview handled by canvas viewport
333
+ },
334
+
335
+ onPointerUp(_e, ctx) {
336
+ if (!rectDrawing) return;
337
+ rectDrawing = false;
338
+
339
+ const x = Math.min(rectStartX, ctx.canvasX);
340
+ const y = Math.min(rectStartY, ctx.canvasY);
341
+ const w = Math.abs(ctx.canvasX - rectStartX) + 1;
342
+ const h = Math.abs(ctx.canvasY - rectStartY) + 1;
343
+
344
+ ctx.api.dispatch({
345
+ type: 'select_rect',
346
+ plugin: 'builtin/selection',
347
+ version: '1.0.0',
348
+ params: { x, y, w, h },
349
+ });
350
+ },
351
+ });
352
+
353
+ // Lasso select tool
354
+ let lassoDrawing = false;
355
+ let lassoPoints: Point[] = [];
356
+
357
+ api.addTool('lasso-select', {
358
+ icon: SelectIcon,
359
+ cursor: 'crosshair',
360
+
361
+ onPointerDown(_e, ctx) {
362
+ lassoDrawing = true;
363
+ lassoPoints = [{ x: ctx.canvasX, y: ctx.canvasY }];
364
+ },
365
+
366
+ onPointerMove(_e, ctx) {
367
+ if (!lassoDrawing) return;
368
+ lassoPoints.push({ x: ctx.canvasX, y: ctx.canvasY });
369
+ },
370
+
371
+ onPointerUp(_e, ctx) {
372
+ if (!lassoDrawing) return;
373
+ lassoDrawing = false;
374
+
375
+ if (lassoPoints.length < 3) return;
376
+
377
+ ctx.api.dispatch({
378
+ type: 'select_lasso',
379
+ plugin: 'builtin/selection',
380
+ version: '1.0.0',
381
+ params: { points: lassoPoints },
382
+ });
383
+
384
+ lassoPoints = [];
385
+ },
386
+ });
387
+ },
388
+ };
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Tests for the selection tool plugin.
3
+ *
4
+ * Covers rect select, lasso select, point-in-polygon,
5
+ * move selection, delete selection, and deselect.
6
+ */
7
+
8
+ import { describe, it, expect, beforeEach } from 'vitest';
9
+ import { PixelBuffer } from '../../src/lib/canvas/pixel-buffer.js';
10
+ import { commandRegistry } from '../../src/lib/core/registries.svelte.js';
11
+ import { dispatch, undoLast, _resetForTesting as resetDispatcher, setContext } from '../../src/lib/core/dispatcher.js';
12
+ import type { Command } from '../../src/lib/core/commands.js';
13
+ import type { CommandType, ParamsOf } from '../../src/lib/core/command-params.js';
14
+ import { createPluginAPI } from '../../src/lib/core/plugin-api.js';
15
+
16
+ import {
17
+ selectionToolPlugin,
18
+ selectRect,
19
+ selectLasso,
20
+ deselect,
21
+ isSelected,
22
+ hasSelection,
23
+ getSelectedPixels,
24
+ pointInPolygon,
25
+ } from './selection-tool.js';
26
+
27
+ // --- Helpers ---
28
+
29
+ // Typed overload: validates params when type is a known command literal
30
+ function makeCommand<T extends CommandType>(type: T, params: ParamsOf<T>): Command;
31
+ // String fallback: for dynamic/unknown command types in tests
32
+ function makeCommand(type: string, params?: Record<string, unknown>): Command;
33
+ function makeCommand(type: string, params: Record<string, unknown> = {}): Command {
34
+ return { type, plugin: 'test', version: '1.0.0', params, timestamp: Date.now(), id: crypto.randomUUID() };
35
+ }
36
+
37
+ function setupBuffer(width = 16, height = 16): PixelBuffer {
38
+ const buffer = new PixelBuffer(width, height);
39
+ setContext({ getActiveBuffer: () => buffer });
40
+ return buffer;
41
+ }
42
+
43
+ // --- Tests ---
44
+
45
+ describe('Selection Tool Plugin', () => {
46
+ beforeEach(() => {
47
+ resetDispatcher();
48
+ deselect();
49
+ const api = createPluginAPI('builtin/selection');
50
+ selectionToolPlugin.register(api);
51
+ });
52
+
53
+ it('should register all selection commands', () => {
54
+ expect(commandRegistry.get('select_rect')).toBeDefined();
55
+ expect(commandRegistry.get('select_lasso')).toBeDefined();
56
+ expect(commandRegistry.get('move_selection')).toBeDefined();
57
+ expect(commandRegistry.get('delete_selection')).toBeDefined();
58
+ expect(commandRegistry.get('deselect')).toBeDefined();
59
+ });
60
+
61
+ it('rect select sets correct pixels in selection', () => {
62
+ selectRect(2, 3, 4, 5);
63
+ expect(hasSelection()).toBe(true);
64
+ expect(isSelected(2, 3)).toBe(true);
65
+ expect(isSelected(5, 7)).toBe(true);
66
+ expect(isSelected(1, 3)).toBe(false);
67
+ expect(isSelected(6, 3)).toBe(false);
68
+ expect(getSelectedPixels().size).toBe(20); // 4 * 5
69
+ });
70
+
71
+ it('lasso select with triangle selects correct interior points', () => {
72
+ // Triangle with vertices at (0,0), (10,0), (5,10)
73
+ const triangle = [{ x: 0, y: 0 }, { x: 10, y: 0 }, { x: 5, y: 10 }];
74
+ selectLasso(triangle);
75
+ expect(hasSelection()).toBe(true);
76
+ // Center of the triangle should be selected
77
+ expect(isSelected(5, 3)).toBe(true);
78
+ // Far outside should not be selected
79
+ expect(isSelected(15, 15)).toBe(false);
80
+ });
81
+
82
+ it('point-in-polygon handles boundary and concave shapes', () => {
83
+ // Concave L-shape polygon
84
+ const lShape = [
85
+ { x: 0, y: 0 },
86
+ { x: 4, y: 0 },
87
+ { x: 4, y: 2 },
88
+ { x: 2, y: 2 },
89
+ { x: 2, y: 4 },
90
+ { x: 0, y: 4 },
91
+ ];
92
+ // Inside the L
93
+ expect(pointInPolygon(1, 1, lShape)).toBe(true);
94
+ expect(pointInPolygon(1, 3, lShape)).toBe(true);
95
+ // Inside the notch (outside the L)
96
+ expect(pointInPolygon(3, 3, lShape)).toBe(false);
97
+ // Clearly outside
98
+ expect(pointInPolygon(5, 5, lShape)).toBe(false);
99
+ });
100
+
101
+ it('move selection: pixels move and old area becomes transparent', () => {
102
+ const buffer = setupBuffer(16, 16);
103
+ // Paint some pixels
104
+ buffer.setPixel(2, 2, 255, 0, 0, 255);
105
+ buffer.setPixel(3, 2, 0, 255, 0, 255);
106
+ selectRect(2, 2, 2, 1);
107
+
108
+ dispatch(makeCommand('move_selection', { dx: 5, dy: 3 }));
109
+
110
+ // Old positions should be transparent
111
+ expect(buffer.getPixel(2, 2)).toEqual([0, 0, 0, 0]);
112
+ expect(buffer.getPixel(3, 2)).toEqual([0, 0, 0, 0]);
113
+ // New positions should have the colors
114
+ expect(buffer.getPixel(7, 5)).toEqual([255, 0, 0, 255]);
115
+ expect(buffer.getPixel(8, 5)).toEqual([0, 255, 0, 255]);
116
+ });
117
+
118
+ it('delete selection: selected pixels become transparent', () => {
119
+ const buffer = setupBuffer(8, 8);
120
+ buffer.fill(255, 0, 0, 255);
121
+ selectRect(1, 1, 3, 3);
122
+
123
+ dispatch(makeCommand('delete_selection', {}));
124
+
125
+ // Selected region should be transparent
126
+ expect(buffer.getPixel(1, 1)).toEqual([0, 0, 0, 0]);
127
+ expect(buffer.getPixel(2, 2)).toEqual([0, 0, 0, 0]);
128
+ expect(buffer.getPixel(3, 3)).toEqual([0, 0, 0, 0]);
129
+ // Outside selection should be unchanged
130
+ expect(buffer.getPixel(0, 0)).toEqual([255, 0, 0, 255]);
131
+ expect(buffer.getPixel(4, 4)).toEqual([255, 0, 0, 255]);
132
+ });
133
+
134
+ it('deselect clears the selection', () => {
135
+ selectRect(0, 0, 5, 5);
136
+ expect(hasSelection()).toBe(true);
137
+ deselect();
138
+ expect(hasSelection()).toBe(false);
139
+ expect(getSelectedPixels().size).toBe(0);
140
+ });
141
+
142
+ it('undo delete_selection restores pixels', () => {
143
+ const buffer = setupBuffer(8, 8);
144
+ buffer.fill(100, 200, 50, 255);
145
+ selectRect(2, 2, 2, 2);
146
+
147
+ dispatch(makeCommand('delete_selection', {}));
148
+ expect(buffer.getPixel(2, 2)).toEqual([0, 0, 0, 0]);
149
+
150
+ undoLast();
151
+ expect(buffer.getPixel(2, 2)).toEqual([100, 200, 50, 255]);
152
+ expect(buffer.getPixel(3, 3)).toEqual([100, 200, 50, 255]);
153
+ });
154
+
155
+ it('undo move_selection restores pixels to original positions', () => {
156
+ const buffer = setupBuffer(16, 16);
157
+ buffer.setPixel(4, 4, 255, 128, 0, 255);
158
+ selectRect(4, 4, 1, 1);
159
+
160
+ dispatch(makeCommand('move_selection', { dx: 3, dy: 2 }));
161
+ expect(buffer.getPixel(4, 4)).toEqual([0, 0, 0, 0]);
162
+ expect(buffer.getPixel(7, 6)).toEqual([255, 128, 0, 255]);
163
+
164
+ undoLast();
165
+ expect(buffer.getPixel(4, 4)).toEqual([255, 128, 0, 255]);
166
+ });
167
+
168
+ it('point-in-polygon correctly handles a square', () => {
169
+ const square = [
170
+ { x: 0, y: 0 },
171
+ { x: 10, y: 0 },
172
+ { x: 10, y: 10 },
173
+ { x: 0, y: 10 },
174
+ ];
175
+ expect(pointInPolygon(5, 5, square)).toBe(true);
176
+ expect(pointInPolygon(-1, 5, square)).toBe(false);
177
+ expect(pointInPolygon(11, 5, square)).toBe(false);
178
+ });
179
+
180
+ it('select_rect command via dispatch', () => {
181
+ setupBuffer();
182
+ dispatch(makeCommand('select_rect', { x: 1, y: 1, w: 3, h: 3 }));
183
+ expect(isSelected(1, 1)).toBe(true);
184
+ expect(isSelected(3, 3)).toBe(true);
185
+ expect(getSelectedPixels().size).toBe(9);
186
+ });
187
+
188
+ it('select_lasso command via dispatch', () => {
189
+ setupBuffer();
190
+ const triangle = [{ x: 0, y: 0 }, { x: 10, y: 0 }, { x: 5, y: 10 }];
191
+ dispatch(makeCommand('select_lasso', { points: triangle }));
192
+ expect(hasSelection()).toBe(true);
193
+ expect(isSelected(5, 3)).toBe(true);
194
+ });
195
+ });