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,12 @@
1
+ ---
2
+ title: server.src.pixelweaver.state
3
+ description: "In-memory authoritative state for PixelWeaver projects."
4
+ generated: true
5
+ nav_group: "API Reference"
6
+ nav_order: 22
7
+ ---
8
+ <!-- generated by selfdoc gen, do not edit -->
9
+
10
+ # server.src.pixelweaver.state
11
+
12
+ :-: ref path="server.src.pixelweaver.state"
@@ -0,0 +1,12 @@
1
+ ---
2
+ title: server.src.pixelweaver.storage
3
+ description: "Project file I/O for PixelWeaver."
4
+ generated: true
5
+ nav_group: "API Reference"
6
+ nav_order: 23
7
+ ---
8
+ <!-- generated by selfdoc gen, do not edit -->
9
+
10
+ # server.src.pixelweaver.storage
11
+
12
+ :-: ref path="server.src.pixelweaver.storage"
@@ -0,0 +1,12 @@
1
+ ---
2
+ title: server.src.pixelweaver.websocket
3
+ description: "WebSocket message handler for PixelWeaver."
4
+ generated: true
5
+ nav_group: "API Reference"
6
+ nav_order: 24
7
+ ---
8
+ <!-- generated by selfdoc gen, do not edit -->
9
+
10
+ # server.src.pixelweaver.websocket
11
+
12
+ :-: ref path="server.src.pixelweaver.websocket"
@@ -0,0 +1,12 @@
1
+ ---
2
+ title: server.src.pixelweaver
3
+ description: "PixelWeaver collaboration server."
4
+ generated: true
5
+ nav_group: "API Reference"
6
+ nav_order: 1
7
+ ---
8
+ <!-- generated by selfdoc gen, do not edit -->
9
+
10
+ # server.src.pixelweaver
11
+
12
+ :-: ref path="server.src.pixelweaver"
@@ -0,0 +1,35 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ test.describe('App Launch', () => {
4
+ test('should load the application', async ({ page }) => {
5
+ await page.goto('/');
6
+ // Check that the app shell rendered
7
+ await expect(page.locator('canvas').first()).toBeVisible();
8
+ });
9
+
10
+ test('should show the menu bar', async ({ page }) => {
11
+ await page.goto('/');
12
+ await expect(page.getByText('PixelWeaver')).toBeVisible();
13
+ });
14
+
15
+ test('should show the tool bar', async ({ page }) => {
16
+ await page.goto('/');
17
+ // The toolbar should have tool buttons
18
+ await expect(page.locator('.toolbar-panel')).toBeVisible();
19
+ });
20
+
21
+ test('should show the status bar', async ({ page }) => {
22
+ await page.goto('/');
23
+ // Status bar shows canvas dimensions
24
+ await expect(page.getByText('32 x 32')).toBeVisible();
25
+ });
26
+
27
+ test('should show the layer panel', async ({ page }) => {
28
+ await page.goto('/');
29
+ await expect(page.getByText('Layers', { exact: true })).toBeVisible();
30
+ // Click the Layers tab to reveal its content
31
+ await page.getByText('Layers', { exact: true }).click();
32
+ // Default layer should exist
33
+ await expect(page.getByText('Layer 1')).toBeVisible();
34
+ });
35
+ });
@@ -0,0 +1,26 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ test.describe('Menu Bar', () => {
4
+ test('should open File menu on click', async ({ page }) => {
5
+ await page.goto('/');
6
+ await page.getByText('File').click();
7
+ // Dropdown should appear with menu items
8
+ await expect(page.getByText('New Project')).toBeVisible();
9
+ await expect(page.getByText('Export')).toBeVisible();
10
+ });
11
+
12
+ test('should open Edit menu', async ({ page }) => {
13
+ await page.goto('/');
14
+ await page.getByRole('menuitem', { name: 'Edit' }).click();
15
+ await expect(page.getByText('Undo')).toBeVisible();
16
+ await expect(page.getByText('Redo')).toBeVisible();
17
+ });
18
+
19
+ test('should close menu on Escape', async ({ page }) => {
20
+ await page.goto('/');
21
+ await page.getByText('File').click();
22
+ await expect(page.getByText('New Project')).toBeVisible();
23
+ await page.keyboard.press('Escape');
24
+ await expect(page.getByText('New Project')).not.toBeVisible();
25
+ });
26
+ });
@@ -0,0 +1,27 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ test.describe('Tool Activation', () => {
4
+ test('should activate pencil tool with keyboard shortcut B', async ({ page }) => {
5
+ await page.goto('/');
6
+ await page.keyboard.press('b');
7
+ // The status bar should show the active tool
8
+ // Check for 'pencil' text in the status bar area
9
+ await expect(page.locator('.statusbar')).toContainText('pencil');
10
+ });
11
+
12
+ test('should activate eraser tool with keyboard shortcut E', async ({ page }) => {
13
+ await page.goto('/');
14
+ await page.keyboard.press('e');
15
+ await expect(page.locator('.statusbar')).toContainText('eraser');
16
+ });
17
+
18
+ test('should switch tools by clicking toolbar buttons', async ({ page }) => {
19
+ await page.goto('/');
20
+ // Find and click a tool button (they show tool name as text)
21
+ const fillButton = page.locator('.toolbar button', { hasText: /fill/i });
22
+ if (await fillButton.count() > 0) {
23
+ await fillButton.click();
24
+ await expect(page.locator('.statusbar')).toContainText('fill');
25
+ }
26
+ });
27
+ });
@@ -0,0 +1,11 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ test.describe('Undo/Redo', () => {
4
+ test('should support Ctrl+Z for undo', async ({ page }) => {
5
+ await page.goto('/');
6
+ // This is a basic test -- just verify no crash on undo with empty stack
7
+ await page.keyboard.press('Control+z');
8
+ // App should still be responsive
9
+ await expect(page.locator('canvas').first()).toBeVisible();
10
+ });
11
+ });
@@ -0,0 +1,62 @@
1
+ import js from '@eslint/js';
2
+ import ts from 'typescript-eslint';
3
+ import svelte from 'eslint-plugin-svelte';
4
+ import svelteParser from 'svelte-eslint-parser';
5
+ import globals from 'globals';
6
+
7
+ export default [
8
+ js.configs.recommended,
9
+ ...ts.configs.strictTypeChecked,
10
+ ...svelte.configs['flat/recommended'],
11
+ {
12
+ languageOptions: {
13
+ parserOptions: {
14
+ projectService: true,
15
+ tsconfigRootDir: import.meta.dirname,
16
+ extraFileExtensions: ['.svelte'],
17
+ },
18
+ },
19
+ },
20
+ {
21
+ files: ['**/*.svelte', '**/*.svelte.ts'],
22
+ languageOptions: {
23
+ parser: svelteParser,
24
+ parserOptions: {
25
+ parser: ts.parser,
26
+ projectService: true,
27
+ tsconfigRootDir: import.meta.dirname,
28
+ extraFileExtensions: ['.svelte'],
29
+ },
30
+ },
31
+ },
32
+ {
33
+ languageOptions: {
34
+ globals: {
35
+ ...globals.browser,
36
+ ...globals.node,
37
+ },
38
+ },
39
+ },
40
+ {
41
+ // Honor the leading-underscore convention for intentionally unused symbols.
42
+ // Matches the prevailing _e / _ctx / _params pattern used in callback
43
+ // signatures where arguments are required but unused.
44
+ rules: {
45
+ '@typescript-eslint/no-unused-vars': [
46
+ 'error',
47
+ {
48
+ args: 'all',
49
+ argsIgnorePattern: '^_',
50
+ caughtErrors: 'all',
51
+ caughtErrorsIgnorePattern: '^_',
52
+ destructuredArrayIgnorePattern: '^_',
53
+ varsIgnorePattern: '^_',
54
+ ignoreRestSiblings: true,
55
+ },
56
+ ],
57
+ },
58
+ },
59
+ {
60
+ ignores: ['dist/', 'node_modules/', 'src-tauri/', 'server/', 'scripts/', '*.config.js', '*.config.ts'],
61
+ },
62
+ ];
package/index.html ADDED
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>PixelWeaver</title>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.ts"></script>
12
+ </body>
13
+ </html>
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "pixelweaver",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "vite build",
8
+ "preview": "vite preview",
9
+ "check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json",
10
+ "test": "vitest run",
11
+ "test:watch": "vitest",
12
+ "lint": "eslint .",
13
+ "format": "prettier --write .",
14
+ "test:e2e": "playwright test"
15
+ },
16
+ "devDependencies": {
17
+ "@eslint/js": "^9.24.0",
18
+ "@fontsource-variable/inter": "^5.2.8",
19
+ "@iconify-json/lucide": "^1.2.101",
20
+ "@playwright/test": "^1.59.1",
21
+ "@sveltejs/vite-plugin-svelte": "^7.0.0",
22
+ "@tsconfig/svelte": "^5.0.8",
23
+ "@types/culori": "^4.0.1",
24
+ "@types/node": "^24.12.2",
25
+ "eslint": "^9.24.0",
26
+ "eslint-plugin-svelte": "^3.5.1",
27
+ "globals": "^16.0.0",
28
+ "jsdom": "^29.0.2",
29
+ "prettier": "^3.5.3",
30
+ "prettier-plugin-svelte": "^3.5.1",
31
+ "svelte": "^5.55.1",
32
+ "svelte-check": "^4.4.6",
33
+ "svelte-eslint-parser": "^1.2.0",
34
+ "typescript": "~6.0.2",
35
+ "typescript-eslint": "^8.32.1",
36
+ "unplugin-icons": "^23.0.1",
37
+ "vite": "^8.0.4",
38
+ "vitest": "^3.1.1"
39
+ },
40
+ "dependencies": {
41
+ "culori": "^4.0.2",
42
+ "dockview-core": "^5.2.0",
43
+ "fflate": "^0.8.2"
44
+ },
45
+ "keywords": [
46
+ "rlsbl"
47
+ ]
48
+ }
@@ -0,0 +1,19 @@
1
+ import { defineConfig } from '@playwright/test';
2
+
3
+ export default defineConfig({
4
+ testDir: './e2e',
5
+ fullyParallel: true,
6
+ forbidOnly: !!process.env.CI,
7
+ retries: process.env.CI ? 2 : 0,
8
+ workers: process.env.CI ? 1 : undefined,
9
+ reporter: 'html',
10
+ use: {
11
+ baseURL: 'http://localhost:5173',
12
+ trace: 'on-first-retry',
13
+ },
14
+ webServer: {
15
+ command: 'npm run dev',
16
+ url: 'http://localhost:5173',
17
+ reuseExistingServer: !process.env.CI,
18
+ },
19
+ });
File without changes
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Advanced Fill Tool Plugin -- flood fill with color tolerance.
3
+ *
4
+ * Registers:
5
+ * - Command: `flood_fill_tolerance` (tier: 'frame')
6
+ * - Tool: `fill-tolerance`
7
+ *
8
+ * Extends the basic flood fill by allowing a tolerance threshold.
9
+ * Pixels whose color distance (OKLab) from the target pixel is within
10
+ * the tolerance are included in the fill region.
11
+ */
12
+
13
+ import type { PluginModule } from '../../src/lib/core/plugin-loader.js';
14
+ import type { PixelBuffer } from '../../src/lib/canvas/pixel-buffer.js';
15
+ import type { Point } from './drawing-utils.js';
16
+ import { snapshotPixels, applyPixels, hexToRgba, makeSnapshotUndo } from './drawing-utils.js';
17
+ import { rgbToHex, colorDistance } from '../../src/lib/color/color-utils.js';
18
+ import AdvFillIcon from '~icons/lucide/wand';
19
+
20
+ /**
21
+ * Flood fill with color tolerance.
22
+ * Returns all connected pixels whose color distance from the starting pixel
23
+ * is <= tolerance (0-1 range, where 0 = exact match, 1 = any color).
24
+ */
25
+ export function floodFillTolerance(
26
+ buffer: PixelBuffer,
27
+ startX: number,
28
+ startY: number,
29
+ tolerance: number,
30
+ ): Point[] {
31
+ if (!buffer.inBounds(startX, startY)) return [];
32
+
33
+ const [tr, tg, tb, ta] = buffer.getPixel(startX, startY);
34
+ const targetHex = rgbToHex(tr, tg, tb);
35
+ const targetIsTransparent = ta === 0;
36
+ const visited = new Set<number>();
37
+ const result: Point[] = [];
38
+ const queue: Point[] = [{ x: startX, y: startY }];
39
+ const w = buffer.width;
40
+
41
+ visited.add(startY * w + startX);
42
+
43
+ while (queue.length > 0) {
44
+ const next = queue.shift();
45
+ if (!next) break;
46
+ const { x, y } = next;
47
+ result.push({ x, y });
48
+
49
+ const neighbors: Point[] = [
50
+ { x: x - 1, y },
51
+ { x: x + 1, y },
52
+ { x, y: y - 1 },
53
+ { x, y: y + 1 },
54
+ ];
55
+
56
+ for (const n of neighbors) {
57
+ if (!buffer.inBounds(n.x, n.y)) continue;
58
+ const key = n.y * w + n.x;
59
+ if (visited.has(key)) continue;
60
+ visited.add(key);
61
+
62
+ const [r, g, b, a] = buffer.getPixel(n.x, n.y);
63
+
64
+ // Handle transparency matching: transparent pixels only match other transparent pixels
65
+ if (targetIsTransparent) {
66
+ if (a === 0) queue.push(n);
67
+ continue;
68
+ }
69
+
70
+ // Non-transparent target: check if neighbor is non-transparent and within tolerance
71
+ if (a === 0) continue;
72
+
73
+ const neighborHex = rgbToHex(r, g, b);
74
+ const dist = colorDistance(targetHex, neighborHex);
75
+ if (dist <= tolerance) {
76
+ queue.push(n);
77
+ }
78
+ }
79
+ }
80
+
81
+ return result;
82
+ }
83
+
84
+ export const advancedFillToolPlugin: PluginModule = {
85
+ name: 'builtin/advanced-fill',
86
+ version: '1.0.0',
87
+ dependencies: [],
88
+ register(api) {
89
+ api.addCommand('flood_fill_tolerance', {
90
+ tier: 'frame',
91
+
92
+ execute(params, ctx) {
93
+ const buffer = ctx.getActiveBuffer?.();
94
+ if (!buffer) return;
95
+
96
+ const { r, g, b, a } = hexToRgba(params["color"]);
97
+ const tolerance = params["tolerance"];
98
+ const region = floodFillTolerance(
99
+ buffer,
100
+ params["x"],
101
+ params["y"],
102
+ tolerance,
103
+ );
104
+
105
+ const snapshot = snapshotPixels(buffer, region);
106
+ const fillPixels = region.map((p) => ({ ...p, r, g, b, a }));
107
+ applyPixels(buffer, fillPixels);
108
+ return snapshot;
109
+ },
110
+
111
+ undo: makeSnapshotUndo(),
112
+
113
+ describe(params) {
114
+ return `Flood fill at (${String(params["x"])}, ${String(params["y"])}) with ${String(params["color"])} tolerance=${String(params["tolerance"])}`;
115
+ },
116
+ });
117
+
118
+ api.addToolbarItem('toolbar:drawing-tools:fill-tolerance', {
119
+ toolbarId: 'drawing-tools',
120
+ kind: 'tool',
121
+ targetId: 'fill-tolerance',
122
+ group: 'advanced',
123
+ order: 10,
124
+ });
125
+
126
+ api.addTool('fill-tolerance', {
127
+ icon: AdvFillIcon,
128
+ cursor: 'crosshair',
129
+
130
+ onPointerDown(_e, ctx) {
131
+ ctx.api.dispatch({
132
+ type: 'flood_fill_tolerance',
133
+ plugin: 'builtin/advanced-fill',
134
+ version: '1.0.0',
135
+ params: {
136
+ x: ctx.canvasX,
137
+ y: ctx.canvasY,
138
+ color: ctx.color,
139
+ tolerance: 0.1,
140
+ layerId: '',
141
+ },
142
+ });
143
+ },
144
+ });
145
+ },
146
+ };
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Circle/Ellipse Tool Plugin -- draws ellipses and circles.
3
+ *
4
+ * Registers:
5
+ * - Command: `draw_ellipse` (tier: 'frame')
6
+ * - Tool: `ellipse`
7
+ *
8
+ * Click-drag defines the bounding box. Shift constrains to a perfect circle
9
+ * (handled at the viewport level; the command just receives cx/cy/rx/ry).
10
+ * Supports outline and filled modes via the `filled` param.
11
+ */
12
+
13
+ import type { PluginModule } from '../../src/lib/core/plugin-loader.js';
14
+ import {
15
+ ellipseOutline,
16
+ ellipseFilled,
17
+ snapshotPixels,
18
+ applyPixels,
19
+ hexToRgba,
20
+ makeSnapshotUndo,
21
+ } from './drawing-utils.js';
22
+ import { getToolOptionValue } from '../../src/lib/core/tool-options-state.svelte.js';
23
+ import { setShapePreview, clearShapePreview } from '../../src/lib/canvas/shape-preview-state.svelte.js';
24
+ import CircleIcon from '~icons/lucide/circle';
25
+ import PaintBucketIcon from '~icons/lucide/paint-bucket';
26
+
27
+ export const circleToolPlugin: PluginModule = {
28
+ name: 'builtin/circle',
29
+ version: '1.0.0',
30
+ dependencies: [],
31
+ register(api) {
32
+ api.addCommand('draw_ellipse', {
33
+ tier: 'frame',
34
+ icon: CircleIcon,
35
+
36
+ execute(params, ctx) {
37
+ const buffer = ctx.getActiveBuffer?.();
38
+ if (!buffer) return;
39
+
40
+ const cx = params["cx"];
41
+ const cy = params["cy"];
42
+ const rx = params["rx"];
43
+ const ry = params["ry"];
44
+ const filled = params["filled"];
45
+ const { r, g, b, a } = hexToRgba(params["color"]);
46
+
47
+ const pointsFn = filled ? ellipseFilled : ellipseOutline;
48
+ const points = pointsFn(cx, cy, rx, ry);
49
+
50
+ const snapshot = snapshotPixels(buffer, points);
51
+ const pixels = points.map((p) => ({ ...p, r, g, b, a }));
52
+ applyPixels(buffer, pixels);
53
+ return snapshot;
54
+ },
55
+
56
+ undo: makeSnapshotUndo(),
57
+
58
+ describe(params) {
59
+ const filled = params["filled"] ? 'filled ' : '';
60
+ return `Drew ${filled}ellipse at (${String(params["cx"])},${String(params["cy"])}) rx=${String(params["rx"])} ry=${String(params["ry"])}`;
61
+ },
62
+ });
63
+
64
+ api.addToolbarItem('toolbar:drawing-tools:ellipse', {
65
+ toolbarId: 'drawing-tools',
66
+ kind: 'tool',
67
+ targetId: 'ellipse',
68
+ group: 'shapes',
69
+ order: 20,
70
+ });
71
+
72
+ // -- Stroke state --
73
+ let drawing = false;
74
+ let startX = 0;
75
+ let startY = 0;
76
+ let color = '';
77
+
78
+ api.addTool('ellipse', {
79
+ icon: CircleIcon,
80
+ cursor: 'crosshair',
81
+ options: [
82
+ { id: 'filled', label: 'Filled', icon: PaintBucketIcon, type: 'toggle', defaultValue: false },
83
+ ],
84
+
85
+ onPointerDown(_e, ctx) {
86
+ drawing = true;
87
+ startX = ctx.canvasX;
88
+ startY = ctx.canvasY;
89
+ color = ctx.color;
90
+ },
91
+
92
+ onPointerMove(e, ctx) {
93
+ if (!drawing) return;
94
+
95
+ // Shift constrains to perfect circle (equal radii)
96
+ let endX = ctx.canvasX;
97
+ let endY = ctx.canvasY;
98
+ if (e.shiftKey) {
99
+ const maxDim = Math.max(Math.abs(endX - startX), Math.abs(endY - startY));
100
+ endX = startX + maxDim * (Math.sign(endX - startX) || 1);
101
+ endY = startY + maxDim * (Math.sign(endY - startY) || 1);
102
+ }
103
+
104
+ // Ctrl draws from center: start becomes center, mirror endpoint
105
+ let previewStartX = startX;
106
+ let previewStartY = startY;
107
+ let previewEndX = endX;
108
+ let previewEndY = endY;
109
+ if (e.ctrlKey) {
110
+ const dx = endX - startX;
111
+ const dy = endY - startY;
112
+ previewStartX = startX - dx;
113
+ previewStartY = startY - dy;
114
+ previewEndX = startX + dx;
115
+ previewEndY = startY + dy;
116
+ }
117
+
118
+ setShapePreview({
119
+ type: 'ellipse',
120
+ startX: previewStartX,
121
+ startY: previewStartY,
122
+ endX: previewEndX,
123
+ endY: previewEndY,
124
+ color,
125
+ filled: getToolOptionValue('ellipse', 'filled') as boolean,
126
+ });
127
+ },
128
+
129
+ onPointerUp(e, ctx) {
130
+ if (!drawing) return;
131
+ drawing = false;
132
+ clearShapePreview();
133
+
134
+ // Shift constrains to perfect circle (equal radii)
135
+ let endX = ctx.canvasX;
136
+ let endY = ctx.canvasY;
137
+ if (e.shiftKey) {
138
+ const maxDim = Math.max(Math.abs(endX - startX), Math.abs(endY - startY));
139
+ endX = startX + maxDim * (Math.sign(endX - startX) || 1);
140
+ endY = startY + maxDim * (Math.sign(endY - startY) || 1);
141
+ }
142
+
143
+ // Ctrl draws from center: start becomes center, mirror endpoint
144
+ let finalStartX = startX;
145
+ let finalStartY = startY;
146
+ let finalEndX = endX;
147
+ let finalEndY = endY;
148
+ if (e.ctrlKey) {
149
+ const dx = endX - startX;
150
+ const dy = endY - startY;
151
+ finalStartX = startX - dx;
152
+ finalStartY = startY - dy;
153
+ finalEndX = startX + dx;
154
+ finalEndY = startY + dy;
155
+ }
156
+
157
+ // Bounding box from drag
158
+ const x0 = Math.min(finalStartX, finalEndX);
159
+ const y0 = Math.min(finalStartY, finalEndY);
160
+ const x1 = Math.max(finalStartX, finalEndX);
161
+ const y1 = Math.max(finalStartY, finalEndY);
162
+
163
+ // Derive center and radii from bounding box
164
+ const cx = Math.round((x0 + x1) / 2);
165
+ const cy = Math.round((y0 + y1) / 2);
166
+ const rx = Math.floor((x1 - x0) / 2);
167
+ const ry = Math.floor((y1 - y0) / 2);
168
+
169
+ ctx.api.dispatch({
170
+ type: 'draw_ellipse',
171
+ plugin: 'builtin/circle',
172
+ version: '1.0.0',
173
+ params: {
174
+ cx,
175
+ cy,
176
+ rx,
177
+ ry,
178
+ filled: getToolOptionValue('ellipse', 'filled'),
179
+ color,
180
+ layerId: '',
181
+ },
182
+ });
183
+ },
184
+ });
185
+ },
186
+ };