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,516 @@
1
+ /**
2
+ * Layer Tree -- the single source of truth for layer structure.
3
+ *
4
+ * Reactive Svelte 5 module using $state runes. All layer queries and mutations
5
+ * go through this module. Mutations are designed to be easily wrapped as
6
+ * command execute/undo pairs (but this module does not wire to the dispatcher).
7
+ *
8
+ * Ordering convention: first element = bottom of the stack (drawn first).
9
+ */
10
+
11
+ import type { Layer, BlendMode, LayerTreeState } from './layer-types.js';
12
+ import { createPixelLayer, createGroupLayer } from './layer-types.js';
13
+
14
+ // --- Reactive state ---
15
+
16
+ let layers = $state<Layer[]>([]);
17
+ let activeLayerId = $state<string>('');
18
+
19
+ // --- Internal helpers ---
20
+
21
+ /**
22
+ * Find a layer and its parent array by ID, searching the entire tree.
23
+ * Returns { layer, siblings, index, parentId } or undefined if not found.
24
+ * parentId is the ID of the parent group, or null for root-level layers.
25
+ */
26
+ function findLayerContext(
27
+ id: string,
28
+ searchIn: Layer[] = layers,
29
+ parentId: string | null = null,
30
+ ): { layer: Layer; siblings: Layer[]; index: number; parentId: string | null } | undefined {
31
+ for (let i = 0; i < searchIn.length; i++) {
32
+ const current = searchIn[i];
33
+ if (!current) continue;
34
+ if (current.id === id) {
35
+ return { layer: current, siblings: searchIn, index: i, parentId };
36
+ }
37
+ if (current.type === 'group' && current.children) {
38
+ const found = findLayerContext(id, current.children, current.id);
39
+ if (found) return found;
40
+ }
41
+ }
42
+ return undefined;
43
+ }
44
+
45
+ /**
46
+ * Collect all layers in depth-first order (bottom-to-top within each level).
47
+ */
48
+ function flattenTree(tree: Layer[]): Layer[] {
49
+ const result: Layer[] = [];
50
+ for (const layer of tree) {
51
+ result.push(layer);
52
+ if (layer.type === 'group' && layer.children) {
53
+ result.push(...flattenTree(layer.children));
54
+ }
55
+ }
56
+ return result;
57
+ }
58
+
59
+ /**
60
+ * Find the nearest pixel layer to a given position in the flat list.
61
+ * Used when the active layer is removed.
62
+ */
63
+ function findNearestPixelLayer(removedFlatIndex: number): Layer | undefined {
64
+ const flat = flattenTree(layers).filter((l) => l.type === 'pixel');
65
+ if (flat.length === 0) return undefined;
66
+ // Prefer the layer at the same index, then look backward, then forward
67
+ const idx = Math.min(removedFlatIndex, flat.length - 1);
68
+ return flat[idx];
69
+ }
70
+
71
+ /** Deep clone a layer (and its children if a group). Assigns new IDs throughout. */
72
+ function deepCloneLayer(layer: Layer): Layer {
73
+ const clone: Layer = {
74
+ ...layer,
75
+ id: crypto.randomUUID(),
76
+ };
77
+ if (layer.type === 'group' && layer.children) {
78
+ clone.children = layer.children.map(deepCloneLayer);
79
+ }
80
+ return clone;
81
+ }
82
+
83
+ /** Deep clone a layer preserving its original IDs (for serialization snapshots). */
84
+ function deepSnapshotLayer(layer: Layer): Layer {
85
+ const snapshot: Layer = { ...layer };
86
+ if (layer.type === 'group' && layer.children) {
87
+ snapshot.children = layer.children.map(deepSnapshotLayer);
88
+ }
89
+ return snapshot;
90
+ }
91
+
92
+ // --- Queries ---
93
+
94
+ export function getLayers(): Layer[] {
95
+ return layers;
96
+ }
97
+
98
+ export function getActiveLayerId(): string {
99
+ return activeLayerId;
100
+ }
101
+
102
+ export function getLayer(id: string): Layer | undefined {
103
+ return findLayerContext(id)?.layer;
104
+ }
105
+
106
+ export function getActiveLayer(): Layer | undefined {
107
+ if (!activeLayerId) return undefined;
108
+ return getLayer(activeLayerId);
109
+ }
110
+
111
+ /** Get the parent group of a layer, or undefined if it's at the root. */
112
+ export function getParent(id: string): Layer | undefined {
113
+ const ctx = findLayerContext(id);
114
+ if (!ctx || ctx.parentId === null) return undefined;
115
+ return findLayerContext(ctx.parentId)?.layer;
116
+ }
117
+
118
+ /** Get the path from the root to a layer (inclusive). */
119
+ export function getPath(id: string): Layer[] {
120
+ function search(tree: Layer[], path: Layer[]): Layer[] | undefined {
121
+ for (const layer of tree) {
122
+ const currentPath = [...path, layer];
123
+ if (layer.id === id) return currentPath;
124
+ if (layer.type === 'group' && layer.children) {
125
+ const found = search(layer.children, currentPath);
126
+ if (found) return found;
127
+ }
128
+ }
129
+ return undefined;
130
+ }
131
+ return search(layers, []) ?? [];
132
+ }
133
+
134
+ /** Flatten the entire tree in depth-first order. */
135
+ export function getFlatList(): Layer[] {
136
+ return flattenTree(layers);
137
+ }
138
+
139
+ /** Get all pixel layers (not groups) in depth-first order. */
140
+ export function getAllPixelLayers(): Layer[] {
141
+ return flattenTree(layers).filter((l) => l.type === 'pixel');
142
+ }
143
+
144
+ /** Get visible pixel layers: pixel layers where visible=true AND all ancestor groups are visible. */
145
+ export function getVisiblePixelLayers(): Layer[] {
146
+ const result: Layer[] = [];
147
+
148
+ function walk(tree: Layer[]): void {
149
+ for (const layer of tree) {
150
+ if (!layer.visible) continue;
151
+ if (layer.type === 'pixel') {
152
+ result.push(layer);
153
+ } else if (layer.children) {
154
+ walk(layer.children);
155
+ }
156
+ }
157
+ }
158
+
159
+ walk(layers);
160
+ return result;
161
+ }
162
+
163
+ // --- Mutations ---
164
+
165
+ /**
166
+ * Add a new pixel layer.
167
+ * By default, inserts above the active layer (or at the top if no active layer).
168
+ */
169
+ export function addLayer(
170
+ name: string,
171
+ options?: { parentId?: string; index?: number; id?: string },
172
+ ): Layer {
173
+ const layer = createPixelLayer(name, options?.id);
174
+
175
+ if (options?.parentId) {
176
+ // Insert into a specific group
177
+ const parentCtx = findLayerContext(options.parentId);
178
+ if (!parentCtx || parentCtx.layer.type !== 'group') {
179
+ throw new Error(`Parent "${options.parentId}" is not a group or does not exist.`);
180
+ }
181
+ // Group layers always have a children array (see createGroupLayer).
182
+ // Initialize if somehow missing so callers can push into the live array.
183
+ parentCtx.layer.children ??= [];
184
+ const children = parentCtx.layer.children;
185
+ const idx = options.index ?? children.length;
186
+ children.splice(idx, 0, layer);
187
+ } else if (options?.index !== undefined) {
188
+ layers.splice(options.index, 0, layer);
189
+ } else {
190
+ // Insert above the active layer
191
+ const activeCtx = activeLayerId ? findLayerContext(activeLayerId) : undefined;
192
+ if (activeCtx) {
193
+ activeCtx.siblings.splice(activeCtx.index + 1, 0, layer);
194
+ } else {
195
+ layers.push(layer);
196
+ }
197
+ }
198
+
199
+ activeLayerId = layer.id;
200
+ // Return the proxy version from the tree so callers get reactive references.
201
+ // The layer was just inserted above, so findLayerContext cannot miss it.
202
+ const inserted = findLayerContext(layer.id);
203
+ if (!inserted) throw new Error(`addLayer: inserted layer "${layer.id}" not found`);
204
+ return inserted.layer;
205
+ }
206
+
207
+ /**
208
+ * Add a new group layer.
209
+ * By default, inserts above the active layer (or at the top if no active layer).
210
+ */
211
+ export function addGroup(
212
+ name: string,
213
+ options?: { parentId?: string; index?: number; id?: string },
214
+ ): Layer {
215
+ const group = createGroupLayer(name, options?.id);
216
+
217
+ if (options?.parentId) {
218
+ const parentCtx = findLayerContext(options.parentId);
219
+ if (!parentCtx || parentCtx.layer.type !== 'group') {
220
+ throw new Error(`Parent "${options.parentId}" is not a group or does not exist.`);
221
+ }
222
+ // Group layers always have a children array (see createGroupLayer).
223
+ // Initialize if somehow missing so callers can push into the live array.
224
+ parentCtx.layer.children ??= [];
225
+ const children = parentCtx.layer.children;
226
+ const idx = options.index ?? children.length;
227
+ children.splice(idx, 0, group);
228
+ } else if (options?.index !== undefined) {
229
+ layers.splice(options.index, 0, group);
230
+ } else {
231
+ const activeCtx = activeLayerId ? findLayerContext(activeLayerId) : undefined;
232
+ if (activeCtx) {
233
+ activeCtx.siblings.splice(activeCtx.index + 1, 0, group);
234
+ } else {
235
+ layers.push(group);
236
+ }
237
+ }
238
+
239
+ // Active layer stays on a pixel layer (don't switch to the group)
240
+ // Return the proxy version from the tree so callers get reactive references.
241
+ // The group was just inserted above, so findLayerContext cannot miss it.
242
+ const inserted = findLayerContext(group.id);
243
+ if (!inserted) throw new Error(`addGroup: inserted group "${group.id}" not found`);
244
+ return inserted.layer;
245
+ }
246
+
247
+ /** Remove a layer (or group) by ID. Adjusts active layer if necessary. */
248
+ export function removeLayer(id: string): void {
249
+ const ctx = findLayerContext(id);
250
+ if (!ctx) return;
251
+
252
+ // Record position of the removed layer in the pixel-only flat list,
253
+ // so findNearestPixelLayer can select the closest remaining pixel layer.
254
+ const pixelFlat = flattenTree(layers).filter((l) => l.type === 'pixel');
255
+ const flatIndex = pixelFlat.findIndex((l) => l.id === id);
256
+
257
+ ctx.siblings.splice(ctx.index, 1);
258
+
259
+ // If we removed the active layer, select the nearest remaining pixel layer
260
+ if (activeLayerId === id || (ctx.layer.type === 'group' && containsLayer(ctx.layer, activeLayerId))) {
261
+ const nearest = findNearestPixelLayer(flatIndex >= 0 ? flatIndex : 0);
262
+ activeLayerId = nearest?.id ?? '';
263
+ }
264
+ }
265
+
266
+ /** Check if a group (recursively) contains a layer with the given ID. */
267
+ function containsLayer(group: Layer, targetId: string): boolean {
268
+ if (!group.children) return false;
269
+ for (const child of group.children) {
270
+ if (child.id === targetId) return true;
271
+ if (child.type === 'group' && containsLayer(child, targetId)) return true;
272
+ }
273
+ return false;
274
+ }
275
+
276
+ /** Duplicate a layer (deep clone with new IDs). Inserts the copy directly above the original. */
277
+ export function duplicateLayer(id: string): Layer {
278
+ const ctx = findLayerContext(id);
279
+ if (!ctx) throw new Error(`Layer "${id}" not found.`);
280
+
281
+ const clone = deepCloneLayer(ctx.layer);
282
+ clone.name = `${ctx.layer.name} copy`;
283
+ ctx.siblings.splice(ctx.index + 1, 0, clone);
284
+
285
+ // If the duplicated layer is a pixel layer, select it
286
+ if (clone.type === 'pixel') {
287
+ activeLayerId = clone.id;
288
+ }
289
+
290
+ // Return the proxy version from the tree. The clone was just spliced in above.
291
+ const inserted = findLayerContext(clone.id);
292
+ if (!inserted) throw new Error(`duplicateLayer: cloned layer "${clone.id}" not found`);
293
+ return inserted.layer;
294
+ }
295
+
296
+ /** Move a layer to a new position. newParentId=null means root level. */
297
+ export function moveLayer(id: string, newParentId: string | null, newIndex: number): void {
298
+ const ctx = findLayerContext(id);
299
+ if (!ctx) throw new Error(`Layer "${id}" not found.`);
300
+
301
+ // Remove from current position
302
+ const [removed] = ctx.siblings.splice(ctx.index, 1) as [Layer];
303
+
304
+ // Insert at new position
305
+ if (newParentId) {
306
+ const parentCtx = findLayerContext(newParentId);
307
+ if (!parentCtx || parentCtx.layer.type !== 'group') {
308
+ throw new Error(`Target parent "${newParentId}" is not a group or does not exist.`);
309
+ }
310
+ // Group layers always have a children array (see createGroupLayer).
311
+ // Initialize if somehow missing so callers can push into the live array.
312
+ parentCtx.layer.children ??= [];
313
+ const children = parentCtx.layer.children;
314
+ const clampedIndex = Math.min(newIndex, children.length);
315
+ children.splice(clampedIndex, 0, removed);
316
+ } else {
317
+ const clampedIndex = Math.min(newIndex, layers.length);
318
+ layers.splice(clampedIndex, 0, removed);
319
+ }
320
+ }
321
+
322
+ export function renameLayer(id: string, name: string): void {
323
+ const ctx = findLayerContext(id);
324
+ if (!ctx) throw new Error(`Layer "${id}" not found.`);
325
+ ctx.layer.name = name;
326
+ }
327
+
328
+ export function setVisibility(id: string, visible: boolean): void {
329
+ const ctx = findLayerContext(id);
330
+ if (!ctx) throw new Error(`Layer "${id}" not found.`);
331
+ ctx.layer.visible = visible;
332
+ }
333
+
334
+ export function setOpacity(id: string, opacity: number): void {
335
+ const ctx = findLayerContext(id);
336
+ if (!ctx) throw new Error(`Layer "${id}" not found.`);
337
+ ctx.layer.opacity = Math.max(0, Math.min(100, opacity));
338
+ }
339
+
340
+ export function setBlendMode(id: string, mode: BlendMode): void {
341
+ const ctx = findLayerContext(id);
342
+ if (!ctx) throw new Error(`Layer "${id}" not found.`);
343
+ ctx.layer.blendMode = mode;
344
+ }
345
+
346
+ export function setLocked(id: string, locked: boolean): void {
347
+ const ctx = findLayerContext(id);
348
+ if (!ctx) throw new Error(`Layer "${id}" not found.`);
349
+ ctx.layer.locked = locked;
350
+ }
351
+
352
+ export function setActiveLayer(id: string): void {
353
+ const layer = getLayer(id);
354
+ if (!layer) throw new Error(`Layer "${id}" not found.`);
355
+ // Active layer should be a pixel layer
356
+ if (layer.type !== 'pixel') {
357
+ throw new Error(`Cannot set active layer to group "${id}". Active layer must be a pixel layer.`);
358
+ }
359
+ activeLayerId = id;
360
+ }
361
+
362
+ export function toggleExpanded(id: string): void {
363
+ const ctx = findLayerContext(id);
364
+ if (!ctx) throw new Error(`Layer "${id}" not found.`);
365
+ if (ctx.layer.type !== 'group') return; // no-op for non-groups
366
+ ctx.layer.expanded = !ctx.layer.expanded;
367
+ }
368
+
369
+ /**
370
+ * Merge a pixel layer into the one below it (placeholder logic).
371
+ * The top layer is removed; the bottom layer keeps its position.
372
+ */
373
+ export function mergeDown(id: string): void {
374
+ const ctx = findLayerContext(id);
375
+ if (!ctx) throw new Error(`Layer "${id}" not found.`);
376
+ if (ctx.layer.type !== 'pixel') throw new Error('Can only merge pixel layers.');
377
+ if (ctx.index === 0) throw new Error('No layer below to merge into.');
378
+
379
+ const below = ctx.siblings[ctx.index - 1];
380
+ if (!below || below.type !== 'pixel') throw new Error('Layer below is not a pixel layer.');
381
+
382
+ // Placeholder: actual pixel merging would happen in the compositor.
383
+ // For now, just remove the top layer (the pixels would be merged externally).
384
+ ctx.siblings.splice(ctx.index, 1);
385
+
386
+ if (activeLayerId === id) {
387
+ activeLayerId = below.id;
388
+ }
389
+ }
390
+
391
+ /**
392
+ * Flatten a group into a single pixel layer.
393
+ * Replaces the group with a pixel layer at the same position.
394
+ * Returns the replacement pixel layer so callers can store composited
395
+ * pixel data under its ID.
396
+ */
397
+ export function flattenGroup(id: string): Layer {
398
+ const ctx = findLayerContext(id);
399
+ if (!ctx) throw new Error(`Layer "${id}" not found.`);
400
+ if (ctx.layer.type !== 'group') throw new Error('Can only flatten groups.');
401
+
402
+ const replacement = createPixelLayer(ctx.layer.name);
403
+ replacement.visible = ctx.layer.visible;
404
+ replacement.opacity = ctx.layer.opacity;
405
+ replacement.blendMode = ctx.layer.blendMode;
406
+
407
+ ctx.siblings[ctx.index] = replacement;
408
+
409
+ // If the active layer was inside the group, select the replacement
410
+ if (activeLayerId === id || containsLayer(ctx.layer, activeLayerId)) {
411
+ activeLayerId = replacement.id;
412
+ }
413
+
414
+ return replacement;
415
+ }
416
+
417
+ /**
418
+ * Wrap selected layers in a new group.
419
+ * The layers must all be siblings (same parent). The group takes their position.
420
+ */
421
+ export function groupLayers(ids: string[], groupName: string): Layer {
422
+ if (ids.length === 0) throw new Error('No layers to group.');
423
+
424
+ // All layers must be siblings
425
+ const contexts = ids.map((id) => {
426
+ const ctx = findLayerContext(id);
427
+ if (!ctx) throw new Error(`Layer "${id}" not found.`);
428
+ return ctx;
429
+ });
430
+
431
+ // ids.length > 0 is checked above, so contexts[0] exists.
432
+ const firstCtx = contexts[0];
433
+ if (!firstCtx) throw new Error('groupLayers: empty context list');
434
+ // Verify they share the same parent (compare by parentId, not array reference,
435
+ // because Svelte 5 $state proxies may return different proxy instances)
436
+ const sharedParentId = firstCtx.parentId;
437
+ for (const ctx of contexts) {
438
+ if (ctx.parentId !== sharedParentId) {
439
+ throw new Error('All layers must be siblings (same parent) to group.');
440
+ }
441
+ }
442
+ // Get the actual sibling array from the first context
443
+ const parentArray = firstCtx.siblings;
444
+
445
+ // Sort by index (ascending) to preserve order
446
+ contexts.sort((a, b) => a.index - b.index);
447
+
448
+ // Create the group
449
+ const group = createGroupLayer(groupName);
450
+
451
+ // Remove layers from parent (in reverse order to preserve indices)
452
+ const removedLayers: Layer[] = [];
453
+ for (let i = contexts.length - 1; i >= 0; i--) {
454
+ const ctxI = contexts[i];
455
+ if (!ctxI) continue;
456
+ // Re-find the index by ID since previous removals shift indices,
457
+ // and Svelte proxies make reference-based indexOf unreliable
458
+ const currentIndex = parentArray.findIndex((l) => l.id === ctxI.layer.id);
459
+ if (currentIndex !== -1) {
460
+ const removed = parentArray.splice(currentIndex, 1)[0];
461
+ if (removed) removedLayers.unshift(removed);
462
+ }
463
+ }
464
+
465
+ group.children = removedLayers;
466
+
467
+ // Insert the group at the position of the first (lowest) removed layer
468
+ const insertIndex = Math.min(firstCtx.index, parentArray.length);
469
+ parentArray.splice(insertIndex, 0, group);
470
+
471
+ // Return the proxy version from the tree (just inserted above).
472
+ const inserted = findLayerContext(group.id);
473
+ if (!inserted) throw new Error(`groupLayers: group "${group.id}" not found after insert`);
474
+ return inserted.layer;
475
+ }
476
+
477
+ /**
478
+ * Dissolve a group, moving its children to the group's parent at its position.
479
+ */
480
+ export function ungroupLayer(id: string): void {
481
+ const ctx = findLayerContext(id);
482
+ if (!ctx) throw new Error(`Layer "${id}" not found.`);
483
+ if (ctx.layer.type !== 'group') throw new Error('Can only ungroup groups.');
484
+
485
+ const children = ctx.layer.children ?? [];
486
+
487
+ // Remove the group from its parent
488
+ ctx.siblings.splice(ctx.index, 1);
489
+
490
+ // Insert all children at the group's former position
491
+ ctx.siblings.splice(ctx.index, 0, ...children);
492
+ }
493
+
494
+ // --- Serialization ---
495
+
496
+ /** Export the tree as a plain JSON-safe object. */
497
+ export function serialize(): LayerTreeState {
498
+ return {
499
+ layers: layers.map(deepSnapshotLayer),
500
+ activeLayerId,
501
+ };
502
+ }
503
+
504
+ /** Restore the tree from a serialized state. */
505
+ export function deserialize(state: LayerTreeState): void {
506
+ layers = state.layers.map(deepSnapshotLayer);
507
+ activeLayerId = state.activeLayerId;
508
+ }
509
+
510
+ /**
511
+ * Reset all layer state. Intended for tests only.
512
+ */
513
+ export function _resetForTesting(): void {
514
+ layers = [];
515
+ activeLayerId = '';
516
+ }