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,91 @@
1
+ /**
2
+ * Tile Source Registry -- maps tile reference ids to live PixelBuffer getters.
3
+ *
4
+ * The level editor stores tile placements as abstract `(projectRef, canvasRef,
5
+ * frameIndex)` triples. To render those placements as actual pixels, the
6
+ * editor needs a way to look up the buffer behind each reference. This
7
+ * registry is that bridge: callers (a future tile picker UI, project loader,
8
+ * etc.) register lazy getters keyed by a string id, and the renderer reads
9
+ * the current buffer on every frame so source edits propagate live.
10
+ *
11
+ * Backed by SvelteMap so plugin UIs that iterate the registry stay reactive
12
+ * to register/unregister calls.
13
+ */
14
+ import { SvelteMap } from 'svelte/reactivity';
15
+ import type { PixelBuffer } from '../canvas/pixel-buffer.js';
16
+ import type { TilePlacement } from './map-state.svelte.js';
17
+
18
+ /** Canonical id format -- `"${projectRef}:${canvasRef}:${frameIndex}"`. */
19
+ export function tileRefId(ref: {
20
+ projectRef: string;
21
+ canvasRef: string;
22
+ frameIndex: number;
23
+ }): string {
24
+ return `${ref.projectRef}:${ref.canvasRef}:${String(ref.frameIndex)}`;
25
+ }
26
+
27
+ /** Build the canonical id for a TilePlacement. */
28
+ export function placementToTileRefId(p: TilePlacement): string {
29
+ return tileRefId(p);
30
+ }
31
+
32
+ /** A registered entry: lazy getter so source edits propagate without re-registration. */
33
+ interface TileSourceEntry {
34
+ getBuffer: () => PixelBuffer;
35
+ }
36
+
37
+ // Reactive store. SvelteMap mutations trigger re-renders in any reader.
38
+ const entries = new SvelteMap<string, TileSourceEntry>();
39
+
40
+ export const tileSourceRegistry = {
41
+ /**
42
+ * Register (or replace) a tile source under the given id.
43
+ * The getter is invoked on every render, so it should be cheap and
44
+ * always return the current buffer for that frame.
45
+ */
46
+ register(id: string, getBuffer: () => PixelBuffer): void {
47
+ entries.set(id, { getBuffer });
48
+ },
49
+
50
+ /** Remove a tile source. */
51
+ unregister(id: string): void {
52
+ entries.delete(id);
53
+ },
54
+
55
+ /** Resolve an id to its current PixelBuffer, or null if not registered. */
56
+ get(id: string): PixelBuffer | null {
57
+ const entry = entries.get(id);
58
+ if (!entry) return null;
59
+ try {
60
+ return entry.getBuffer();
61
+ } catch {
62
+ // A getter may throw if the underlying source was destroyed mid-frame.
63
+ // Treat as missing -- the renderer will fall back to its placeholder.
64
+ return null;
65
+ }
66
+ },
67
+
68
+ /** Whether an id is registered. */
69
+ has(id: string): boolean {
70
+ return entries.has(id);
71
+ },
72
+
73
+ /** All registered ids. Useful for a future tile picker UI. */
74
+ list(): string[] {
75
+ return Array.from(entries.keys());
76
+ },
77
+
78
+ /** Reactive read of the underlying map (for tile picker iteration). */
79
+ getAll(): ReadonlyMap<string, TileSourceEntry> {
80
+ return entries;
81
+ },
82
+
83
+ get size(): number {
84
+ return entries.size;
85
+ },
86
+
87
+ /** Test helper -- clears every entry. */
88
+ _resetForTesting(): void {
89
+ entries.clear();
90
+ },
91
+ };
@@ -0,0 +1,144 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { mapState } from './map-state.svelte.js';
3
+ import { exportTiledMap } from './tiled-export.js';
4
+
5
+ describe('tiled-export', () => {
6
+ beforeEach(() => {
7
+ mapState._resetForTesting();
8
+ mapState.gridCols = 10;
9
+ mapState.gridRows = 10;
10
+ mapState.tileWidth = 32;
11
+ mapState.tileHeight = 16;
12
+ });
13
+
14
+ it('should produce a valid TiledMap structure', () => {
15
+ const result = exportTiledMap(mapState, null);
16
+
17
+ expect(result.type).toBe('map');
18
+ expect(result.version).toBe('1.10');
19
+ expect(result.orientation).toBe('isometric');
20
+ expect(result.renderorder).toBe('right-down');
21
+ expect(result.width).toBe(10);
22
+ expect(result.height).toBe(10);
23
+ expect(result.tilewidth).toBe(32);
24
+ expect(result.tileheight).toBe(16);
25
+ expect(result.layers).toEqual([]);
26
+ expect(result.tilesets).toEqual([]);
27
+ });
28
+
29
+ it('should export tile layers with correct GID data', () => {
30
+ const layer = mapState.addLayer('Ground', 'tile');
31
+ mapState.placeTile(layer.id, 0, 0, {
32
+ projectRef: 'proj', canvasRef: 'grass', frameIndex: 0, height: 0,
33
+ });
34
+ mapState.placeTile(layer.id, 2, 1, {
35
+ projectRef: 'proj', canvasRef: 'grass', frameIndex: 0, height: 0,
36
+ });
37
+
38
+ const result = exportTiledMap(mapState, null);
39
+
40
+ expect(result.layers).toHaveLength(1);
41
+ const tiledLayer = result.layers[0];
42
+ if (!tiledLayer) throw new Error("missing layer");
43
+ expect(tiledLayer.type).toBe('tilelayer');
44
+ expect(tiledLayer.name).toBe('Ground');
45
+ expect(tiledLayer.data).toBeDefined();
46
+ expect(tiledLayer.data?.length).toBe(100); // 10x10
47
+
48
+ // GID 1 should be at position [0,0] (index 0) and [2,1] (index 12)
49
+ expect(tiledLayer.data?.[0]).toBe(1);
50
+ expect(tiledLayer.data?.[12]).toBe(1);
51
+ // Empty tile should be 0
52
+ expect(tiledLayer.data?.[1]).toBe(0);
53
+
54
+ // Tileset should be created (legacy path uses registry id format)
55
+ expect(result.tilesets).toHaveLength(1);
56
+ expect(result.tilesets[0]?.name).toBe('proj:grass:0');
57
+ expect(result.tilesets[0]?.firstgid).toBe(1);
58
+ });
59
+
60
+ it('should export entity layers as object groups', () => {
61
+ const layer = mapState.addLayer('NPCs', 'entity');
62
+ mapState.addEntity(layer.id, {
63
+ projectRef: 'proj',
64
+ canvasRef: 'hero',
65
+ x: 100,
66
+ y: 200,
67
+ height: 0,
68
+ properties: { name: 'Player', health: '100' },
69
+ });
70
+
71
+ const result = exportTiledMap(mapState, null);
72
+
73
+ expect(result.layers).toHaveLength(1);
74
+ const tiledLayer = result.layers[0];
75
+ if (!tiledLayer) throw new Error("missing layer");
76
+ expect(tiledLayer.type).toBe('objectgroup');
77
+ expect(tiledLayer.name).toBe('NPCs');
78
+ expect(tiledLayer.objects).toHaveLength(1);
79
+
80
+ const obj = tiledLayer.objects?.[0];
81
+ if (!obj) throw new Error("missing obj");
82
+ expect(obj.x).toBe(100);
83
+ expect(obj.y).toBe(200);
84
+ expect(obj.properties).toBeDefined();
85
+ expect(obj.properties).toHaveLength(2);
86
+ expect(obj.properties?.find((p) => p.name === 'name')?.value).toBe('Player');
87
+ });
88
+
89
+ it('should export collision shapes as object groups', () => {
90
+ const layer = mapState.addLayer('Collision', 'collision');
91
+ mapState.addCollisionShape(layer.id, {
92
+ type: 'rect',
93
+ points: [{ x: 10, y: 20 }, { x: 42, y: 52 }],
94
+ });
95
+ mapState.addCollisionShape(layer.id, {
96
+ type: 'polygon',
97
+ points: [{ x: 0, y: 0 }, { x: 16, y: 0 }, { x: 8, y: 16 }],
98
+ });
99
+
100
+ const result = exportTiledMap(mapState, null);
101
+
102
+ expect(result.layers).toHaveLength(1);
103
+ const tiledLayer = result.layers[0];
104
+ if (!tiledLayer) throw new Error("missing layer");
105
+ expect(tiledLayer.type).toBe('objectgroup');
106
+ expect(tiledLayer.objects).toHaveLength(2);
107
+
108
+ // Rect
109
+ const rect = tiledLayer.objects?.[0];
110
+ if (!rect) throw new Error("missing rect");
111
+ expect(rect.x).toBe(10);
112
+ expect(rect.y).toBe(20);
113
+ expect(rect.width).toBe(32);
114
+ expect(rect.height).toBe(32);
115
+ expect(rect.type).toBe('collision');
116
+
117
+ // Polygon
118
+ const poly = tiledLayer.objects?.[1];
119
+ if (!poly) throw new Error("missing poly");
120
+ expect(poly.polygon).toBeDefined();
121
+ expect(poly.polygon).toHaveLength(3);
122
+ // First point should be (0,0) since anchor is subtracted
123
+ expect(poly.polygon?.[0]).toEqual({ x: 0, y: 0 });
124
+ });
125
+
126
+ it('should create separate tilesets for different tile sources', () => {
127
+ const layer = mapState.addLayer('Ground', 'tile');
128
+ mapState.placeTile(layer.id, 0, 0, {
129
+ projectRef: 'proj', canvasRef: 'grass', frameIndex: 0, height: 0,
130
+ });
131
+ mapState.placeTile(layer.id, 1, 0, {
132
+ projectRef: 'proj', canvasRef: 'stone', frameIndex: 0, height: 0,
133
+ });
134
+
135
+ const result = exportTiledMap(mapState, null);
136
+
137
+ expect(result.tilesets).toHaveLength(2);
138
+ expect(result.tilesets[0]?.firstgid).toBe(1);
139
+ expect(result.tilesets[1]?.firstgid).toBe(2);
140
+ // Tiles should have different GIDs
141
+ expect(result.layers[0]?.data?.[0]).toBe(1); // grass
142
+ expect(result.layers[0]?.data?.[1]).toBe(2); // stone
143
+ });
144
+ });
@@ -0,0 +1,374 @@
1
+ /**
2
+ * Tiled Export -- converts map state to the Tiled JSON format (.tmj).
3
+ *
4
+ * Produces a Tiled-compatible JSON document that can be opened in the
5
+ * Tiled map editor. Tile layers become Tiled tile layers with GID references,
6
+ * entity layers become object layers, and collision layers become object layers
7
+ * with shape objects.
8
+ *
9
+ * Reference: https://doc.mapeditor.org/en/stable/reference/json-map-format/
10
+ */
11
+
12
+ import type { mapState } from './map-state.svelte.js';
13
+ import { PixelBuffer } from '../canvas/pixel-buffer.js';
14
+ import { tileSourceRegistry, placementToTileRefId } from './tile-source-registry.svelte.js';
15
+
16
+ // --- Tiled JSON format types ---
17
+
18
+ export interface TiledProperty {
19
+ name: string;
20
+ type: string;
21
+ value: string | number | boolean;
22
+ }
23
+
24
+ export interface TiledObject {
25
+ id: number;
26
+ name: string;
27
+ type: string;
28
+ x: number;
29
+ y: number;
30
+ width: number;
31
+ height: number;
32
+ rotation: number;
33
+ visible: boolean;
34
+ properties?: TiledProperty[];
35
+ polygon?: { x: number; y: number }[];
36
+ }
37
+
38
+ export interface TiledLayer {
39
+ id: number;
40
+ name: string;
41
+ type: 'tilelayer' | 'objectgroup';
42
+ visible: boolean;
43
+ opacity: number;
44
+ x: number;
45
+ y: number;
46
+ width?: number;
47
+ height?: number;
48
+ data?: number[];
49
+ objects?: TiledObject[];
50
+ }
51
+
52
+ export interface TiledTileset {
53
+ firstgid: number;
54
+ name: string;
55
+ tilewidth: number;
56
+ tileheight: number;
57
+ tilecount: number;
58
+ columns: number;
59
+ image?: string;
60
+ imagewidth?: number;
61
+ imageheight?: number;
62
+ }
63
+
64
+ export interface TiledMap {
65
+ type: 'map';
66
+ version: '1.10';
67
+ tiledversion: string;
68
+ orientation: 'isometric' | 'orthogonal';
69
+ renderorder: 'right-down';
70
+ width: number;
71
+ height: number;
72
+ tilewidth: number;
73
+ tileheight: number;
74
+ layers: TiledLayer[];
75
+ tilesets: TiledTileset[];
76
+ properties?: TiledProperty[];
77
+ }
78
+
79
+ /** Result of the tileset spritesheet builder. */
80
+ export interface TilesetSpritesheetResult {
81
+ /** The combined spritesheet image (horizontal strip). */
82
+ spritesheet: PixelBuffer;
83
+ /** Maps each unique tile ref key to its 1-based GID in the tileset. */
84
+ gidMap: Map<string, number>;
85
+ /** Number of unique tiles in the spritesheet. */
86
+ tileCount: number;
87
+ }
88
+
89
+ /**
90
+ * Collect all unique tile ref keys from tile layers and assign sequential GIDs.
91
+ * The ref key format is "projectRef:canvasRef:frameIndex" matching the
92
+ * tile source registry's id format (via placementToTileRefId).
93
+ */
94
+ function collectUniqueTileRefs(state: typeof mapState): string[] {
95
+ const seen = new Set<string>();
96
+ const ordered: string[] = [];
97
+ for (const layer of state.layers) {
98
+ if (layer.type === 'tile' && layer.tiles) {
99
+ for (const placement of layer.tiles.values()) {
100
+ const key = placementToTileRefId(placement);
101
+ if (!seen.has(key)) {
102
+ seen.add(key);
103
+ ordered.push(key);
104
+ }
105
+ }
106
+ }
107
+ }
108
+ return ordered;
109
+ }
110
+
111
+ /**
112
+ * Build a horizontal-strip tileset spritesheet from the unique tiles in the map.
113
+ *
114
+ * Each tile's PixelBuffer is fetched from the tile source registry and placed
115
+ * side-by-side in a single row. Tiles that can't be resolved (missing from the
116
+ * registry) get a transparent slot so GID indices remain stable.
117
+ *
118
+ * @returns null if the map has no tile placements.
119
+ */
120
+ export function buildTilesetSpritesheet(
121
+ state: typeof mapState,
122
+ ): TilesetSpritesheetResult | null {
123
+ const refs = collectUniqueTileRefs(state);
124
+ if (refs.length === 0) return null;
125
+
126
+ const tw = state.tileWidth;
127
+ const th = state.tileHeight;
128
+
129
+ // GID 1-based: index 0 in the strip -> GID 1
130
+ const gidMap = new Map<string, number>();
131
+ const buffers: (PixelBuffer | null)[] = [];
132
+
133
+ for (let i = 0; i < refs.length; i++) {
134
+ const key = refs[i];
135
+ if (key === undefined) continue;
136
+ gidMap.set(key, i + 1);
137
+ buffers.push(tileSourceRegistry.get(key));
138
+ }
139
+
140
+ // Compose horizontal strip
141
+ const sheetWidth = tw * refs.length;
142
+ const sheetHeight = th;
143
+ const spritesheet = new PixelBuffer(sheetWidth, sheetHeight);
144
+
145
+ for (let i = 0; i < buffers.length; i++) {
146
+ const buf = buffers[i];
147
+ if (!buf) continue;
148
+ // Paste each tile at its horizontal slot; paste handles clipping
149
+ spritesheet.paste(buf, i * tw, 0);
150
+ }
151
+
152
+ return { spritesheet, gidMap, tileCount: refs.length };
153
+ }
154
+
155
+ /**
156
+ * Export the map state as a Tiled-compatible JSON document (.tmj).
157
+ *
158
+ * When spritesheetResult is provided, the exported map references a single
159
+ * tileset backed by the "tileset.png" spritesheet image. Without it, the
160
+ * export falls back to one imageless tileset per unique tile (legacy behavior
161
+ * that can't render in Tiled without manual image linking).
162
+ *
163
+ * @param state - The current map state (from mapState)
164
+ * @param spritesheetResult - Output of buildTilesetSpritesheet(); pass null to
165
+ * get the legacy imageless export.
166
+ * @returns A TiledMap object ready for JSON.stringify()
167
+ */
168
+ export function exportTiledMap(
169
+ state: typeof mapState,
170
+ spritesheetResult: TilesetSpritesheetResult | null,
171
+ ): TiledMap {
172
+ // --- Build tileset(s) and GID lookup ---
173
+
174
+ let gidMap: Map<string, number>;
175
+ let tilesets: TiledTileset[];
176
+
177
+ if (spritesheetResult) {
178
+ // Single tileset referencing the spritesheet image
179
+ gidMap = spritesheetResult.gidMap;
180
+ const tw = state.tileWidth;
181
+ const th = state.tileHeight;
182
+ tilesets = [
183
+ {
184
+ firstgid: 1,
185
+ name: 'tileset',
186
+ tilewidth: tw,
187
+ tileheight: th,
188
+ tilecount: spritesheetResult.tileCount,
189
+ columns: spritesheetResult.tileCount,
190
+ image: 'tileset.png',
191
+ imagewidth: tw * spritesheetResult.tileCount,
192
+ imageheight: th,
193
+ },
194
+ ];
195
+ } else {
196
+ // Legacy fallback: one tileset per unique tile, no image field.
197
+ gidMap = new Map();
198
+ tilesets = [];
199
+ let nextGid = 1;
200
+ for (const layer of state.layers) {
201
+ if (layer.type === 'tile' && layer.tiles) {
202
+ for (const placement of layer.tiles.values()) {
203
+ const key = placementToTileRefId(placement);
204
+ if (!gidMap.has(key)) {
205
+ gidMap.set(key, nextGid);
206
+ tilesets.push({
207
+ firstgid: nextGid,
208
+ name: key,
209
+ tilewidth: state.tileWidth,
210
+ tileheight: state.tileHeight,
211
+ tilecount: 1,
212
+ columns: 1,
213
+ });
214
+ nextGid++;
215
+ }
216
+ }
217
+ }
218
+ }
219
+ }
220
+
221
+ // --- Build layers ---
222
+ let layerId = 1;
223
+ const tiledLayers: TiledLayer[] = [];
224
+
225
+ for (const layer of state.layers) {
226
+ if (layer.type === 'tile') {
227
+ // Tile layer: produce a flat data array of GIDs, row-major.
228
+ // GID 0 means empty tile.
229
+ const data = new Array<number>(state.gridCols * state.gridRows).fill(0);
230
+ if (layer.tiles) {
231
+ for (const [key, placement] of layer.tiles.entries()) {
232
+ const [colStr, rowStr] = key.split(',') as [string, string];
233
+ const col = parseInt(colStr, 10);
234
+ const row = parseInt(rowStr, 10);
235
+ if (col >= 0 && col < state.gridCols && row >= 0 && row < state.gridRows) {
236
+ const ref = placementToTileRefId(placement);
237
+ const gid = gidMap.get(ref) ?? 0;
238
+ data[row * state.gridCols + col] = gid;
239
+ }
240
+ }
241
+ }
242
+ tiledLayers.push({
243
+ id: layerId++,
244
+ name: layer.name,
245
+ type: 'tilelayer',
246
+ visible: layer.visible,
247
+ opacity: 1,
248
+ x: 0,
249
+ y: 0,
250
+ width: state.gridCols,
251
+ height: state.gridRows,
252
+ data,
253
+ });
254
+ } else if (layer.type === 'entity') {
255
+ // Entity layer: becomes an object group
256
+ let objectId = 1;
257
+ const objects: TiledObject[] = (layer.entities ?? []).map((entity) => {
258
+ const obj: TiledObject = {
259
+ id: objectId++,
260
+ name: entity.projectRef,
261
+ type: entity.canvasRef,
262
+ x: entity.x,
263
+ y: entity.y,
264
+ width: 0,
265
+ height: 0,
266
+ rotation: 0,
267
+ visible: true,
268
+ };
269
+ // Convert entity properties to Tiled properties
270
+ const entries = Object.entries(entity.properties);
271
+ if (entries.length > 0) {
272
+ obj.properties = entries.map(([name, value]) => ({
273
+ name,
274
+ type: 'string',
275
+ value,
276
+ }));
277
+ }
278
+ return obj;
279
+ });
280
+ tiledLayers.push({
281
+ id: layerId++,
282
+ name: layer.name,
283
+ type: 'objectgroup',
284
+ visible: layer.visible,
285
+ opacity: 1,
286
+ x: 0,
287
+ y: 0,
288
+ objects,
289
+ });
290
+ } else {
291
+ // layer.type === 'collision' -- the only remaining case.
292
+ // Collision layer: becomes an object group with shape objects
293
+ let objectId = 1;
294
+ const objects: TiledObject[] = (layer.collisionShapes ?? []).flatMap((shape) => {
295
+ if (shape.type === 'rect' && shape.points.length >= 2) {
296
+ // length >= 2 guard ensures indices 0 and 1 exist
297
+ const topLeft = shape.points[0];
298
+ const bottomRight = shape.points[1];
299
+ if (!topLeft || !bottomRight) return [];
300
+ return {
301
+ id: objectId++,
302
+ name: '',
303
+ type: 'collision',
304
+ x: topLeft.x,
305
+ y: topLeft.y,
306
+ width: bottomRight.x - topLeft.x,
307
+ height: bottomRight.y - topLeft.y,
308
+ rotation: 0,
309
+ visible: true,
310
+ };
311
+ } else {
312
+ // Polygon: first point is the anchor, remaining are relative offsets
313
+ const anchor = shape.points[0] ?? { x: 0, y: 0 };
314
+ const polygon = shape.points.map((p) => ({
315
+ x: p.x - anchor.x,
316
+ y: p.y - anchor.y,
317
+ }));
318
+ return {
319
+ id: objectId++,
320
+ name: '',
321
+ type: 'collision',
322
+ x: anchor.x,
323
+ y: anchor.y,
324
+ width: 0,
325
+ height: 0,
326
+ rotation: 0,
327
+ visible: true,
328
+ polygon,
329
+ };
330
+ }
331
+ });
332
+ tiledLayers.push({
333
+ id: layerId++,
334
+ name: layer.name,
335
+ type: 'objectgroup',
336
+ visible: layer.visible,
337
+ opacity: 1,
338
+ x: 0,
339
+ y: 0,
340
+ objects,
341
+ });
342
+ }
343
+ }
344
+
345
+ // Tiled uses 'orthogonal' for axis-aligned grids and 'isometric' for diamonds.
346
+ const orientation: TiledMap['orientation'] =
347
+ state.gridMode === 'ortho' ? 'orthogonal' : 'isometric';
348
+
349
+ return {
350
+ type: 'map',
351
+ version: '1.10',
352
+ tiledversion: '1.10.2',
353
+ orientation,
354
+ renderorder: 'right-down',
355
+ width: state.gridCols,
356
+ height: state.gridRows,
357
+ tilewidth: state.tileWidth,
358
+ tileheight: state.tileHeight,
359
+ layers: tiledLayers,
360
+ tilesets,
361
+ };
362
+ }
363
+
364
+ /**
365
+ * Convert a PixelBuffer to a PNG Blob via OffscreenCanvas.
366
+ * Used to export the tileset spritesheet alongside the .tmj file.
367
+ */
368
+ export function pixelBufferToPngBlob(buffer: PixelBuffer): Promise<Blob> {
369
+ const canvas = new OffscreenCanvas(buffer.width, buffer.height);
370
+ const ctx = canvas.getContext('2d');
371
+ if (!ctx) return Promise.reject(new Error('Failed to get 2d context'));
372
+ ctx.putImageData(buffer.toImageData(), 0, 0);
373
+ return canvas.convertToBlob({ type: 'image/png' });
374
+ }
@@ -0,0 +1,15 @@
1
+ interface DesktopBridgeApi {
2
+ save_file(data_b64: string, default_name: string, filter_name: string, extensions: string[]): Promise<string | null>;
3
+ open_file(filter_name: string, extensions: string[]): Promise<{ path: string; data: string } | null>;
4
+ write_file(path: string, data_b64: string): Promise<boolean>;
5
+ pick_directory(): Promise<string | null>;
6
+ write_files_to_directory(dir_path: string, files: Array<{ name: string; data: string }>): Promise<boolean>;
7
+ }
8
+
9
+ interface PyWebView {
10
+ api: DesktopBridgeApi;
11
+ }
12
+
13
+ interface Window {
14
+ pywebview?: PyWebView;
15
+ }
File without changes