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,372 @@
1
+ /**
2
+ * Map State -- reactive state for the isometric level editor.
3
+ *
4
+ * Manages a layered map with tile, entity, and collision layers.
5
+ * Uses Svelte 5 runes for reactivity so that UI components automatically
6
+ * update when the map changes.
7
+ */
8
+
9
+ // --- Interfaces ---
10
+
11
+ export interface TilePlacement {
12
+ projectRef: string; // PixelWeaver project name used as tile source
13
+ canvasRef: string; // canvas name within the project
14
+ frameIndex: number; // which frame to display
15
+ height: number; // elevation (integer, half, or continuous)
16
+ }
17
+
18
+ export interface EntityPlacement {
19
+ id: string;
20
+ projectRef: string;
21
+ canvasRef: string;
22
+ x: number; // screen position (free placement)
23
+ y: number;
24
+ height: number;
25
+ properties: Record<string, string>; // key-value metadata
26
+ }
27
+
28
+ export interface CollisionShape {
29
+ id: string;
30
+ type: 'rect' | 'polygon';
31
+ // For rect: [topLeft, bottomRight]; for polygon: ordered vertices
32
+ points: { x: number; y: number }[];
33
+ }
34
+
35
+ export interface MapLayer {
36
+ id: string;
37
+ name: string;
38
+ type: 'tile' | 'entity' | 'collision';
39
+ visible: boolean;
40
+ tiles?: Map<string, TilePlacement>; // "col,row" -> placement (tile layers)
41
+ entities?: EntityPlacement[]; // entity layers
42
+ collisionShapes?: CollisionShape[]; // collision layers
43
+ }
44
+
45
+ /** Serialized form of MapLayer (Maps become plain objects). */
46
+ interface SerializedLayer {
47
+ id: string;
48
+ name: string;
49
+ type: 'tile' | 'entity' | 'collision';
50
+ visible: boolean;
51
+ tiles?: Record<string, TilePlacement>;
52
+ entities?: EntityPlacement[];
53
+ collisionShapes?: CollisionShape[];
54
+ }
55
+
56
+ /** Grid projection mode. Affects rendering and Tiled export orientation. */
57
+ export type GridMode = 'iso' | 'ortho';
58
+
59
+ interface SerializedMapState {
60
+ layers: SerializedLayer[];
61
+ activeLayerId: string;
62
+ gridCols: number;
63
+ gridRows: number;
64
+ tileWidth: number;
65
+ tileHeight: number;
66
+ /** Optional for backward compat with maps saved before activeTile existed. */
67
+ activeTile?: TilePlacement | null;
68
+ /** Optional for backward compat with maps saved before gridMode existed. */
69
+ gridMode?: GridMode;
70
+ }
71
+
72
+ // --- Reactive state ---
73
+
74
+ let layers: MapLayer[] = $state([]);
75
+ let activeLayerId: string = $state('');
76
+ let gridCols: number = $state(20);
77
+ let gridRows: number = $state(20);
78
+ let tileWidth: number = $state(32);
79
+ let tileHeight: number = $state(16);
80
+ // Currently selected tile to paint with. null = nothing selected -> paint is a no-op.
81
+ let activeTile: TilePlacement | null = $state(null);
82
+ // Grid projection: 'iso' = isometric diamond, 'ortho' = axis-aligned rectangles.
83
+ let gridMode: GridMode = $state('iso');
84
+
85
+ // --- Helper: generate a unique ID ---
86
+
87
+ function uid(): string {
88
+ return crypto.randomUUID();
89
+ }
90
+
91
+ // --- Layer key helper ---
92
+
93
+ /** Create the tile lookup key from column and row. */
94
+ function tileKey(col: number, row: number): string {
95
+ return `${String(col)},${String(row)}`;
96
+ }
97
+
98
+ // --- Operations ---
99
+
100
+ function addLayer(name: string, type: MapLayer['type']): MapLayer {
101
+ const layer: MapLayer = {
102
+ id: uid(),
103
+ name,
104
+ type,
105
+ visible: true,
106
+ };
107
+ if (type === 'tile') layer.tiles = new Map();
108
+ if (type === 'entity') layer.entities = [];
109
+ if (type === 'collision') layer.collisionShapes = [];
110
+
111
+ layers = [...layers, layer];
112
+ activeLayerId = layer.id;
113
+ return layer;
114
+ }
115
+
116
+ function removeLayer(id: string): void {
117
+ layers = layers.filter((l) => l.id !== id);
118
+ // If the removed layer was active, switch to the first remaining layer
119
+ if (activeLayerId === id) {
120
+ activeLayerId = layers[0]?.id ?? '';
121
+ }
122
+ }
123
+
124
+ /** Move a layer from one index to another (splice-based reorder). */
125
+ function moveLayer(fromIndex: number, toIndex: number): void {
126
+ if (fromIndex === toIndex) return;
127
+ if (fromIndex < 0 || fromIndex >= layers.length) return;
128
+ if (toIndex < 0 || toIndex >= layers.length) return;
129
+ const copy = [...layers];
130
+ const [removed] = copy.splice(fromIndex, 1);
131
+ copy.splice(toIndex, 0, removed!);
132
+ layers = copy;
133
+ }
134
+
135
+ function findLayer(id: string): MapLayer | undefined {
136
+ return layers.find((l) => l.id === id);
137
+ }
138
+
139
+ function placeTile(layerId: string, col: number, row: number, tile: TilePlacement): void {
140
+ const layer = findLayer(layerId);
141
+ if (!layer || layer.type !== 'tile' || !layer.tiles) return;
142
+ layer.tiles.set(tileKey(col, row), tile);
143
+ }
144
+
145
+ function removeTile(layerId: string, col: number, row: number): void {
146
+ const layer = findLayer(layerId);
147
+ if (!layer || layer.type !== 'tile' || !layer.tiles) return;
148
+ layer.tiles.delete(tileKey(col, row));
149
+ }
150
+
151
+ function addEntity(layerId: string, entity: Omit<EntityPlacement, 'id'>): EntityPlacement {
152
+ const layer = findLayer(layerId);
153
+ if (!layer || layer.type !== 'entity' || !layer.entities) {
154
+ throw new Error(`Layer "${layerId}" is not an entity layer`);
155
+ }
156
+ const placed: EntityPlacement = { ...entity, id: uid() };
157
+ layer.entities.push(placed);
158
+ return placed;
159
+ }
160
+
161
+ /** Re-insert an entity with its original ID (used by undo). */
162
+ function restoreEntity(layerId: string, entity: EntityPlacement): void {
163
+ const layer = findLayer(layerId);
164
+ if (!layer || layer.type !== 'entity' || !layer.entities) return;
165
+ layer.entities.push(entity);
166
+ }
167
+
168
+ function removeEntity(layerId: string, entityId: string): void {
169
+ const layer = findLayer(layerId);
170
+ if (!layer || layer.type !== 'entity' || !layer.entities) return;
171
+ const idx = layer.entities.findIndex((e) => e.id === entityId);
172
+ if (idx !== -1) layer.entities.splice(idx, 1);
173
+ }
174
+
175
+ function updateEntityProperties(
176
+ layerId: string,
177
+ entityId: string,
178
+ properties: Record<string, string>,
179
+ ): void {
180
+ const layer = findLayer(layerId);
181
+ if (!layer || layer.type !== 'entity' || !layer.entities) return;
182
+ const entity = layer.entities.find((e) => e.id === entityId);
183
+ if (entity) {
184
+ entity.properties = { ...entity.properties, ...properties };
185
+ }
186
+ }
187
+
188
+ function addCollisionShape(
189
+ layerId: string,
190
+ shape: Omit<CollisionShape, 'id'>,
191
+ ): CollisionShape {
192
+ const layer = findLayer(layerId);
193
+ if (!layer || layer.type !== 'collision' || !layer.collisionShapes) {
194
+ throw new Error(`Layer "${layerId}" is not a collision layer`);
195
+ }
196
+ const placed: CollisionShape = { ...shape, id: uid() };
197
+ layer.collisionShapes.push(placed);
198
+ return placed;
199
+ }
200
+
201
+ /** Re-insert a collision shape with its original ID (used by undo). */
202
+ function restoreCollisionShape(layerId: string, shape: CollisionShape): void {
203
+ const layer = findLayer(layerId);
204
+ if (!layer || layer.type !== 'collision' || !layer.collisionShapes) return;
205
+ layer.collisionShapes.push(shape);
206
+ }
207
+
208
+ function removeCollisionShape(layerId: string, shapeId: string): void {
209
+ const layer = findLayer(layerId);
210
+ if (!layer || layer.type !== 'collision' || !layer.collisionShapes) return;
211
+ const idx = layer.collisionShapes.findIndex((s) => s.id === shapeId);
212
+ if (idx !== -1) layer.collisionShapes.splice(idx, 1);
213
+ }
214
+
215
+ function moveEntity(layerId: string, entityId: string, x: number, y: number): void {
216
+ const layer = findLayer(layerId);
217
+ if (!layer || layer.type !== 'entity' || !layer.entities) return;
218
+ const entity = layer.entities.find((e) => e.id === entityId);
219
+ if (entity) {
220
+ entity.x = x;
221
+ entity.y = y;
222
+ }
223
+ }
224
+
225
+ function getEntity(layerId: string, entityId: string): EntityPlacement | undefined {
226
+ const layer = findLayer(layerId);
227
+ if (!layer || layer.type !== 'entity' || !layer.entities) return undefined;
228
+ return layer.entities.find((e) => e.id === entityId);
229
+ }
230
+
231
+ function setEntityHeight(layerId: string, entityId: string, height: number): void {
232
+ const layer = findLayer(layerId);
233
+ if (!layer || layer.type !== 'entity' || !layer.entities) return;
234
+ const entity = layer.entities.find((e) => e.id === entityId);
235
+ if (entity) {
236
+ entity.height = height;
237
+ }
238
+ }
239
+
240
+ function updateCollisionShape(
241
+ layerId: string,
242
+ shapeId: string,
243
+ points: { x: number; y: number }[],
244
+ ): void {
245
+ const layer = findLayer(layerId);
246
+ if (!layer || layer.type !== 'collision' || !layer.collisionShapes) return;
247
+ const shape = layer.collisionShapes.find((s) => s.id === shapeId);
248
+ if (shape) {
249
+ shape.points = points;
250
+ }
251
+ }
252
+
253
+ function getCollisionShape(layerId: string, shapeId: string): CollisionShape | undefined {
254
+ const layer = findLayer(layerId);
255
+ if (!layer || layer.type !== 'collision' || !layer.collisionShapes) return undefined;
256
+ return layer.collisionShapes.find((s) => s.id === shapeId);
257
+ }
258
+
259
+ // --- Serialization ---
260
+
261
+ function serialize(): SerializedMapState {
262
+ return {
263
+ layers: layers.map((l) => {
264
+ const serialized: SerializedLayer = {
265
+ id: l.id,
266
+ name: l.name,
267
+ type: l.type,
268
+ visible: l.visible,
269
+ };
270
+ if (l.tiles) {
271
+ serialized.tiles = Object.fromEntries(l.tiles);
272
+ }
273
+ if (l.entities) {
274
+ serialized.entities = [...l.entities];
275
+ }
276
+ if (l.collisionShapes) {
277
+ serialized.collisionShapes = [...l.collisionShapes];
278
+ }
279
+ return serialized;
280
+ }),
281
+ activeLayerId,
282
+ gridCols,
283
+ gridRows,
284
+ tileWidth,
285
+ tileHeight,
286
+ activeTile,
287
+ gridMode,
288
+ };
289
+ }
290
+
291
+ function deserialize(data: SerializedMapState): void {
292
+ layers = data.layers.map((l) => {
293
+ const layer: MapLayer = {
294
+ id: l.id,
295
+ name: l.name,
296
+ type: l.type,
297
+ visible: l.visible,
298
+ };
299
+ if (l.tiles) {
300
+ layer.tiles = new Map(Object.entries(l.tiles));
301
+ }
302
+ if (l.entities) {
303
+ layer.entities = [...l.entities];
304
+ }
305
+ if (l.collisionShapes) {
306
+ layer.collisionShapes = [...l.collisionShapes];
307
+ }
308
+ return layer;
309
+ });
310
+ activeLayerId = data.activeLayerId;
311
+ gridCols = data.gridCols;
312
+ gridRows = data.gridRows;
313
+ tileWidth = data.tileWidth;
314
+ tileHeight = data.tileHeight;
315
+ // Backward compat: older serialized maps may not have these fields.
316
+ activeTile = data.activeTile ?? null;
317
+ gridMode = data.gridMode ?? 'iso';
318
+ }
319
+
320
+ // --- Reset (for tests) ---
321
+
322
+ function _resetForTesting(): void {
323
+ layers = [];
324
+ activeLayerId = '';
325
+ gridCols = 20;
326
+ gridRows = 20;
327
+ tileWidth = 32;
328
+ tileHeight = 16;
329
+ activeTile = null;
330
+ gridMode = 'iso';
331
+ }
332
+
333
+ // --- Public API ---
334
+
335
+ export const mapState = {
336
+ get layers() { return layers; },
337
+ get activeLayerId() { return activeLayerId; },
338
+ set activeLayerId(id: string) { activeLayerId = id; },
339
+ get gridCols() { return gridCols; },
340
+ set gridCols(v: number) { gridCols = v; },
341
+ get gridRows() { return gridRows; },
342
+ set gridRows(v: number) { gridRows = v; },
343
+ get tileWidth() { return tileWidth; },
344
+ set tileWidth(v: number) { tileWidth = v; },
345
+ get tileHeight() { return tileHeight; },
346
+ set tileHeight(v: number) { tileHeight = v; },
347
+ get activeTile() { return activeTile; },
348
+ set activeTile(v: TilePlacement | null) { activeTile = v; },
349
+ get gridMode() { return gridMode; },
350
+ set gridMode(v: GridMode) { gridMode = v; },
351
+
352
+ addLayer,
353
+ removeLayer,
354
+ moveLayer,
355
+ placeTile,
356
+ removeTile,
357
+ addEntity,
358
+ restoreEntity,
359
+ removeEntity,
360
+ updateEntityProperties,
361
+ moveEntity,
362
+ getEntity,
363
+ setEntityHeight,
364
+ addCollisionShape,
365
+ restoreCollisionShape,
366
+ removeCollisionShape,
367
+ updateCollisionShape,
368
+ getCollisionShape,
369
+ serialize,
370
+ deserialize,
371
+ _resetForTesting,
372
+ };
@@ -0,0 +1,243 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { mapState } from './map-state.svelte.js';
3
+
4
+ describe('MapState', () => {
5
+ beforeEach(() => {
6
+ mapState._resetForTesting();
7
+ });
8
+
9
+ describe('layer management', () => {
10
+ it('should add a tile layer with an empty tiles map', () => {
11
+ const layer = mapState.addLayer('Ground', 'tile');
12
+ expect(layer.name).toBe('Ground');
13
+ expect(layer.type).toBe('tile');
14
+ expect(layer.visible).toBe(true);
15
+ expect(layer.tiles).toBeInstanceOf(Map);
16
+ expect(layer.tiles?.size).toBe(0);
17
+ expect(mapState.layers).toHaveLength(1);
18
+ expect(mapState.activeLayerId).toBe(layer.id);
19
+ });
20
+
21
+ it('should add an entity layer with an empty entities array', () => {
22
+ const layer = mapState.addLayer('NPCs', 'entity');
23
+ expect(layer.type).toBe('entity');
24
+ expect(layer.entities).toEqual([]);
25
+ });
26
+
27
+ it('should add a collision layer with an empty shapes array', () => {
28
+ const layer = mapState.addLayer('Walls', 'collision');
29
+ expect(layer.type).toBe('collision');
30
+ expect(layer.collisionShapes).toEqual([]);
31
+ });
32
+
33
+ it('should remove a layer by id', () => {
34
+ const l1 = mapState.addLayer('A', 'tile');
35
+ const l2 = mapState.addLayer('B', 'tile');
36
+ mapState.removeLayer(l1.id);
37
+ expect(mapState.layers).toHaveLength(1);
38
+ expect(mapState.layers[0]?.id).toBe(l2.id);
39
+ });
40
+
41
+ it('should switch active layer when the active one is removed', () => {
42
+ const l1 = mapState.addLayer('A', 'tile');
43
+ mapState.addLayer('B', 'tile');
44
+ // Active is now B (last added)
45
+ mapState.activeLayerId = l1.id;
46
+ mapState.removeLayer(l1.id);
47
+ // Should fallback to the first remaining layer
48
+ expect(mapState.activeLayerId).toBe(mapState.layers[0]?.id);
49
+ });
50
+ });
51
+
52
+ describe('tile operations', () => {
53
+ it('should place and retrieve a tile', () => {
54
+ const layer = mapState.addLayer('Ground', 'tile');
55
+ const placement = {
56
+ projectRef: 'myProject',
57
+ canvasRef: 'grassTile',
58
+ frameIndex: 0,
59
+ height: 0,
60
+ };
61
+ mapState.placeTile(layer.id, 3, 5, placement);
62
+ expect(layer.tiles?.get('3,5')).toEqual(placement);
63
+ });
64
+
65
+ it('should overwrite an existing tile at the same position', () => {
66
+ const layer = mapState.addLayer('Ground', 'tile');
67
+ mapState.placeTile(layer.id, 1, 1, {
68
+ projectRef: 'a', canvasRef: 'b', frameIndex: 0, height: 0,
69
+ });
70
+ mapState.placeTile(layer.id, 1, 1, {
71
+ projectRef: 'c', canvasRef: 'd', frameIndex: 1, height: 2,
72
+ });
73
+ expect(layer.tiles?.get('1,1')?.projectRef).toBe('c');
74
+ });
75
+
76
+ it('should remove a tile', () => {
77
+ const layer = mapState.addLayer('Ground', 'tile');
78
+ mapState.placeTile(layer.id, 2, 3, {
79
+ projectRef: 'p', canvasRef: 'c', frameIndex: 0, height: 0,
80
+ });
81
+ mapState.removeTile(layer.id, 2, 3);
82
+ expect(layer.tiles?.has('2,3')).toBe(false);
83
+ });
84
+ });
85
+
86
+ describe('entity operations', () => {
87
+ // Helper: re-fetch a layer from mapState.layers to get the current proxy state
88
+ function getLayer(id: string) {
89
+ const layer = mapState.layers.find((l) => l.id === id);
90
+ if (!layer) throw new Error(`Layer ${id} not found in test`);
91
+ return layer;
92
+ }
93
+
94
+ it('should add an entity and assign it an id', () => {
95
+ const layer = mapState.addLayer('Entities', 'entity');
96
+ const entity = mapState.addEntity(layer.id, {
97
+ projectRef: 'proj',
98
+ canvasRef: 'hero',
99
+ x: 100,
100
+ y: 200,
101
+ height: 0,
102
+ properties: { name: 'Player' },
103
+ });
104
+ expect(entity.id).toBeDefined();
105
+ expect(entity.x).toBe(100);
106
+ expect(getLayer(layer.id).entities).toHaveLength(1);
107
+ });
108
+
109
+ it('should remove an entity by id', () => {
110
+ const layer = mapState.addLayer('Entities', 'entity');
111
+ const e1 = mapState.addEntity(layer.id, {
112
+ projectRef: 'p', canvasRef: 'c', x: 0, y: 0, height: 0, properties: {},
113
+ });
114
+ const e2 = mapState.addEntity(layer.id, {
115
+ projectRef: 'p', canvasRef: 'c', x: 50, y: 50, height: 0, properties: {},
116
+ });
117
+ mapState.removeEntity(layer.id, e1.id);
118
+ const current = getLayer(layer.id);
119
+ expect(current.entities).toHaveLength(1);
120
+ expect(current.entities?.[0]?.id).toBe(e2.id);
121
+ });
122
+
123
+ it('should update entity properties by merging', () => {
124
+ const layer = mapState.addLayer('Entities', 'entity');
125
+ const entity = mapState.addEntity(layer.id, {
126
+ projectRef: 'p', canvasRef: 'c', x: 0, y: 0, height: 0,
127
+ properties: { name: 'NPC', dialogue: 'Hello' },
128
+ });
129
+ mapState.updateEntityProperties(layer.id, entity.id, { dialogue: 'Goodbye', quest: 'main' });
130
+ const updated = getLayer(layer.id).entities?.find((e) => e.id === entity.id);
131
+ if (!updated) throw new Error('updated entity missing');
132
+ expect(updated.properties).toEqual({
133
+ name: 'NPC',
134
+ dialogue: 'Goodbye',
135
+ quest: 'main',
136
+ });
137
+ });
138
+ });
139
+
140
+ describe('collision shape operations', () => {
141
+ function getLayer(id: string) {
142
+ const layer = mapState.layers.find((l) => l.id === id);
143
+ if (!layer) throw new Error(`Layer ${id} not found in test`);
144
+ return layer;
145
+ }
146
+
147
+ it('should add a rect collision shape', () => {
148
+ const layer = mapState.addLayer('Collision', 'collision');
149
+ const shape = mapState.addCollisionShape(layer.id, {
150
+ type: 'rect',
151
+ points: [{ x: 0, y: 0 }, { x: 32, y: 32 }],
152
+ });
153
+ expect(shape.id).toBeDefined();
154
+ expect(shape.type).toBe('rect');
155
+ expect(getLayer(layer.id).collisionShapes).toHaveLength(1);
156
+ });
157
+
158
+ it('should add a polygon collision shape', () => {
159
+ const layer = mapState.addLayer('Collision', 'collision');
160
+ const shape = mapState.addCollisionShape(layer.id, {
161
+ type: 'polygon',
162
+ points: [{ x: 0, y: 0 }, { x: 16, y: 0 }, { x: 8, y: 16 }],
163
+ });
164
+ expect(shape.type).toBe('polygon');
165
+ expect(shape.points).toHaveLength(3);
166
+ });
167
+
168
+ it('should remove a collision shape by id', () => {
169
+ const layer = mapState.addLayer('Collision', 'collision');
170
+ const s1 = mapState.addCollisionShape(layer.id, {
171
+ type: 'rect', points: [{ x: 0, y: 0 }, { x: 10, y: 10 }],
172
+ });
173
+ mapState.addCollisionShape(layer.id, {
174
+ type: 'rect', points: [{ x: 20, y: 20 }, { x: 30, y: 30 }],
175
+ });
176
+ mapState.removeCollisionShape(layer.id, s1.id);
177
+ expect(getLayer(layer.id).collisionShapes).toHaveLength(1);
178
+ });
179
+ });
180
+
181
+ describe('serialization', () => {
182
+ it('should roundtrip serialize/deserialize with all layer types', () => {
183
+ const tileLayer = mapState.addLayer('Ground', 'tile');
184
+ mapState.placeTile(tileLayer.id, 0, 0, {
185
+ projectRef: 'p', canvasRef: 'grass', frameIndex: 0, height: 0,
186
+ });
187
+ mapState.placeTile(tileLayer.id, 1, 2, {
188
+ projectRef: 'p', canvasRef: 'stone', frameIndex: 1, height: 1,
189
+ });
190
+
191
+ const entityLayer = mapState.addLayer('Entities', 'entity');
192
+ mapState.addEntity(entityLayer.id, {
193
+ projectRef: 'p', canvasRef: 'hero', x: 50, y: 60, height: 0,
194
+ properties: { type: 'player' },
195
+ });
196
+
197
+ const collisionLayer = mapState.addLayer('Collision', 'collision');
198
+ mapState.addCollisionShape(collisionLayer.id, {
199
+ type: 'rect', points: [{ x: 0, y: 0 }, { x: 32, y: 32 }],
200
+ });
201
+
202
+ mapState.gridCols = 30;
203
+ mapState.gridRows = 25;
204
+
205
+ const serialized = mapState.serialize();
206
+
207
+ // Reset and deserialize
208
+ mapState._resetForTesting();
209
+ expect(mapState.layers).toHaveLength(0);
210
+
211
+ mapState.deserialize(serialized);
212
+
213
+ expect(mapState.layers).toHaveLength(3);
214
+ expect(mapState.gridCols).toBe(30);
215
+ expect(mapState.gridRows).toBe(25);
216
+
217
+ // Verify tile data survived
218
+ const restoredTile = mapState.layers[0];
219
+ expect(restoredTile?.tiles).toBeInstanceOf(Map);
220
+ expect(restoredTile?.tiles?.get('0,0')?.canvasRef).toBe('grass');
221
+ expect(restoredTile?.tiles?.get('1,2')?.canvasRef).toBe('stone');
222
+
223
+ // Verify entity data survived
224
+ const restoredEntity = mapState.layers[1];
225
+ expect(restoredEntity?.entities).toHaveLength(1);
226
+ expect(restoredEntity?.entities?.[0]?.properties["type"]).toBe('player');
227
+
228
+ // Verify collision data survived
229
+ const restoredCollision = mapState.layers[2];
230
+ expect(restoredCollision?.collisionShapes).toHaveLength(1);
231
+ expect(restoredCollision?.collisionShapes?.[0]?.type).toBe('rect');
232
+ });
233
+
234
+ it('should produce JSON-serializable output', () => {
235
+ mapState.addLayer('Test', 'tile');
236
+ const serialized = mapState.serialize();
237
+ const json = JSON.stringify(serialized);
238
+ const parsed = JSON.parse(json) as { layers: { name: string }[] };
239
+ expect(parsed.layers).toHaveLength(1);
240
+ expect(parsed.layers[0]?.name).toBe('Test');
241
+ });
242
+ });
243
+ });
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Tile Picker Panel Plugin -- registers TilePickerPanel as a dockable
3
+ * panel in the right sidebar. Shows all tile sources as a selectable
4
+ * grid for the level editor.
5
+ *
6
+ * Discovered by bootstrap via the `*-plugin.ts` glob.
7
+ */
8
+
9
+ import type { PluginModule } from '../core/plugin-loader.js';
10
+ import TilePickerPanel from './TilePickerPanel.svelte';
11
+
12
+ export const tilePickerPanelPlugin: PluginModule = {
13
+ name: 'leveleditor/tile-picker-panel',
14
+ version: '1.0.0',
15
+ dependencies: [],
16
+ register(api) {
17
+ api.addPanel('tile-picker', {
18
+ title: 'Tile Picker',
19
+ component: TilePickerPanel,
20
+ position: 'right',
21
+ minWidth: 120,
22
+ maxWidth: 280,
23
+ });
24
+ },
25
+ };