evomeme 0.1.9 → 0.1.10-beta.13

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 (477) hide show
  1. package/README.md +2 -0
  2. package/dist/canvas-public/app.js +509 -5999
  3. package/dist/canvas-public/assets/presets/catalog.json +47 -0
  4. package/dist/canvas-public/assets/presets/tiles/A_01_CaoDi.png +0 -0
  5. package/dist/canvas-public/assets/presets/tiles/A_02_ShaMo.png +0 -0
  6. package/dist/canvas-public/assets/presets/tiles/A_03_TianKong.png +0 -0
  7. package/dist/canvas-public/assets/presets/tiles/A_04_ShuiBo.png +0 -0
  8. package/dist/canvas-public/assets/presets/tiles/A_05_HaiMian.png +0 -0
  9. package/dist/canvas-public/assets/presets/tiles/A_06_MuDiBan.png +0 -0
  10. package/dist/canvas-public/assets/presets/tiles/A_07_ShiZhuan.png +0 -0
  11. package/dist/canvas-public/assets/presets/tiles/A_08_AnRongYan.png +0 -0
  12. package/dist/canvas-public/assets/presets/tiles/A_09_QingTaiShi.png +0 -0
  13. package/dist/canvas-public/assets/presets/tiles/A_10_ShenHai.png +0 -0
  14. package/dist/canvas-public/assets/presets/tiles/A_11_HuiYanShi.png +0 -0
  15. package/dist/canvas-public/assets/presets/tiles/A_12_HongTu.png +0 -0
  16. package/dist/canvas-public/assets/presets/tiles/A_13_GengDi.png +0 -0
  17. package/dist/canvas-public/assets/presets/tiles/A_14_HuoShanYan.png +0 -0
  18. package/dist/canvas-public/assets/presets/tiles/A_15_NiTu.png +0 -0
  19. package/dist/canvas-public/assets/presets/tiles/A_16_HuangSha.png +0 -0
  20. package/dist/canvas-public/assets/presets/tiles/A_17_CongLin.png +0 -0
  21. package/dist/canvas-public/assets/presets/tiles/A_18_ZiYanShi.png +0 -0
  22. package/dist/canvas-public/assets/presets/tiles/A_19_MoGuTu.png +0 -0
  23. package/dist/canvas-public/assets/presets/tiles/A_20_HuaDian.png +0 -0
  24. package/dist/canvas-public/assets/presets/tiles/A_21_HaiLang.png +0 -0
  25. package/dist/canvas-public/assets/presets/tiles/A_22_GanTu.png +0 -0
  26. package/dist/canvas-public/assets/presets/tiles/A_23_QingTaiShiBan.png +0 -0
  27. package/dist/canvas-public/assets/presets/tiles/A_24_XingKong.png +0 -0
  28. package/dist/canvas-public/assets/presets/tiles/A_25_MoFaFuWen.png +0 -0
  29. package/dist/canvas-public/assets/presets/tiles/A_26_ShiTuYeDi.png +0 -0
  30. package/dist/canvas-public/assets/presets/tiles/A_27_LanZhuan.png +0 -0
  31. package/dist/canvas-public/assets/presets/tiles/A_28_HongZhuan.png +0 -0
  32. package/dist/canvas-public/assets/presets/tiles/A_29_YueQiu.png +0 -0
  33. package/dist/canvas-public/assets/presets/tiles/A_30_JinShuGe.png +0 -0
  34. package/dist/canvas-public/assets/presets/tiles/A_31_JinShuJingGai.png +0 -0
  35. package/dist/canvas-public/assets/presets/tiles/A_32_ShuZiChuang.png +0 -0
  36. package/dist/canvas-public/canvas/actions.js +144 -0
  37. package/dist/canvas-public/canvas/camera.js +28 -0
  38. package/dist/canvas-public/canvas/chrome.js +14 -0
  39. package/dist/canvas-public/canvas/data.js +123 -0
  40. package/dist/canvas-public/canvas/dependencies.js +19 -0
  41. package/dist/canvas-public/canvas/interactions.js +544 -0
  42. package/dist/canvas-public/canvas/panel-resize.js +5 -0
  43. package/dist/canvas-public/canvas/renderer.js +297 -0
  44. package/dist/canvas-public/canvas/selection.js +62 -0
  45. package/dist/canvas-public/canvas/serialization.js +118 -0
  46. package/dist/canvas-public/canvas/sync.js +202 -0
  47. package/dist/canvas-public/core/api.js +32 -0
  48. package/dist/canvas-public/core/feature-registry.js +29 -0
  49. package/dist/canvas-public/core/feature-state.js +228 -0
  50. package/dist/canvas-public/core/layout.js +133 -0
  51. package/dist/canvas-public/core/operations.js +38 -0
  52. package/dist/canvas-public/core/project.js +203 -0
  53. package/dist/canvas-public/core/state.js +27 -0
  54. package/dist/canvas-public/dialogs/context-menu.js +118 -0
  55. package/dist/canvas-public/elements/base.js +100 -0
  56. package/dist/canvas-public/elements/connections.js +105 -0
  57. package/dist/canvas-public/elements/drag.js +53 -0
  58. package/dist/canvas-public/elements/image.js +259 -0
  59. package/dist/canvas-public/elements/loading-recovery.js +5 -0
  60. package/dist/canvas-public/elements/loading.js +21 -0
  61. package/dist/canvas-public/elements/map.js +64 -0
  62. package/dist/canvas-public/elements/registry.js +58 -0
  63. package/dist/canvas-public/elements/replacement.js +78 -0
  64. package/dist/canvas-public/elements/resize.js +210 -0
  65. package/dist/canvas-public/elements/task-pipeline.js +235 -0
  66. package/dist/canvas-public/elements/types.js +21 -0
  67. package/dist/canvas-public/elements/ui.js +14 -0
  68. package/dist/canvas-public/elements/video.js +64 -0
  69. package/dist/canvas-public/features/assets/local-assets.js +72 -0
  70. package/dist/canvas-public/features/assets/official-assets.js +539 -0
  71. package/dist/canvas-public/features/editing/canvas-size.js +532 -0
  72. package/dist/canvas-public/features/editing/color-key.js +471 -0
  73. package/dist/canvas-public/features/editing/cutout.js +20 -0
  74. package/dist/canvas-public/features/editing/interactive-image-editing.js +982 -0
  75. package/dist/canvas-public/features/editing/layer-decomposition.js +45 -0
  76. package/dist/canvas-public/features/editing/manual-split.js +115 -0
  77. package/dist/canvas-public/features/editing/perfect-pixel.js +299 -0
  78. package/dist/canvas-public/features/editing/scroll-preview.js +130 -0
  79. package/dist/canvas-public/features/editing/scroll-seam-elimination.js +71 -0
  80. package/dist/canvas-public/features/editing/seam-elimination.js +46 -0
  81. package/dist/canvas-public/features/editing/smart-split.js +49 -0
  82. package/dist/canvas-public/features/editing/source-preview.js +43 -0
  83. package/dist/canvas-public/features/editing/video-cycle.js +49 -0
  84. package/dist/canvas-public/features/editing/video-frames.js +35 -0
  85. package/dist/canvas-public/features/frame-editor/frame-editing.js +755 -0
  86. package/dist/canvas-public/features/frame-editor/frame-model.js +63 -0
  87. package/dist/canvas-public/features/frame-editor/playback.js +83 -0
  88. package/dist/canvas-public/features/frame-editor/preview.js +151 -0
  89. package/dist/canvas-public/features/generation/animation.js +272 -0
  90. package/dist/canvas-public/features/generation/asset-batch.js +102 -0
  91. package/dist/canvas-public/features/generation/direction.js +163 -0
  92. package/dist/canvas-public/features/generation/fields.js +73 -0
  93. package/dist/canvas-public/features/generation/game-ui.js +64 -0
  94. package/dist/canvas-public/features/generation/image.js +96 -0
  95. package/dist/canvas-public/features/generation/scroll-image.js +157 -0
  96. package/dist/canvas-public/features/generation/seamless-tile.js +52 -0
  97. package/dist/canvas-public/features/generation/tile-transition-atlas.js +79 -0
  98. package/dist/canvas-public/features/generation/video.js +105 -0
  99. package/dist/canvas-public/features/map/creation.js +73 -0
  100. package/dist/canvas-public/features/map/tile-map-editor.js +2520 -0
  101. package/dist/canvas-public/features/registry.js +59 -0
  102. package/dist/canvas-public/index.html +84 -259
  103. package/dist/canvas-public/inspector/controls.js +49 -0
  104. package/dist/canvas-public/inspector/frame-preview.js +11 -0
  105. package/dist/canvas-public/inspector/inspector.js +190 -0
  106. package/dist/canvas-public/inspector/media-inspector.js +517 -0
  107. package/dist/canvas-public/inspector/media-preview.js +56 -0
  108. package/dist/canvas-public/inspector/number-stepper.js +96 -0
  109. package/dist/canvas-public/inspector/resize.js +88 -0
  110. package/dist/canvas-public/inspector/scroll-preview.js +11 -0
  111. package/dist/canvas-public/inspector/ui-inspector.js +14 -0
  112. package/dist/canvas-public/language/en-US.js +1059 -0
  113. package/dist/canvas-public/language/index.js +91 -0
  114. package/dist/canvas-public/language/zh-CN.js +1059 -0
  115. package/dist/canvas-public/map/export.js +10 -0
  116. package/dist/canvas-public/map/inspector.js +8 -0
  117. package/dist/canvas-public/map/model.js +409 -0
  118. package/dist/canvas-public/map/tile-map-editor.js +2 -0
  119. package/dist/canvas-public/media/metadata.js +183 -0
  120. package/dist/canvas-public/media/support.js +183 -0
  121. package/dist/canvas-public/styles/base.css +49 -0
  122. package/dist/canvas-public/styles/canvas.css +127 -0
  123. package/dist/canvas-public/styles/controls.css +203 -0
  124. package/dist/canvas-public/styles/dialogs.css +521 -0
  125. package/dist/canvas-public/styles/feature-form.css +409 -0
  126. package/dist/canvas-public/styles/inspector.css +441 -0
  127. package/dist/canvas-public/styles/layout.css +344 -0
  128. package/dist/canvas-public/styles/map.css +11 -0
  129. package/dist/canvas-public/styles/nodes.css +636 -0
  130. package/dist/canvas-public/styles.css +9 -2247
  131. package/dist/canvas-public/ui/controls.js +222 -0
  132. package/dist/canvas-public/ui/elements.js +9 -0
  133. package/dist/canvas-public/ui/feature-controller.js +296 -0
  134. package/dist/canvas-public/ui/feature-form.js +402 -0
  135. package/dist/canvas-public/ui/settings.js +84 -0
  136. package/dist/canvas-public/ui/toolbar.js +55 -0
  137. package/dist/canvas-public/vendor/pixi-tilemap.js +126 -0
  138. package/dist/canvas-public/vendor/pixi.min.js +2340 -0
  139. package/dist/canvas-public/workers/color-key-worker.js +289 -0
  140. package/dist/image-workers/perfect-pixel/perfect-pixel-core.js +441 -0
  141. package/dist/image-workers/perfect-pixel/perfect-pixel-worker.cjs +10 -0
  142. package/dist/index.js +46710 -12400
  143. package/dist/skills/en-US/evomeme-canvas/SKILL.md +52 -0
  144. package/dist/skills/en-US/evomeme-canvas/agents/openai.yaml +7 -0
  145. package/dist/skills/en-US/evomeme-cloud-fs/SKILL.md +203 -0
  146. package/dist/skills/en-US/evomeme-cloud-fs/agents/openai.yaml +7 -0
  147. package/dist/skills/en-US/evomeme-cover/SKILL.md +131 -0
  148. package/dist/skills/en-US/evomeme-cover/agents/openai.yaml +7 -0
  149. package/dist/skills/en-US/evomeme-game-maker/SKILL.md +288 -0
  150. package/dist/skills/en-US/evomeme-game-maker/agents/openai.yaml +4 -0
  151. package/dist/skills/en-US/evomeme-login/SKILL.md +97 -0
  152. package/dist/skills/en-US/evomeme-login/agents/openai.yaml +7 -0
  153. package/dist/skills/en-US/evomeme-mcp/SKILL.md +71 -0
  154. package/dist/skills/en-US/evomeme-mcp/agents/openai.yaml +7 -0
  155. package/dist/skills/en-US/evomeme-mcp/references/troubleshooting.md +55 -0
  156. package/dist/skills/en-US/evomeme-native-bridge-sdk/SKILL.md +286 -0
  157. package/dist/skills/en-US/evomeme-native-bridge-sdk/agents/openai.yaml +8 -0
  158. package/dist/skills/en-US/evomeme-tos-upload/SKILL.md +47 -0
  159. package/dist/skills/en-US/evomeme-tos-upload/agents/openai.yaml +5 -0
  160. package/dist/skills/en-US/evomeme-ui/SKILL.md +123 -0
  161. package/dist/skills/en-US/evomeme-ui/agents/openai.yaml +4 -0
  162. package/dist/skills/en-US/evomeme-ui/references/host-layout.md +69 -0
  163. package/dist/skills/en-US/game-art/SKILL.md +121 -0
  164. package/dist/skills/en-US/game-art/agents/openai.yaml +8 -0
  165. package/dist/skills/en-US/game-asset-align/SKILL.md +47 -0
  166. package/dist/skills/en-US/game-asset-align/agents/openai.yaml +8 -0
  167. package/dist/skills/en-US/game-asset-align/references/alignment-contract.md +61 -0
  168. package/dist/skills/en-US/game-asset-align/references/blender-scene-pipeline.md +35 -0
  169. package/dist/skills/en-US/game-asset-align/references/canonical-package.md +26 -0
  170. package/dist/skills/en-US/game-asset-align/references/engine-capabilities.md +14 -0
  171. package/dist/skills/en-US/game-asset-align/references/lightweight-review.md +34 -0
  172. package/dist/skills/en-US/game-asset-align/references/repair-policy.md +30 -0
  173. package/dist/skills/en-US/game-asset-align/references/tool-contract.md +69 -0
  174. package/dist/skills/en-US/game-asset-align/references/visual-review.md +26 -0
  175. package/dist/skills/en-US/game-asset-batch-generator/SKILL.md +117 -0
  176. package/dist/skills/en-US/game-asset-batch-generator/agents/openai.yaml +7 -0
  177. package/dist/skills/en-US/game-asset-image-generator/SKILL.md +86 -0
  178. package/dist/skills/en-US/game-asset-image-generator/agents/openai.yaml +7 -0
  179. package/dist/skills/en-US/game-asset-image-generator/references/background.md +34 -0
  180. package/dist/skills/en-US/game-asset-image-generator/references/static-game-assets.md +38 -0
  181. package/dist/skills/en-US/game-asset-mesh-generator/SKILL.md +35 -0
  182. package/dist/skills/en-US/game-asset-mesh-generator/agents/openai.yaml +7 -0
  183. package/dist/skills/en-US/game-asset-mesh-generator/references/animation-presets.md +313 -0
  184. package/dist/skills/en-US/game-asset-mesh-generator/references/generation-contracts.md +131 -0
  185. package/dist/skills/en-US/game-asset-mesh-generator/references/image-generator-workflows.md +65 -0
  186. package/dist/skills/en-US/game-asset-mesh-generator/references/postprocess-contracts.md +187 -0
  187. package/dist/skills/en-US/game-asset-mesh-generator/references/threejs-integration.md +113 -0
  188. package/dist/skills/en-US/game-asset-mesh-generator/references/tool-reference.md +67 -0
  189. package/dist/skills/en-US/game-audio/SKILL.md +121 -0
  190. package/dist/skills/en-US/game-audio/agents/openai.yaml +4 -0
  191. package/{skills → dist/skills/en-US}/game-audio/references/bgm-cdn.md +7 -11
  192. package/{skills → dist/skills/en-US}/game-audio/references/sfx-cdn.md +6 -4
  193. package/dist/skills/en-US/game-character-view-generator/SKILL.md +110 -0
  194. package/dist/skills/en-US/game-character-view-generator/agents/openai.yaml +7 -0
  195. package/dist/skills/en-US/game-demo/SKILL.md +103 -0
  196. package/dist/skills/en-US/game-demo/agents/openai.yaml +7 -0
  197. package/dist/skills/en-US/game-image-layer-decomposer/SKILL.md +99 -0
  198. package/dist/skills/en-US/game-image-layer-decomposer/agents/openai.yaml +7 -0
  199. package/dist/skills/en-US/game-image-splitter/SKILL.md +114 -0
  200. package/dist/skills/en-US/game-image-splitter/agents/openai.yaml +7 -0
  201. package/dist/skills/en-US/game-maker/SKILL.md +111 -0
  202. package/dist/skills/en-US/game-maker/agents/openai.yaml +4 -0
  203. package/dist/skills/en-US/game-perfect-pixel/SKILL.md +123 -0
  204. package/dist/skills/en-US/game-perfect-pixel/agents/openai.yaml +7 -0
  205. package/dist/skills/en-US/game-scroll-generator/SKILL.md +110 -0
  206. package/dist/skills/en-US/game-scroll-generator/agents/openai.yaml +7 -0
  207. package/dist/skills/en-US/game-test/SKILL.md +58 -0
  208. package/dist/skills/en-US/game-test/agents/openai.yaml +7 -0
  209. package/dist/skills/en-US/game-tile-generator/SKILL.md +152 -0
  210. package/dist/skills/en-US/game-tile-generator/agents/openai.yaml +7 -0
  211. package/dist/skills/en-US/game-ui/SKILL.md +177 -0
  212. package/dist/skills/en-US/game-ui/agents/openai.yaml +4 -0
  213. package/dist/skills/en-US/game-ui-generator/SKILL.md +82 -0
  214. package/dist/skills/en-US/game-ui-generator/agents/openai.yaml +7 -0
  215. package/dist/skills/en-US/game-ui-generator/references/component-patterns.md +65 -0
  216. package/dist/skills/en-US/game-ui-generator/references/runtime-integration.md +45 -0
  217. package/dist/skills/en-US/game-video-cycle/SKILL.md +98 -0
  218. package/dist/skills/en-US/game-video-cycle/agents/openai.yaml +7 -0
  219. package/dist/skills/en-US/game-video-generator/SKILL.md +78 -0
  220. package/dist/skills/en-US/game-video-generator/agents/openai.yaml +7 -0
  221. package/dist/skills/en-US/godot-3d-game/SKILL.md +41 -0
  222. package/dist/skills/en-US/godot-3d-game/agents/openai.yaml +8 -0
  223. package/dist/skills/en-US/godot-3d-game/references/alignment-gates.md +26 -0
  224. package/dist/skills/en-US/godot-3d-game/references/appearance-alignment.md +54 -0
  225. package/dist/skills/en-US/godot-3d-game/references/build-and-qa.md +22 -0
  226. package/dist/skills/en-US/godot-3d-game/references/canonical-asset-runtime.md +27 -0
  227. package/dist/skills/en-US/godot-3d-game/references/import-and-wrapper-scenes.md +20 -0
  228. package/dist/skills/en-US/godot-3d-game/references/physics-colliders.md +13 -0
  229. package/dist/skills/en-US/godot-3d-game/references/project-structure.md +22 -0
  230. package/dist/skills/en-US/godot-3d-game/references/skeleton-animation.md +16 -0
  231. package/dist/skills/en-US/phaser-2d-game/SKILL.md +127 -0
  232. package/dist/skills/en-US/phaser-2d-game/agents/openai.yaml +8 -0
  233. package/dist/skills/en-US/phaser-2d-game/references/engine-performance.md +64 -0
  234. package/dist/skills/en-US/phaser-2d-game/references/engine-physics.md +139 -0
  235. package/dist/skills/en-US/phaser-2d-game/references/engine-runtime.md +129 -0
  236. package/dist/skills/en-US/phaser-2d-game/references/engine-spritesheets.md +99 -0
  237. package/dist/skills/en-US/phaser-2d-game/references/engine-tilemaps.md +80 -0
  238. package/dist/skills/en-US/phaser-2d-game/references/file-structure.md +94 -0
  239. package/dist/skills/en-US/phaser-2d-graphics-builder/SKILL.md +68 -0
  240. package/dist/skills/en-US/phaser-2d-graphics-builder/agents/openai.yaml +7 -0
  241. package/dist/skills/en-US/threejs-3d-game/SKILL.md +117 -0
  242. package/dist/skills/en-US/threejs-3d-game/agents/openai.yaml +7 -0
  243. package/dist/skills/en-US/threejs-3d-game/references/engine-asset-pipeline.md +155 -0
  244. package/dist/skills/en-US/threejs-3d-game/references/engine-audio.md +70 -0
  245. package/dist/skills/en-US/threejs-3d-game/references/engine-input-camera.md +84 -0
  246. package/dist/skills/en-US/threejs-3d-game/references/engine-performance.md +96 -0
  247. package/dist/skills/en-US/threejs-3d-game/references/engine-physics.md +142 -0
  248. package/dist/skills/en-US/threejs-3d-game/references/engine-runtime.md +199 -0
  249. package/dist/skills/en-US/threejs-3d-game/references/file-structure.md +353 -0
  250. package/dist/skills/en-US/threejs-aaa-graphics-builder/SKILL.md +62 -0
  251. package/dist/skills/en-US/threejs-aaa-graphics-builder/agents/openai.yaml +7 -0
  252. package/{skills → dist/skills/zh-CN}/evomeme-cloud-fs/SKILL.md +33 -6
  253. package/{skills → dist/skills/zh-CN}/evomeme-cover/SKILL.md +7 -8
  254. package/{skills → dist/skills/zh-CN}/evomeme-game-maker/SKILL.md +91 -88
  255. package/{skills → dist/skills/zh-CN}/evomeme-login/SKILL.md +5 -0
  256. package/dist/skills/zh-CN/evomeme-mcp/SKILL.md +71 -0
  257. package/dist/skills/zh-CN/evomeme-mcp/agents/openai.yaml +7 -0
  258. package/dist/skills/zh-CN/evomeme-mcp/references/troubleshooting.md +55 -0
  259. package/{skills → dist/skills/zh-CN}/evomeme-tos-upload/SKILL.md +6 -5
  260. package/dist/skills/zh-CN/evomeme-ui/SKILL.md +123 -0
  261. package/dist/skills/zh-CN/evomeme-ui/agents/openai.yaml +4 -0
  262. package/dist/skills/zh-CN/evomeme-ui/references/host-layout.md +69 -0
  263. package/dist/skills/zh-CN/game-asset-align/SKILL.md +47 -0
  264. package/dist/skills/zh-CN/game-asset-align/agents/openai.yaml +8 -0
  265. package/dist/skills/zh-CN/game-asset-align/references/alignment-contract.md +61 -0
  266. package/dist/skills/zh-CN/game-asset-align/references/blender-scene-pipeline.md +35 -0
  267. package/dist/skills/zh-CN/game-asset-align/references/canonical-package.md +26 -0
  268. package/dist/skills/zh-CN/game-asset-align/references/engine-capabilities.md +14 -0
  269. package/dist/skills/zh-CN/game-asset-align/references/lightweight-review.md +34 -0
  270. package/dist/skills/zh-CN/game-asset-align/references/repair-policy.md +30 -0
  271. package/dist/skills/zh-CN/game-asset-align/references/tool-contract.md +69 -0
  272. package/dist/skills/zh-CN/game-asset-align/references/visual-review.md +26 -0
  273. package/dist/skills/zh-CN/game-asset-batch-generator/SKILL.md +117 -0
  274. package/dist/skills/zh-CN/game-asset-batch-generator/agents/openai.yaml +7 -0
  275. package/dist/skills/zh-CN/game-asset-image-generator/SKILL.md +86 -0
  276. package/{skills → dist/skills/zh-CN}/game-asset-image-generator/references/background.md +3 -1
  277. package/{skills → dist/skills/zh-CN}/game-asset-image-generator/references/static-game-assets.md +2 -2
  278. package/dist/skills/zh-CN/game-asset-mesh-generator/SKILL.md +35 -0
  279. package/dist/skills/zh-CN/game-asset-mesh-generator/agents/openai.yaml +7 -0
  280. package/dist/skills/zh-CN/game-asset-mesh-generator/references/animation-presets.md +313 -0
  281. package/dist/skills/zh-CN/game-asset-mesh-generator/references/generation-contracts.md +131 -0
  282. package/dist/skills/zh-CN/game-asset-mesh-generator/references/image-generator-workflows.md +65 -0
  283. package/dist/skills/zh-CN/game-asset-mesh-generator/references/postprocess-contracts.md +187 -0
  284. package/dist/skills/zh-CN/game-asset-mesh-generator/references/threejs-integration.md +113 -0
  285. package/dist/skills/zh-CN/game-asset-mesh-generator/references/tool-reference.md +67 -0
  286. package/dist/skills/zh-CN/game-audio/SKILL.md +121 -0
  287. package/dist/skills/zh-CN/game-audio/agents/openai.yaml +4 -0
  288. package/dist/skills/zh-CN/game-audio/references/bgm-cdn.md +123 -0
  289. package/dist/skills/zh-CN/game-audio/references/sfx-cdn.md +79 -0
  290. package/dist/skills/zh-CN/game-character-view-generator/SKILL.md +110 -0
  291. package/dist/skills/zh-CN/game-character-view-generator/agents/openai.yaml +7 -0
  292. package/dist/skills/zh-CN/game-image-layer-decomposer/SKILL.md +99 -0
  293. package/dist/skills/zh-CN/game-image-layer-decomposer/agents/openai.yaml +7 -0
  294. package/dist/skills/zh-CN/game-image-splitter/SKILL.md +114 -0
  295. package/dist/skills/zh-CN/game-image-splitter/agents/openai.yaml +7 -0
  296. package/dist/skills/zh-CN/game-perfect-pixel/SKILL.md +123 -0
  297. package/dist/skills/zh-CN/game-perfect-pixel/agents/openai.yaml +7 -0
  298. package/dist/skills/zh-CN/game-scroll-generator/SKILL.md +110 -0
  299. package/dist/skills/zh-CN/game-scroll-generator/agents/openai.yaml +7 -0
  300. package/dist/skills/zh-CN/game-test/references/checklists/playtest-qa.md +12 -0
  301. package/dist/skills/zh-CN/game-tile-generator/SKILL.md +151 -0
  302. package/dist/skills/zh-CN/game-tile-generator/agents/openai.yaml +7 -0
  303. package/{skills → dist/skills/zh-CN}/game-ui/SKILL.md +9 -1
  304. package/dist/skills/zh-CN/game-ui/references/checklists/game-ui-quality.md +14 -0
  305. package/dist/skills/zh-CN/game-ui/references/checklists/hud-readability.md +12 -0
  306. package/dist/skills/zh-CN/game-ui/references/checklists/mobile-input.md +12 -0
  307. package/dist/skills/zh-CN/game-ui/references/checklists/responsive-ui-fit.md +12 -0
  308. package/dist/skills/zh-CN/game-ui/references/ui-patterns.md +140 -0
  309. package/dist/skills/zh-CN/game-ui/references/ui-prompt-templates.md +63 -0
  310. package/dist/skills/zh-CN/game-ui-generator/SKILL.md +82 -0
  311. package/dist/skills/zh-CN/game-ui-generator/agents/openai.yaml +7 -0
  312. package/dist/skills/zh-CN/game-ui-generator/references/component-patterns.md +65 -0
  313. package/dist/skills/zh-CN/game-ui-generator/references/runtime-integration.md +45 -0
  314. package/dist/skills/zh-CN/game-video-cycle/SKILL.md +98 -0
  315. package/dist/skills/zh-CN/game-video-cycle/agents/openai.yaml +7 -0
  316. package/dist/skills/zh-CN/game-video-generator/SKILL.md +78 -0
  317. package/dist/skills/zh-CN/game-video-generator/agents/openai.yaml +7 -0
  318. package/dist/skills/zh-CN/godot-3d-game/SKILL.md +41 -0
  319. package/dist/skills/zh-CN/godot-3d-game/agents/openai.yaml +8 -0
  320. package/dist/skills/zh-CN/godot-3d-game/references/alignment-gates.md +26 -0
  321. package/dist/skills/zh-CN/godot-3d-game/references/appearance-alignment.md +54 -0
  322. package/dist/skills/zh-CN/godot-3d-game/references/build-and-qa.md +22 -0
  323. package/dist/skills/zh-CN/godot-3d-game/references/canonical-asset-runtime.md +27 -0
  324. package/dist/skills/zh-CN/godot-3d-game/references/import-and-wrapper-scenes.md +20 -0
  325. package/dist/skills/zh-CN/godot-3d-game/references/physics-colliders.md +13 -0
  326. package/dist/skills/zh-CN/godot-3d-game/references/project-structure.md +22 -0
  327. package/dist/skills/zh-CN/godot-3d-game/references/skeleton-animation.md +16 -0
  328. package/dist/skills/zh-CN/phaser-2d-graphics-builder/assets/scorecard-anchors/scene-1.jpeg +0 -0
  329. package/dist/skills/zh-CN/phaser-2d-graphics-builder/assets/scorecard-anchors/scene-2.jpeg +0 -0
  330. package/dist/skills/zh-CN/phaser-2d-graphics-builder/assets/scorecard-anchors/scene-3.jpg +0 -0
  331. package/dist/skills/zh-CN/phaser-2d-graphics-builder/references/checklists/compose-quality.md +10 -0
  332. package/dist/skills/zh-CN/phaser-2d-graphics-builder/references/checklists/performance-safe-visual-detail.md +9 -0
  333. package/dist/skills/zh-CN/phaser-2d-graphics-builder/references/checklists/premium-game-quality-gate.md +15 -0
  334. package/dist/skills/zh-CN/phaser-2d-graphics-builder/references/checklists/sprite-quality.md +10 -0
  335. package/dist/skills/zh-CN/phaser-2d-graphics-builder/references/checklists/technical-art-quality.md +9 -0
  336. package/dist/skills/zh-CN/phaser-2d-graphics-builder/references/checklists/visual-scorecard.md +9 -0
  337. package/dist/skills/zh-CN/phaser-2d-graphics-builder/references/compose-recipes.md +71 -0
  338. package/dist/skills/zh-CN/phaser-2d-graphics-builder/references/graphics-prompt-templates.md +33 -0
  339. package/dist/skills/zh-CN/phaser-2d-graphics-builder/references/implementation-blueprint.md +82 -0
  340. package/dist/skills/zh-CN/phaser-2d-graphics-builder/references/sprite-recipes.md +90 -0
  341. package/dist/skills/zh-CN/phaser-2d-graphics-builder/references/technical-art.md +67 -0
  342. package/dist/skills/zh-CN/phaser-2d-graphics-builder/references/vfx-recipes.md +67 -0
  343. package/dist/skills/zh-CN/phaser-2d-graphics-builder/references/visual-scorecard.md +129 -0
  344. package/{skills → dist/skills/zh-CN}/threejs-3d-game/SKILL.md +6 -2
  345. package/dist/skills/zh-CN/threejs-3d-game/references/engine-asset-pipeline.md +155 -0
  346. package/{skills → dist/skills/zh-CN}/threejs-aaa-graphics-builder/SKILL.md +6 -6
  347. package/dist/skills/zh-CN/threejs-aaa-graphics-builder/assets/scorecard-anchors/scene-1.jpg +0 -0
  348. package/dist/skills/zh-CN/threejs-aaa-graphics-builder/assets/scorecard-anchors/scene-2.jpg +0 -0
  349. package/dist/skills/zh-CN/threejs-aaa-graphics-builder/assets/scorecard-anchors/scene-3.jpg +0 -0
  350. package/dist/skills/zh-CN/threejs-aaa-graphics-builder/references/checklists/aaa-game-quality-gate.md +23 -0
  351. package/dist/skills/zh-CN/threejs-aaa-graphics-builder/references/checklists/aaa-visual-scorecard.md +19 -0
  352. package/dist/skills/zh-CN/threejs-aaa-graphics-builder/references/checklists/material-lighting-quality.md +14 -0
  353. package/dist/skills/zh-CN/threejs-aaa-graphics-builder/references/checklists/performance-safe-visual-detail.md +14 -0
  354. package/dist/skills/zh-CN/threejs-aaa-graphics-builder/references/checklists/procedural-model-quality.md +15 -0
  355. package/dist/skills/zh-CN/threejs-aaa-graphics-builder/references/checklists/technical-art-quality.md +17 -0
  356. package/dist/skills/zh-CN/threejs-aaa-graphics-builder/references/graphics-prompt-templates.md +236 -0
  357. package/dist/skills/zh-CN/threejs-aaa-graphics-builder/references/implementation-blueprint.md +161 -0
  358. package/dist/skills/zh-CN/threejs-aaa-graphics-builder/references/model-recipes.md +146 -0
  359. package/dist/skills/zh-CN/threejs-aaa-graphics-builder/references/render-recipes.md +90 -0
  360. package/dist/skills/zh-CN/threejs-aaa-graphics-builder/references/shader-cookbook.md +292 -0
  361. package/dist/skills/zh-CN/threejs-aaa-graphics-builder/references/technical-art.md +167 -0
  362. package/dist/skills/zh-CN/threejs-aaa-graphics-builder/references/visual-scorecard.md +147 -0
  363. package/dist/stage-blender/bake_materials.py +102 -0
  364. package/dist/stage-blender/export_scene.py +275 -0
  365. package/dist/stage-blender/preview_camera.py +90 -0
  366. package/dist/stage-blender/repair_mesh.py +112 -0
  367. package/dist/stage-public/app.js +759 -0
  368. package/dist/stage-public/baked/bakePilot.js +44 -0
  369. package/dist/stage-public/baked/bakedScene.js +129 -0
  370. package/dist/stage-public/baked/sceneAppearance.js +225 -0
  371. package/dist/stage-public/baked/sceneCamera.js +45 -0
  372. package/dist/stage-public/comparison-contract.js +28 -0
  373. package/dist/stage-public/delivery.html +4 -0
  374. package/dist/stage-public/delivery.js +17 -0
  375. package/dist/stage-public/godot/appearance.gd +170 -0
  376. package/dist/stage-public/godot/preview.gd +79 -0
  377. package/dist/stage-public/index.html +141 -0
  378. package/dist/stage-public/review-app.js +289 -0
  379. package/dist/stage-public/review-runtime.js +63 -0
  380. package/dist/stage-public/review.css +24 -0
  381. package/dist/stage-public/review.html +47 -0
  382. package/dist/stage-public/scene-runtime.js +212 -0
  383. package/dist/stage-public/sceneContractRuntime.js +235 -0
  384. package/dist/stage-public/styles.css +328 -0
  385. package/dist/stage-public/three-appearance.js +104 -0
  386. package/dist/stage-public/vendor/three170/LICENSE +21 -0
  387. package/dist/stage-public/vendor/three170/build/three.module.js +54571 -0
  388. package/dist/stage-public/vendor/three170/examples/jsm/controls/OrbitControls.js +1523 -0
  389. package/dist/stage-public/vendor/three170/examples/jsm/environments/RoomEnvironment.js +144 -0
  390. package/dist/stage-public/vendor/three170/examples/jsm/libs/fflate.module.js +2672 -0
  391. package/dist/stage-public/vendor/three170/examples/jsm/loaders/EXRLoader.js +2563 -0
  392. package/dist/stage-public/vendor/three170/examples/jsm/loaders/GLTFLoader.js +4727 -0
  393. package/dist/stage-public/vendor/three170/examples/jsm/utils/BufferGeometryUtils.js +1373 -0
  394. package/dist/tile-transition-mask.svg +40 -0
  395. package/package.json +24 -8
  396. package/dist/canvas-public/cycle-detect.js +0 -166
  397. package/dist/canvas-public/smart-split.js +0 -185
  398. package/skills/game-asset-image-generator/SKILL.md +0 -104
  399. package/skills/game-audio/SKILL.md +0 -82
  400. package/skills/game-audio/agents/openai.yaml +0 -4
  401. package/skills/threejs-3d-game/references/engine-asset-pipeline.md +0 -116
  402. /package/{skills → dist/skills/en-US}/game-test/references/checklists/playtest-qa.md +0 -0
  403. /package/{skills → dist/skills/en-US}/game-ui/references/checklists/game-ui-quality.md +0 -0
  404. /package/{skills → dist/skills/en-US}/game-ui/references/checklists/hud-readability.md +0 -0
  405. /package/{skills → dist/skills/en-US}/game-ui/references/checklists/mobile-input.md +0 -0
  406. /package/{skills → dist/skills/en-US}/game-ui/references/checklists/responsive-ui-fit.md +0 -0
  407. /package/{skills → dist/skills/en-US}/game-ui/references/ui-patterns.md +0 -0
  408. /package/{skills → dist/skills/en-US}/game-ui/references/ui-prompt-templates.md +0 -0
  409. /package/{skills → dist/skills/en-US}/phaser-2d-graphics-builder/assets/scorecard-anchors/scene-1.jpeg +0 -0
  410. /package/{skills → dist/skills/en-US}/phaser-2d-graphics-builder/assets/scorecard-anchors/scene-2.jpeg +0 -0
  411. /package/{skills → dist/skills/en-US}/phaser-2d-graphics-builder/assets/scorecard-anchors/scene-3.jpg +0 -0
  412. /package/{skills → dist/skills/en-US}/phaser-2d-graphics-builder/references/checklists/compose-quality.md +0 -0
  413. /package/{skills → dist/skills/en-US}/phaser-2d-graphics-builder/references/checklists/performance-safe-visual-detail.md +0 -0
  414. /package/{skills → dist/skills/en-US}/phaser-2d-graphics-builder/references/checklists/premium-game-quality-gate.md +0 -0
  415. /package/{skills → dist/skills/en-US}/phaser-2d-graphics-builder/references/checklists/sprite-quality.md +0 -0
  416. /package/{skills → dist/skills/en-US}/phaser-2d-graphics-builder/references/checklists/technical-art-quality.md +0 -0
  417. /package/{skills → dist/skills/en-US}/phaser-2d-graphics-builder/references/checklists/visual-scorecard.md +0 -0
  418. /package/{skills → dist/skills/en-US}/phaser-2d-graphics-builder/references/compose-recipes.md +0 -0
  419. /package/{skills → dist/skills/en-US}/phaser-2d-graphics-builder/references/graphics-prompt-templates.md +0 -0
  420. /package/{skills → dist/skills/en-US}/phaser-2d-graphics-builder/references/implementation-blueprint.md +0 -0
  421. /package/{skills → dist/skills/en-US}/phaser-2d-graphics-builder/references/sprite-recipes.md +0 -0
  422. /package/{skills → dist/skills/en-US}/phaser-2d-graphics-builder/references/technical-art.md +0 -0
  423. /package/{skills → dist/skills/en-US}/phaser-2d-graphics-builder/references/vfx-recipes.md +0 -0
  424. /package/{skills → dist/skills/en-US}/phaser-2d-graphics-builder/references/visual-scorecard.md +0 -0
  425. /package/{skills → dist/skills/en-US}/threejs-aaa-graphics-builder/assets/scorecard-anchors/scene-1.jpg +0 -0
  426. /package/{skills → dist/skills/en-US}/threejs-aaa-graphics-builder/assets/scorecard-anchors/scene-2.jpg +0 -0
  427. /package/{skills → dist/skills/en-US}/threejs-aaa-graphics-builder/assets/scorecard-anchors/scene-3.jpg +0 -0
  428. /package/{skills → dist/skills/en-US}/threejs-aaa-graphics-builder/references/checklists/aaa-game-quality-gate.md +0 -0
  429. /package/{skills → dist/skills/en-US}/threejs-aaa-graphics-builder/references/checklists/aaa-visual-scorecard.md +0 -0
  430. /package/{skills → dist/skills/en-US}/threejs-aaa-graphics-builder/references/checklists/material-lighting-quality.md +0 -0
  431. /package/{skills → dist/skills/en-US}/threejs-aaa-graphics-builder/references/checklists/performance-safe-visual-detail.md +0 -0
  432. /package/{skills → dist/skills/en-US}/threejs-aaa-graphics-builder/references/checklists/procedural-model-quality.md +0 -0
  433. /package/{skills → dist/skills/en-US}/threejs-aaa-graphics-builder/references/checklists/technical-art-quality.md +0 -0
  434. /package/{skills → dist/skills/en-US}/threejs-aaa-graphics-builder/references/graphics-prompt-templates.md +0 -0
  435. /package/{skills → dist/skills/en-US}/threejs-aaa-graphics-builder/references/implementation-blueprint.md +0 -0
  436. /package/{skills → dist/skills/en-US}/threejs-aaa-graphics-builder/references/model-recipes.md +0 -0
  437. /package/{skills → dist/skills/en-US}/threejs-aaa-graphics-builder/references/render-recipes.md +0 -0
  438. /package/{skills → dist/skills/en-US}/threejs-aaa-graphics-builder/references/shader-cookbook.md +0 -0
  439. /package/{skills → dist/skills/en-US}/threejs-aaa-graphics-builder/references/technical-art.md +0 -0
  440. /package/{skills → dist/skills/en-US}/threejs-aaa-graphics-builder/references/visual-scorecard.md +0 -0
  441. /package/{skills → dist/skills/zh-CN}/evomeme-canvas/SKILL.md +0 -0
  442. /package/{skills → dist/skills/zh-CN}/evomeme-canvas/agents/openai.yaml +0 -0
  443. /package/{skills → dist/skills/zh-CN}/evomeme-cloud-fs/agents/openai.yaml +0 -0
  444. /package/{skills → dist/skills/zh-CN}/evomeme-cover/agents/openai.yaml +0 -0
  445. /package/{skills → dist/skills/zh-CN}/evomeme-game-maker/agents/openai.yaml +0 -0
  446. /package/{skills → dist/skills/zh-CN}/evomeme-login/agents/openai.yaml +0 -0
  447. /package/{skills → dist/skills/zh-CN}/evomeme-native-bridge-sdk/SKILL.md +0 -0
  448. /package/{skills → dist/skills/zh-CN}/evomeme-native-bridge-sdk/agents/openai.yaml +0 -0
  449. /package/{skills → dist/skills/zh-CN}/evomeme-tos-upload/agents/openai.yaml +0 -0
  450. /package/{skills → dist/skills/zh-CN}/game-art/SKILL.md +0 -0
  451. /package/{skills → dist/skills/zh-CN}/game-art/agents/openai.yaml +0 -0
  452. /package/{skills → dist/skills/zh-CN}/game-asset-image-generator/agents/openai.yaml +0 -0
  453. /package/{skills → dist/skills/zh-CN}/game-demo/SKILL.md +0 -0
  454. /package/{skills → dist/skills/zh-CN}/game-demo/agents/openai.yaml +0 -0
  455. /package/{skills → dist/skills/zh-CN}/game-maker/SKILL.md +0 -0
  456. /package/{skills → dist/skills/zh-CN}/game-maker/agents/openai.yaml +0 -0
  457. /package/{skills → dist/skills/zh-CN}/game-test/SKILL.md +0 -0
  458. /package/{skills → dist/skills/zh-CN}/game-test/agents/openai.yaml +0 -0
  459. /package/{skills → dist/skills/zh-CN}/game-ui/agents/openai.yaml +0 -0
  460. /package/{skills → dist/skills/zh-CN}/phaser-2d-game/SKILL.md +0 -0
  461. /package/{skills → dist/skills/zh-CN}/phaser-2d-game/agents/openai.yaml +0 -0
  462. /package/{skills → dist/skills/zh-CN}/phaser-2d-game/references/engine-performance.md +0 -0
  463. /package/{skills → dist/skills/zh-CN}/phaser-2d-game/references/engine-physics.md +0 -0
  464. /package/{skills → dist/skills/zh-CN}/phaser-2d-game/references/engine-runtime.md +0 -0
  465. /package/{skills → dist/skills/zh-CN}/phaser-2d-game/references/engine-spritesheets.md +0 -0
  466. /package/{skills → dist/skills/zh-CN}/phaser-2d-game/references/engine-tilemaps.md +0 -0
  467. /package/{skills → dist/skills/zh-CN}/phaser-2d-game/references/file-structure.md +0 -0
  468. /package/{skills → dist/skills/zh-CN}/phaser-2d-graphics-builder/SKILL.md +0 -0
  469. /package/{skills → dist/skills/zh-CN}/phaser-2d-graphics-builder/agents/openai.yaml +0 -0
  470. /package/{skills → dist/skills/zh-CN}/threejs-3d-game/agents/openai.yaml +0 -0
  471. /package/{skills → dist/skills/zh-CN}/threejs-3d-game/references/engine-audio.md +0 -0
  472. /package/{skills → dist/skills/zh-CN}/threejs-3d-game/references/engine-input-camera.md +0 -0
  473. /package/{skills → dist/skills/zh-CN}/threejs-3d-game/references/engine-performance.md +0 -0
  474. /package/{skills → dist/skills/zh-CN}/threejs-3d-game/references/engine-physics.md +0 -0
  475. /package/{skills → dist/skills/zh-CN}/threejs-3d-game/references/engine-runtime.md +0 -0
  476. /package/{skills → dist/skills/zh-CN}/threejs-3d-game/references/file-structure.md +0 -0
  477. /package/{skills → dist/skills/zh-CN}/threejs-aaa-graphics-builder/agents/openai.yaml +0 -0
@@ -0,0 +1,2520 @@
1
+ import { errorT } from '../../language/index.js'
2
+ import { t } from '../../language/index.js'
3
+ import { installFeatureStyles } from '../../ui/controls.js'
4
+ /**
5
+ * 地图编辑视图:负责 PixiJS/@pixi/tilemap 渲染、画笔交互和挂载实例。
6
+ * 可测试的数据与图层逻辑位于 model.js,对外导出入口位于 export.js。
7
+ */
8
+
9
+ import {
10
+ AUTO_TERRAIN_ATLAS_SIDE,
11
+ ATLAS_CELL_SIZE,
12
+ ATLAS_COLUMNS,
13
+ GRID_BLOCK_SIZE,
14
+ MAJOR_GRID_SCREEN_WIDTH,
15
+ MAX_ATLAS_ZOOM,
16
+ MAX_EXPORT_SIDE,
17
+ MAX_GRID_SIDE,
18
+ MIN_ATLAS_ZOOM,
19
+ MIN_MINOR_GRID_CELL_SCREEN_SIZE,
20
+ MINOR_GRID_SCREEN_WIDTH,
21
+ TILE_DRAG_TYPE,
22
+ TILE_MAP_NODE_MIN_WIDTH,
23
+ TILE_MAP_NODE_WIDTH,
24
+ autoTerrainMaskAt,
25
+ autoTerrainTileIndex,
26
+ clampInteger,
27
+ createLayerId,
28
+ createPaletteFromSources,
29
+ normalizeCells,
30
+ normalizePalette,
31
+ normalizeSource,
32
+ normalizeTileMapData,
33
+ reorderLayersByVisualDrop,
34
+ safeName,
35
+ tileMapNodeHeightForWidth,
36
+ } from '../../map/model.js'
37
+
38
+ export {
39
+ TILE_MAP_FOOTER_HEIGHT,
40
+ TILE_MAP_NODE_HEIGHT,
41
+ TILE_MAP_NODE_MIN_HEIGHT,
42
+ createPaletteFromSources,
43
+ createTileMapItem,
44
+ isTileMapItem,
45
+ normalizeTileMapData,
46
+ normalizeTileMapItem,
47
+ reorderLayersByVisualDrop,
48
+ serializeTileMapItem,
49
+ tileMapContentAspectRatio,
50
+ tileMapNodeHeightForWidth,
51
+ tileMapSummary,
52
+ autoTerrainMaskAt,
53
+ autoTerrainTileIndex,
54
+ } from '../../map/model.js'
55
+
56
+ const mountedViews = new Map()
57
+ let pixiRuntimePromise = null
58
+
59
+ function loadScriptOnce(src) {
60
+ const existing = document.querySelector(`script[data-tile-map-runtime="${src}"]`)
61
+ if (existing) {
62
+ if (existing.dataset.loaded === 'true') return Promise.resolve()
63
+ return new Promise((resolve, reject) => {
64
+ existing.addEventListener('load', resolve, { once: true })
65
+ existing.addEventListener(
66
+ 'error',
67
+ () => reject(new Error(`${t('map.tileMapEditor.text001')}${src}`)),
68
+ { once: true },
69
+ )
70
+ })
71
+ }
72
+ return new Promise((resolve, reject) => {
73
+ const script = document.createElement('script')
74
+ script.src = src
75
+ script.dataset.tileMapRuntime = src
76
+ script.addEventListener(
77
+ 'load',
78
+ () => {
79
+ script.dataset.loaded = 'true'
80
+ resolve()
81
+ },
82
+ { once: true },
83
+ )
84
+ script.addEventListener(
85
+ 'error',
86
+ () => reject(new Error(`${t('map.tileMapEditor.text001')}${src}`)),
87
+ { once: true },
88
+ )
89
+ document.head.appendChild(script)
90
+ })
91
+ }
92
+
93
+ async function ensurePixiRuntime() {
94
+ if (globalThis.PIXI?.Application && globalThis.PIXI?.tilemap?.CompositeTilemap) return
95
+ if (!pixiRuntimePromise) {
96
+ pixiRuntimePromise = (async () => {
97
+ if (!globalThis.PIXI?.Application) await loadScriptOnce('/vendor/pixi.min.js')
98
+ if (!globalThis.PIXI?.tilemap?.CompositeTilemap)
99
+ await loadScriptOnce('/vendor/pixi-tilemap.js')
100
+ if (!globalThis.PIXI?.Application || !globalThis.PIXI?.tilemap?.CompositeTilemap) {
101
+ throw new Error(errorT('map.tileMapEditor.text002'))
102
+ }
103
+ })().catch((error) => {
104
+ pixiRuntimePromise = null
105
+ throw error
106
+ })
107
+ }
108
+ await pixiRuntimePromise
109
+ }
110
+
111
+ function makeElement(tagName, className, text) {
112
+ const element = document.createElement(tagName)
113
+ if (className) element.className = className
114
+ if (text !== undefined) element.textContent = text
115
+ return element
116
+ }
117
+
118
+ function makeButton(text, className = 'tile-map-button') {
119
+ const button = makeElement('button', className, text)
120
+ button.type = 'button'
121
+ return button
122
+ }
123
+
124
+ function sourceTileFrame(source, tileIndex) {
125
+ const column = tileIndex % source.columns
126
+ const row = Math.floor(tileIndex / source.columns)
127
+ return {
128
+ x: source.marginX + column * (source.tileWidth + source.spacingX),
129
+ y: source.marginY + row * (source.tileHeight + source.spacingY),
130
+ width: source.tileWidth,
131
+ height: source.tileHeight,
132
+ }
133
+ }
134
+
135
+ function suggestedSplit(width, height) {
136
+ if (width === height) return { columns: 1, rows: 1 }
137
+ if (width > height && width % height === 0 && width / height <= 32) {
138
+ return { columns: width / height, rows: 1 }
139
+ }
140
+ if (height > width && height % width === 0 && height / width <= 32) {
141
+ return { columns: 1, rows: height / width }
142
+ }
143
+ return { columns: 1, rows: 1 }
144
+ }
145
+
146
+ function containedTileSetLayout(sourceWidth, sourceHeight, columns, rows, tileSize) {
147
+ const canvasWidth = columns * tileSize
148
+ const canvasHeight = rows * tileSize
149
+ const scale = Math.min(canvasWidth / sourceWidth, canvasHeight / sourceHeight)
150
+ const width = Math.max(1, Math.min(canvasWidth, Math.round(sourceWidth * scale)))
151
+ const height = Math.max(1, Math.min(canvasHeight, Math.round(sourceHeight * scale)))
152
+ return {
153
+ width,
154
+ height,
155
+ offsetX: Math.floor((canvasWidth - width) / 2),
156
+ offsetY: Math.floor((canvasHeight - height) / 2),
157
+ canvasWidth,
158
+ canvasHeight,
159
+ }
160
+ }
161
+
162
+ function drawTransparencyGrid(context, width, height) {
163
+ const size = 12
164
+ context.fillStyle = '#f0f2f5'
165
+ context.fillRect(0, 0, width, height)
166
+ context.fillStyle = '#cfd4da'
167
+ for (let y = 0; y < height; y += size) {
168
+ for (let x = 0; x < width; x += size) {
169
+ if ((Math.floor(x / size) + Math.floor(y / size)) % 2 === 0) continue
170
+ context.fillRect(x, y, size, size)
171
+ }
172
+ }
173
+ }
174
+
175
+ function loadHtmlImage(url) {
176
+ return new Promise((resolve, reject) => {
177
+ const image = new Image()
178
+ image.crossOrigin = 'anonymous'
179
+ image.decoding = 'async'
180
+ image.onload = () => resolve(image)
181
+ image.onerror = () => reject(new Error(errorT('map.tileMapEditor.text003')))
182
+ image.src = url
183
+ })
184
+ }
185
+
186
+ function downloadBlob(blob, filename) {
187
+ const url = URL.createObjectURL(blob)
188
+ const anchor = document.createElement('a')
189
+ anchor.href = url
190
+ anchor.download = filename
191
+ document.body.appendChild(anchor)
192
+ anchor.click()
193
+ anchor.remove()
194
+ setTimeout(() => URL.revokeObjectURL(url), 1000)
195
+ }
196
+
197
+ function safeFileStem(name) {
198
+ return safeName(name, 'tile-map')
199
+ .replace(/[\\/:*?"<>|]+/g, '-')
200
+ .replace(/\s+/g, '-')
201
+ .slice(0, 80)
202
+ }
203
+
204
+ class TileMapView {
205
+ constructor(container, item, callbacks) {
206
+ this.container = container
207
+ this.item = item
208
+ this.map = item.tileMap = normalizeTileMapData(item.tileMap)
209
+ this.callbacks = callbacks
210
+ this.app = null
211
+ this.backgroundLayer = null
212
+ this.tileLayer = null
213
+ this.gridLayer = null
214
+ this.hoverLayer = null
215
+ this.baseTextureCache = new Map()
216
+ this.imageCache = new Map()
217
+ this.renderSequence = 0
218
+ this.disposed = false
219
+ this.painting = false
220
+ this.strokeChanged = false
221
+ this.strokeBefore = null
222
+ this.outerScale = 1
223
+ this.history = []
224
+ this.future = []
225
+ this.resizeObserver = null
226
+ this.resizeFrame = 0
227
+ this.inspectorRoot = null
228
+ this.selectedStatus = null
229
+ this.stageHost = null
230
+ this.palette = null
231
+ this.tilesetCanvas = null
232
+ this.tilesetSelectionCanvas = null
233
+ this.brushContextMenu = null
234
+ this.paletteEmpty = null
235
+ this.atlasCells = []
236
+ this.atlasDrag = null
237
+ this.atlasZoom = 1
238
+ this.atlasZoomLabel = null
239
+ this.paletteRenderSequence = 0
240
+ this.layerRenderSequence = 0
241
+ this.layerList = null
242
+ this.draggedLayerId = null
243
+ this.undoButton = null
244
+ this.redoButton = null
245
+ this.brushButton = null
246
+ this.terrainBrushButton = null
247
+ this.eraserButton = null
248
+ this.specLabel = null
249
+ }
250
+
251
+ activate() {
252
+ this.callbacks.onActivate?.(this.item.id)
253
+ }
254
+
255
+ reportError(error) {
256
+ const message = String(error?.message || error || t('map.tileMapEditor.text004'))
257
+ if (this.selectedStatus) this.selectedStatus.textContent = message
258
+ this.callbacks.onError?.(message)
259
+ }
260
+
261
+ activeLayer() {
262
+ return (
263
+ this.map.layers.find((layer) => layer.id === this.map.activeLayerId) || this.map.layers[0]
264
+ )
265
+ }
266
+
267
+ layerSnapshot() {
268
+ return {
269
+ activeLayerId: this.map.activeLayerId,
270
+ layers: this.map.layers.map((layer) => ({ ...layer, cells: { ...layer.cells } })),
271
+ }
272
+ }
273
+
274
+ restoreLayerSnapshot(snapshot) {
275
+ this.map.layers = snapshot.layers.map((layer) => ({ ...layer, cells: { ...layer.cells } }))
276
+ this.map.activeLayerId = this.map.layers.some((layer) => layer.id === snapshot.activeLayerId)
277
+ ? snapshot.activeLayerId
278
+ : this.map.layers[this.map.layers.length - 1].id
279
+ }
280
+
281
+ notifyChange() {
282
+ this.item.tileMap = this.map
283
+ this.updateHeaderSummary()
284
+ this.callbacks.onChange?.(this.item.id, this.map)
285
+ }
286
+
287
+ build() {
288
+ const root = makeElement('div', 'tile-map-editor')
289
+ root.dataset.tileMapEditor = this.item.id
290
+ this.stageHost = makeElement('div', 'tile-map-stage')
291
+ const dragHandle = makeElement('div', 'canvas-node-caption tile-map-drag-handle')
292
+ const mapName = makeElement(
293
+ 'span',
294
+ 'tile-map-footer-name',
295
+ this.item.folderName || this.item.name || t('map.tileMapEditor.text005'),
296
+ )
297
+ this.specLabel = makeElement(
298
+ 'span',
299
+ 'tile-map-footer-spec',
300
+ `${t('map.tileMapEditor.text006')}${this.map.cellSize}${t('map.tileMapEditor.text006.part1')}${this.map.cellSize}${t('map.tileMapEditor.text006.part2')}`,
301
+ )
302
+ dragHandle.append(mapName, this.specLabel)
303
+ dragHandle.title = t('map.tileMapEditor.text007')
304
+ root.append(this.stageHost, dragHandle)
305
+ this.container.appendChild(root)
306
+
307
+ root.addEventListener('pointerdown', (event) => {
308
+ this.activate()
309
+ if (event.button === 0 && !event.target.closest('.tile-map-drag-handle'))
310
+ event.stopPropagation()
311
+ })
312
+ void this.mountPixi()
313
+ }
314
+
315
+ mountInspector(container) {
316
+ this.closeBrushContextMenu()
317
+ this.inspectorRoot?.remove()
318
+ this.atlasDrag = null
319
+
320
+ const root = makeElement('section', 'tile-map-inspector')
321
+ root.dataset.tileMapInspector = this.item.id
322
+ this.inspectorRoot = root
323
+
324
+ const iconButton = (label, paths) => {
325
+ const button = makeButton('', 'tile-map-icon-button')
326
+ button.title = label
327
+ button.setAttribute('aria-label', label)
328
+ button.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true">${paths}</svg>`
329
+ return button
330
+ }
331
+
332
+ const layerSection = makeElement('section', 'tile-map-layer-section')
333
+ const layerHeader = makeElement('div', 'tile-map-layer-header')
334
+ layerHeader.appendChild(makeElement('strong', null, t('map.tileMapEditor.text008')))
335
+ const addLayerButton = makeButton(t('map.tileMapEditor.text009'))
336
+ this.layerList = makeElement('div', 'tile-map-layer-list')
337
+ layerHeader.appendChild(addLayerButton)
338
+ layerSection.append(layerHeader, this.layerList)
339
+
340
+ const toolActions = makeElement('div', 'tile-map-tool-strip')
341
+ this.brushButton = iconButton(
342
+ t('map.tileMapEditor.text010'),
343
+ '<path d="m14.7 2.3 7 7-11 11H3v-7Z"/><path d="m13 4 7 7"/>',
344
+ )
345
+ this.terrainBrushButton = iconButton(
346
+ t('map.tileMapEditor.text011'),
347
+ '<path d="M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4z"/><path d="m15 18 2 2 4-5"/>',
348
+ )
349
+ this.eraserButton = iconButton(
350
+ t('map.tileMapEditor.text012'),
351
+ '<path d="m7 21-4-4a2 2 0 0 1 0-3L13.5 3.5a2 2 0 0 1 3 0l4 4a2 2 0 0 1 0 3L10 21Z"/><path d="m6.5 10 7 7"/><path d="M7 21h14"/>',
352
+ )
353
+ this.undoButton = iconButton(
354
+ t('map.tileMapEditor.text013'),
355
+ '<path d="M9 7 4 12l5 5"/><path d="M20 17a7 7 0 0 0-7-7H4"/>',
356
+ )
357
+ this.redoButton = iconButton(
358
+ t('map.tileMapEditor.text014'),
359
+ '<path d="m15 7 5 5-5 5"/><path d="M4 17a7 7 0 0 1 7-7h9"/>',
360
+ )
361
+ toolActions.append(
362
+ this.brushButton,
363
+ this.terrainBrushButton,
364
+ this.eraserButton,
365
+ this.undoButton,
366
+ this.redoButton,
367
+ )
368
+
369
+ const gridFields = makeElement('div', 'tile-map-grid-fields')
370
+ const columnsInput = this.numberField(
371
+ t('map.tileMapEditor.text015'),
372
+ this.map.columns,
373
+ 1,
374
+ MAX_GRID_SIDE,
375
+ )
376
+ const rowsInput = this.numberField(
377
+ t('map.tileMapEditor.text016'),
378
+ this.map.rows,
379
+ 1,
380
+ MAX_GRID_SIDE,
381
+ )
382
+ gridFields.append(columnsInput.label, rowsInput.label)
383
+
384
+ this.palette = makeElement('div', 'tile-map-palette')
385
+ this.tilesetCanvas = document.createElement('canvas')
386
+ this.tilesetCanvas.className = 'tile-map-tileset-canvas'
387
+ this.tilesetCanvas.tabIndex = 0
388
+ this.tilesetSelectionCanvas = document.createElement('canvas')
389
+ this.tilesetSelectionCanvas.className = 'tile-map-tileset-selection'
390
+ this.tilesetSelectionCanvas.setAttribute('aria-hidden', 'true')
391
+ this.paletteEmpty = makeElement('p', 'tile-map-palette-empty', t('map.tileMapEditor.text017'))
392
+ this.palette.append(this.paletteEmpty, this.tilesetCanvas, this.tilesetSelectionCanvas)
393
+
394
+ this.selectedStatus = null
395
+ root.append(this.palette, toolActions, gridFields, layerSection)
396
+ container.appendChild(root)
397
+
398
+ for (const control of root.querySelectorAll('button, input')) {
399
+ control.addEventListener('pointerdown', (event) => event.stopPropagation())
400
+ }
401
+
402
+ const openDroppedImage = async (event) => {
403
+ event.preventDefault()
404
+ event.stopPropagation()
405
+ root.classList.remove('is-image-over')
406
+ const raw = event.dataTransfer?.getData(TILE_DRAG_TYPE)
407
+ if (!raw) {
408
+ this.reportError(t('map.tileMapEditor.text018'))
409
+ return
410
+ }
411
+ try {
412
+ const reference = JSON.parse(raw)
413
+ if (!reference?.id) throw new Error(errorT('map.tileMapEditor.text019'))
414
+ await this.openSliceDialog(reference)
415
+ } catch (error) {
416
+ this.reportError(error)
417
+ }
418
+ }
419
+ root.addEventListener('dragenter', (event) => {
420
+ if (!event.dataTransfer?.types?.includes(TILE_DRAG_TYPE)) return
421
+ event.preventDefault()
422
+ root.classList.add('is-image-over')
423
+ })
424
+ root.addEventListener('dragover', (event) => {
425
+ if (!event.dataTransfer?.types?.includes(TILE_DRAG_TYPE)) return
426
+ event.preventDefault()
427
+ event.dataTransfer.dropEffect = 'copy'
428
+ })
429
+ root.addEventListener('dragleave', (event) => {
430
+ if (!root.contains(event.relatedTarget)) root.classList.remove('is-image-over')
431
+ })
432
+ root.addEventListener('drop', openDroppedImage)
433
+
434
+ this.brushButton.addEventListener('click', () => this.setTool('brush'))
435
+ this.terrainBrushButton.addEventListener('click', () => this.setTool('terrain'))
436
+ this.eraserButton.addEventListener('click', () => this.setTool('eraser'))
437
+ this.undoButton.addEventListener('click', () => this.undo())
438
+ this.redoButton.addEventListener('click', () => this.redo())
439
+ addLayerButton.addEventListener('click', () => this.addLayer())
440
+
441
+ const commitGrid = () => {
442
+ const columns = clampInteger(columnsInput.input.value, 1, MAX_GRID_SIDE, this.map.columns)
443
+ const rows = clampInteger(rowsInput.input.value, 1, MAX_GRID_SIDE, this.map.rows)
444
+ const cellSize = this.map.cellSize
445
+ if (columns * cellSize > MAX_EXPORT_SIDE || rows * cellSize > MAX_EXPORT_SIDE) {
446
+ this.reportError(
447
+ `${t('map.tileMapEditor.text020')}${MAX_EXPORT_SIDE}${t('map.tileMapEditor.text020.part1')}${MAX_EXPORT_SIDE}${t('map.tileMapEditor.text020.part2')}`,
448
+ )
449
+ columnsInput.input.value = String(this.map.columns)
450
+ rowsInput.input.value = String(this.map.rows)
451
+ return
452
+ }
453
+ const changed =
454
+ columns !== this.map.columns || rows !== this.map.rows || cellSize !== this.map.cellSize
455
+ if (!changed) return
456
+ this.map.columns = columns
457
+ this.map.rows = rows
458
+ this.map.cellSize = cellSize
459
+ this.map.layers = this.map.layers.map((layer) => ({
460
+ ...layer,
461
+ cells: normalizeCells(layer.cells, columns, rows, this.map.sources),
462
+ }))
463
+ this.future = []
464
+ this.lockNodeAspectRatio()
465
+ this.scheduleRendererResize()
466
+ void this.renderTiles()
467
+ this.notifyChange()
468
+ }
469
+ columnsInput.input.addEventListener('change', commitGrid)
470
+ rowsInput.input.addEventListener('change', commitGrid)
471
+
472
+ this.setTool(this.map.activeTool, false)
473
+ this.bindTilesetSelection()
474
+ this.setAtlasZoom(this.atlasZoom)
475
+ this.renderPalette()
476
+ this.renderLayerList()
477
+ this.updateHeaderSummary()
478
+ this.updateHistoryButtons()
479
+ }
480
+
481
+ numberField(labelText, value, minimum, maximum, suffix = '') {
482
+ const label = makeElement('label', 'tile-map-number-field')
483
+ label.appendChild(makeElement('span', null, labelText))
484
+ const input = document.createElement('input')
485
+ input.type = 'number'
486
+ input.min = String(minimum)
487
+ input.max = String(maximum)
488
+ input.step = '1'
489
+ input.value = String(value)
490
+ label.appendChild(input)
491
+ if (suffix) label.appendChild(makeElement('span', 'tile-map-unit', suffix))
492
+ return { label, input }
493
+ }
494
+
495
+ addLayer() {
496
+ this.pushHistory(this.layerSnapshot())
497
+ const layer = {
498
+ id: createLayerId(),
499
+ name: `${t('map.tileMapEditor.text021')}${this.map.layers.length + 1}`,
500
+ cells: {},
501
+ }
502
+ this.map.layers.push(layer)
503
+ this.map.activeLayerId = layer.id
504
+ this.future = []
505
+ this.renderLayerList()
506
+ this.renderTiles()
507
+ this.notifyChange()
508
+ }
509
+
510
+ deleteLayer(layerId) {
511
+ if (this.map.layers.length <= 1) return
512
+ const index = this.map.layers.findIndex((layer) => layer.id === layerId)
513
+ if (index < 0) return
514
+ const layer = this.map.layers[index]
515
+ if (
516
+ Object.keys(layer.cells).length &&
517
+ !window.confirm(
518
+ `${t('map.tileMapEditor.text022')}${layer.name}${t('map.tileMapEditor.text022.part1')}`,
519
+ )
520
+ )
521
+ return
522
+ this.pushHistory(this.layerSnapshot())
523
+ this.map.layers.splice(index, 1)
524
+ if (this.map.activeLayerId === layerId) {
525
+ this.map.activeLayerId = this.map.layers[Math.min(index, this.map.layers.length - 1)].id
526
+ }
527
+ this.future = []
528
+ this.renderLayerList()
529
+ this.renderTiles()
530
+ this.notifyChange()
531
+ }
532
+
533
+ selectLayer(layerId) {
534
+ if (
535
+ layerId === this.map.activeLayerId ||
536
+ !this.map.layers.some((layer) => layer.id === layerId)
537
+ )
538
+ return
539
+ this.map.activeLayerId = layerId
540
+ this.renderLayerList()
541
+ this.notifyChange()
542
+ }
543
+
544
+ renameLayer(layerId, value) {
545
+ const layer = this.map.layers.find((candidate) => candidate.id === layerId)
546
+ if (!layer) return
547
+ const name = safeName(value, layer.name)
548
+ if (name === layer.name) {
549
+ this.renderLayerList()
550
+ return
551
+ }
552
+ this.pushHistory(this.layerSnapshot())
553
+ layer.name = name
554
+ this.future = []
555
+ this.renderLayerList()
556
+ this.notifyChange()
557
+ }
558
+
559
+ clearLayerDragStyles() {
560
+ if (!this.layerList) return
561
+ for (const row of this.layerList.children) {
562
+ row.classList.remove('is-dragging', 'is-drop-before', 'is-drop-after')
563
+ }
564
+ }
565
+
566
+ reorderLayer(draggedLayerId, targetLayerId, placeAfter) {
567
+ const reordered = reorderLayersByVisualDrop(
568
+ this.map.layers,
569
+ draggedLayerId,
570
+ targetLayerId,
571
+ placeAfter,
572
+ )
573
+ if (
574
+ reordered === this.map.layers ||
575
+ reordered.every((layer, index) => layer === this.map.layers[index])
576
+ )
577
+ return
578
+ this.pushHistory(this.layerSnapshot())
579
+ this.map.layers = reordered
580
+ this.future = []
581
+ this.renderLayerList()
582
+ this.renderTiles()
583
+ this.notifyChange()
584
+ }
585
+
586
+ renderLayerList() {
587
+ if (!this.layerList) return
588
+ this.layerList.replaceChildren()
589
+ for (const layer of [...this.map.layers].reverse()) {
590
+ const row = makeElement('div', 'tile-map-layer-item')
591
+ row.classList.toggle('is-active', layer.id === this.map.activeLayerId)
592
+ row.dataset.layerId = layer.id
593
+ row.tabIndex = 0
594
+
595
+ const reorderHandle = makeElement('span', 'tile-map-layer-drag-handle', '⠿')
596
+ reorderHandle.draggable = true
597
+ reorderHandle.title = `${t('map.tileMapEditor.text023')}${layer.name}${t('map.tileMapEditor.text023.part1')}`
598
+ reorderHandle.setAttribute('aria-label', reorderHandle.title)
599
+ reorderHandle.addEventListener('pointerdown', (event) => event.stopPropagation())
600
+ reorderHandle.addEventListener('click', (event) => event.stopPropagation())
601
+ reorderHandle.addEventListener('dragstart', (event) => {
602
+ this.draggedLayerId = layer.id
603
+ row.classList.add('is-dragging')
604
+ event.dataTransfer.effectAllowed = 'move'
605
+ event.dataTransfer.setData('text/plain', layer.id)
606
+ })
607
+ reorderHandle.addEventListener('dragend', () => {
608
+ this.draggedLayerId = null
609
+ this.clearLayerDragStyles()
610
+ })
611
+
612
+ const thumbnail = document.createElement('canvas')
613
+ thumbnail.className = 'tile-map-layer-thumbnail'
614
+ thumbnail.width = 112
615
+ thumbnail.height = 72
616
+ thumbnail.setAttribute('aria-hidden', 'true')
617
+
618
+ const name = document.createElement('input')
619
+ name.type = 'text'
620
+ name.className = 'tile-map-layer-name'
621
+ name.value = layer.name
622
+ name.maxLength = 80
623
+ name.setAttribute('aria-label', t('map.tileMapEditor.text024'))
624
+ name.addEventListener('pointerdown', (event) => event.stopPropagation())
625
+ name.addEventListener('click', (event) => event.stopPropagation())
626
+ name.addEventListener('focus', () => {
627
+ if (this.map.activeLayerId === layer.id) return
628
+ this.map.activeLayerId = layer.id
629
+ for (const item of this.layerList?.children || []) {
630
+ item.classList.toggle('is-active', item.dataset.layerId === layer.id)
631
+ }
632
+ this.notifyChange()
633
+ })
634
+ name.addEventListener('change', () => this.renameLayer(layer.id, name.value))
635
+ name.addEventListener('keydown', (event) => {
636
+ if (event.key === 'Enter') name.blur()
637
+ })
638
+
639
+ const remove = makeButton('×', 'tile-map-layer-delete')
640
+ remove.disabled = this.map.layers.length <= 1
641
+ remove.title = remove.disabled
642
+ ? t('map.tileMapEditor.text025')
643
+ : `${t('map.tileMapEditor.text026')}${layer.name}${t('map.tileMapEditor.text026.part1')}`
644
+ remove.setAttribute('aria-label', remove.title)
645
+ remove.addEventListener('pointerdown', (event) => event.stopPropagation())
646
+ remove.addEventListener('click', (event) => {
647
+ event.stopPropagation()
648
+ this.deleteLayer(layer.id)
649
+ })
650
+
651
+ row.append(reorderHandle, thumbnail, name, remove)
652
+ row.addEventListener('click', () => this.selectLayer(layer.id))
653
+ row.addEventListener('dragover', (event) => {
654
+ if (!this.draggedLayerId || this.draggedLayerId === layer.id) return
655
+ event.preventDefault()
656
+ event.dataTransfer.dropEffect = 'move'
657
+ const placeAfter = event.clientY >= row.getBoundingClientRect().top + row.offsetHeight / 2
658
+ this.clearLayerDragStyles()
659
+ row.classList.add(placeAfter ? 'is-drop-after' : 'is-drop-before')
660
+ const draggedRow = this.layerList?.querySelector(
661
+ `[data-layer-id="${CSS.escape(this.draggedLayerId)}"]`,
662
+ )
663
+ draggedRow?.classList.add('is-dragging')
664
+ })
665
+ row.addEventListener('drop', (event) => {
666
+ if (!this.draggedLayerId || this.draggedLayerId === layer.id) return
667
+ event.preventDefault()
668
+ const draggedLayerId = this.draggedLayerId
669
+ const placeAfter = event.clientY >= row.getBoundingClientRect().top + row.offsetHeight / 2
670
+ this.draggedLayerId = null
671
+ this.clearLayerDragStyles()
672
+ this.reorderLayer(draggedLayerId, layer.id, placeAfter)
673
+ })
674
+ row.addEventListener('keydown', (event) => {
675
+ if ((event.key === 'Enter' || event.key === ' ') && event.target === row) {
676
+ event.preventDefault()
677
+ this.selectLayer(layer.id)
678
+ }
679
+ })
680
+ this.layerList.appendChild(row)
681
+ }
682
+ void this.renderLayerThumbnails()
683
+ }
684
+
685
+ async renderLayerThumbnails() {
686
+ if (!this.layerList) return
687
+ const sequence = ++this.layerRenderSequence
688
+ const sourceMap = new Map(this.map.sources.map((source) => [source.id, source]))
689
+ const usedSourceIds = [
690
+ ...new Set(
691
+ this.map.layers.flatMap((layer) => Object.values(layer.cells).map((cell) => cell.sourceId)),
692
+ ),
693
+ ]
694
+ const loaded = new Map()
695
+ await Promise.all(
696
+ usedSourceIds.map(async (sourceId) => {
697
+ const source = sourceMap.get(sourceId)
698
+ if (!source) return
699
+ try {
700
+ loaded.set(sourceId, await this.htmlImageForSource(source))
701
+ } catch {
702
+ /* Missing assets stay transparent in the thumbnail. */
703
+ }
704
+ }),
705
+ )
706
+ if (this.disposed || sequence !== this.layerRenderSequence || !this.layerList) return
707
+
708
+ for (const row of this.layerList.children) {
709
+ const layer = this.map.layers.find((candidate) => candidate.id === row.dataset.layerId)
710
+ const canvas = row.querySelector('.tile-map-layer-thumbnail')
711
+ if (!layer || !canvas) continue
712
+ const context = canvas.getContext('2d')
713
+ context.imageSmoothingEnabled = false
714
+ context.clearRect(0, 0, canvas.width, canvas.height)
715
+ context.fillStyle = '#101720'
716
+ context.fillRect(0, 0, canvas.width, canvas.height)
717
+ const scale = Math.min(canvas.width / this.map.columns, canvas.height / this.map.rows)
718
+ const tileWidth = Math.max(0.5, scale)
719
+ const tileHeight = Math.max(0.5, scale)
720
+ const offsetX = (canvas.width - this.map.columns * scale) / 2
721
+ const offsetY = (canvas.height - this.map.rows * scale) / 2
722
+ for (const [key, cell] of Object.entries(layer.cells)) {
723
+ const source = sourceMap.get(cell.sourceId)
724
+ const image = loaded.get(cell.sourceId)
725
+ if (!source || !image) continue
726
+ const [x, y] = key.split(',').map(Number)
727
+ const frame = sourceTileFrame(source, cell.tileIndex)
728
+ context.drawImage(
729
+ image,
730
+ frame.x,
731
+ frame.y,
732
+ frame.width,
733
+ frame.height,
734
+ offsetX + x * scale,
735
+ offsetY + y * scale,
736
+ tileWidth,
737
+ tileHeight,
738
+ )
739
+ }
740
+ }
741
+ }
742
+
743
+ async mountPixi() {
744
+ await ensurePixiRuntime()
745
+ const PIXI = globalThis.PIXI
746
+ const CompositeTilemap = PIXI?.tilemap?.CompositeTilemap
747
+ if (!PIXI?.Application || !CompositeTilemap) {
748
+ this.reportError(t('map.tileMapEditor.text027'))
749
+ return
750
+ }
751
+ try {
752
+ const app = new PIXI.Application()
753
+ await app.init({
754
+ resizeTo: this.stageHost,
755
+ preference: 'webgl',
756
+ backgroundColor: 0x10151c,
757
+ antialias: false,
758
+ roundPixels: true,
759
+ autoDensity: true,
760
+ resolution: Math.min(2, window.devicePixelRatio || 1),
761
+ })
762
+ if (this.disposed) {
763
+ app.destroy(true)
764
+ return
765
+ }
766
+ this.app = app
767
+ app.canvas.classList.add('tile-map-pixi-canvas')
768
+ app.canvas.style.touchAction = 'none'
769
+ this.stageHost.prepend(app.canvas)
770
+ this.backgroundLayer = new PIXI.Graphics()
771
+ this.tileLayer = new PIXI.Container()
772
+ this.gridLayer = new PIXI.Graphics()
773
+ this.hoverLayer = new PIXI.Graphics()
774
+ app.stage.sortableChildren = true
775
+ this.backgroundLayer.zIndex = 0
776
+ this.gridLayer.zIndex = 10
777
+ this.tileLayer.zIndex = 20
778
+ this.hoverLayer.zIndex = 30
779
+ app.stage.addChild(this.backgroundLayer, this.tileLayer, this.gridLayer, this.hoverLayer)
780
+ this.bindPixiPointerEvents(app.canvas)
781
+ this.resizeObserver = new ResizeObserver(() => this.scheduleRendererResize())
782
+ this.resizeObserver.observe(this.stageHost)
783
+ this.resizeRendererToHost()
784
+ await this.renderTiles()
785
+ } catch (error) {
786
+ this.reportError(error)
787
+ }
788
+ }
789
+
790
+ bindPixiPointerEvents(canvas) {
791
+ canvas.addEventListener('contextmenu', (event) => event.preventDefault())
792
+ canvas.addEventListener('pointerdown', (event) => {
793
+ if (event.button !== 0) return
794
+ event.preventDefault()
795
+ event.stopPropagation()
796
+ this.activate()
797
+ this.painting = true
798
+ this.strokeChanged = false
799
+ this.strokeBefore = this.layerSnapshot()
800
+ canvas.setPointerCapture?.(event.pointerId)
801
+ this.paintAtEvent(event)
802
+ })
803
+ canvas.addEventListener('pointermove', (event) => {
804
+ const point = this.eventCell(event)
805
+ this.drawHover(point)
806
+ if (!this.painting) return
807
+ event.preventDefault()
808
+ this.paintAtEvent(event)
809
+ })
810
+ const finish = (event) => {
811
+ if (!this.painting) return
812
+ this.painting = false
813
+ if (this.strokeChanged && this.strokeBefore) {
814
+ this.pushHistory(this.strokeBefore)
815
+ this.future = []
816
+ this.renderLayerList()
817
+ this.notifyChange()
818
+ }
819
+ this.strokeBefore = null
820
+ this.strokeChanged = false
821
+ }
822
+ canvas.addEventListener('pointerup', finish)
823
+ canvas.addEventListener('pointercancel', finish)
824
+ canvas.addEventListener('pointerleave', () => {
825
+ this.hoverLayer?.clear()
826
+ finish()
827
+ })
828
+ }
829
+
830
+ clientToScreen(clientX, clientY) {
831
+ if (!this.app?.screen || !this.app.canvas) return null
832
+ const rect = this.app.canvas.getBoundingClientRect()
833
+ if (rect.width <= 0 || rect.height <= 0) return null
834
+ // The outer canvas world is CSS-scaled. Convert visual client coordinates
835
+ // back to Pixi's logical renderer coordinates before applying world transforms.
836
+ return {
837
+ x: ((clientX - rect.left) * this.app.screen.width) / rect.width,
838
+ y: ((clientY - rect.top) * this.app.screen.height) / rect.height,
839
+ }
840
+ }
841
+
842
+ eventCell(event) {
843
+ if (!this.app) return null
844
+ const screenPoint = this.clientToScreen(event.clientX, event.clientY)
845
+ if (!screenPoint) return null
846
+ const cellWidth = this.app.screen.width / this.map.columns
847
+ const cellHeight = this.app.screen.height / this.map.rows
848
+ const x = Math.floor(screenPoint.x / cellWidth)
849
+ const y = Math.floor(screenPoint.y / cellHeight)
850
+ if (x < 0 || y < 0 || x >= this.map.columns || y >= this.map.rows) return null
851
+ return { x, y }
852
+ }
853
+
854
+ paintAtEvent(event) {
855
+ const point = this.eventCell(event)
856
+ if (!point) return
857
+ const key = `${point.x},${point.y}`
858
+ const layer = this.activeLayer()
859
+ const erase = this.map.activeTool === 'eraser'
860
+ if (erase) {
861
+ if (!layer.cells[key]) return
862
+ delete layer.cells[key]
863
+ } else if (this.map.activeTool === 'terrain') {
864
+ const brushImage = this.activeTerrainBrushImage()
865
+ if (!brushImage) {
866
+ if (this.selectedStatus) this.selectedStatus.textContent = t('map.tileMapEditor.text028')
867
+ return
868
+ }
869
+ this.paintAutoTerrainAt(layer, brushImage, point.x, point.y)
870
+ } else {
871
+ if (!this.map.activeBrush) {
872
+ if (this.selectedStatus) this.selectedStatus.textContent = t('map.tileMapEditor.text029')
873
+ return
874
+ }
875
+ const brush = this.map.activeBrush
876
+ const brushColumns = brush.columns || 1
877
+ const brushRows = brush.rows || 1
878
+ const primarySource = this.map.sources.find((candidate) => candidate.id === brush.sourceId)
879
+ const sourceMap = new Map(this.map.sources.map((source) => [source.id, source]))
880
+ const sourceColumn = primarySource ? brush.tileIndex % primarySource.columns : 0
881
+ const sourceRow = primarySource ? Math.floor(brush.tileIndex / primarySource.columns) : 0
882
+ let changed = false
883
+ for (let brushY = 0; brushY < brushRows; brushY += 1) {
884
+ for (let brushX = 0; brushX < brushColumns; brushX += 1) {
885
+ const brushTile = Array.isArray(brush.tiles)
886
+ ? brush.tiles[brushY * brushColumns + brushX]
887
+ : primarySource
888
+ ? {
889
+ sourceId: primarySource.id,
890
+ tileIndex: (sourceRow + brushY) * primarySource.columns + sourceColumn + brushX,
891
+ }
892
+ : null
893
+ if (!brushTile || !sourceMap.has(brushTile.sourceId)) continue
894
+ const targetX = point.x + brushX
895
+ const targetY = point.y + brushY
896
+ if (targetX >= this.map.columns || targetY >= this.map.rows) continue
897
+ const targetKey = `${targetX},${targetY}`
898
+ const next = {
899
+ sourceId: brushTile.sourceId,
900
+ tileIndex: brushTile.tileIndex,
901
+ }
902
+ const current = layer.cells[targetKey]
903
+ if (current?.sourceId === next.sourceId && current?.tileIndex === next.tileIndex) continue
904
+ layer.cells[targetKey] = next
905
+ changed = true
906
+ }
907
+ }
908
+ if (!changed) return
909
+ }
910
+ this.strokeChanged = true
911
+ this.renderTiles()
912
+ this.updateHeaderSummary()
913
+ }
914
+
915
+ activeTerrainBrushImage() {
916
+ const sourceIds = new Set(this.map.sources.map((source) => source.id))
917
+ return (
918
+ this.map.brushImages.find(
919
+ (brushImage) =>
920
+ brushImage.id === this.map.activeTerrainBrushId &&
921
+ brushImage.columns === AUTO_TERRAIN_ATLAS_SIDE &&
922
+ brushImage.rows === AUTO_TERRAIN_ATLAS_SIDE &&
923
+ brushImage.tileSourceIds.length >= AUTO_TERRAIN_ATLAS_SIDE ** 2 &&
924
+ brushImage.tileSourceIds
925
+ .slice(0, AUTO_TERRAIN_ATLAS_SIDE ** 2)
926
+ .every((sourceId) => sourceIds.has(sourceId)),
927
+ ) || null
928
+ )
929
+ }
930
+
931
+ terrainBrushImageForTile(tile) {
932
+ if (!tile?.sourceId) return null
933
+ return (
934
+ this.map.brushImages.find(
935
+ (brushImage) =>
936
+ brushImage.columns === AUTO_TERRAIN_ATLAS_SIDE &&
937
+ brushImage.rows === AUTO_TERRAIN_ATLAS_SIDE &&
938
+ brushImage.tileSourceIds.length >= AUTO_TERRAIN_ATLAS_SIDE ** 2 &&
939
+ brushImage.tileSourceIds.includes(tile.sourceId),
940
+ ) || null
941
+ )
942
+ }
943
+
944
+ paintAutoTerrainAt(layer, brushImage, terrainX, terrainY) {
945
+ const pureTerrainSourceId = brushImage.tileSourceIds[autoTerrainTileIndex(15)]
946
+ if (!pureTerrainSourceId) return
947
+ const occupiedCells = new Set(
948
+ Object.entries(layer.cells)
949
+ .filter(([, cell]) => cell.sourceId === pureTerrainSourceId)
950
+ .map(([key]) => key),
951
+ )
952
+ occupiedCells.add(`${terrainX},${terrainY}`)
953
+ for (let y = terrainY - 1; y <= terrainY + 1; y += 1) {
954
+ for (let x = terrainX - 1; x <= terrainX + 1; x += 1) {
955
+ if (x < 0 || y < 0 || x >= this.map.columns || y >= this.map.rows) continue
956
+ const key = `${x},${y}`
957
+ const mask = autoTerrainMaskAt(occupiedCells, x, y)
958
+ if (!mask) continue
959
+ const tileSourceId = brushImage.tileSourceIds[autoTerrainTileIndex(mask)]
960
+ if (!tileSourceId || !this.map.sources.some((source) => source.id === tileSourceId))
961
+ continue
962
+ layer.cells[key] = { sourceId: tileSourceId, tileIndex: 0 }
963
+ }
964
+ }
965
+ }
966
+
967
+ setTool(tool, notify = true) {
968
+ this.map.activeTool = ['brush', 'terrain', 'eraser'].includes(tool) ? tool : 'brush'
969
+ this.brushButton?.classList.toggle('is-active', this.map.activeTool === 'brush')
970
+ this.terrainBrushButton?.classList.toggle('is-active', this.map.activeTool === 'terrain')
971
+ this.eraserButton?.classList.toggle('is-active', this.map.activeTool === 'eraser')
972
+ this.drawAtlasSelection()
973
+ if (notify) this.notifyChange()
974
+ }
975
+
976
+ pushHistory(snapshot) {
977
+ this.history.push(snapshot)
978
+ if (this.history.length > 50) this.history.shift()
979
+ this.updateHistoryButtons()
980
+ }
981
+
982
+ undo() {
983
+ const previous = this.history.pop()
984
+ if (!previous) return
985
+ this.future.push(this.layerSnapshot())
986
+ this.restoreLayerSnapshot(previous)
987
+ this.renderTiles()
988
+ this.renderLayerList()
989
+ this.notifyChange()
990
+ this.updateHistoryButtons()
991
+ }
992
+
993
+ redo() {
994
+ const next = this.future.pop()
995
+ if (!next) return
996
+ this.history.push(this.layerSnapshot())
997
+ this.restoreLayerSnapshot(next)
998
+ this.renderTiles()
999
+ this.renderLayerList()
1000
+ this.notifyChange()
1001
+ this.updateHistoryButtons()
1002
+ }
1003
+
1004
+ updateHistoryButtons() {
1005
+ if (this.undoButton) this.undoButton.disabled = !this.history.length
1006
+ if (this.redoButton) this.redoButton.disabled = !this.future.length
1007
+ }
1008
+
1009
+ updateHeaderSummary() {
1010
+ if (this.specLabel) {
1011
+ this.specLabel.textContent = `${t('map.tileMapEditor.text006')}${this.map.cellSize}${t('map.tileMapEditor.text006.part1')}${this.map.cellSize}${t('map.tileMapEditor.text006.part2')}`
1012
+ }
1013
+ }
1014
+
1015
+ drawGrid() {
1016
+ if (!this.app?.screen || !this.gridLayer || !this.backgroundLayer) return
1017
+ const width = this.app.screen.width
1018
+ const height = this.app.screen.height
1019
+ const cellWidth = width / this.map.columns
1020
+ const cellHeight = height / this.map.rows
1021
+ this.backgroundLayer.clear()
1022
+ this.backgroundLayer.rect(0, 0, width, height).fill({ color: 0x17202b, alpha: 1 })
1023
+ this.gridLayer.clear()
1024
+ const scaleX = Math.max(0.0001, Math.abs(this.outerScale || 1))
1025
+ const scaleY = scaleX
1026
+ const renderResolution = Math.max(1, Number(this.app.renderer?.resolution) || 1)
1027
+ const renderPixel = 1 / renderResolution
1028
+ const pixelAlignedStrip = (center, requestedSize) => {
1029
+ const size = Math.max(
1030
+ renderPixel,
1031
+ Math.ceil(requestedSize * renderResolution) / renderResolution,
1032
+ )
1033
+ return {
1034
+ start: Math.round((center - size / 2) * renderResolution) / renderResolution,
1035
+ size,
1036
+ }
1037
+ }
1038
+ // Filled strips are stable under fractional WebGL transforms. A nominal
1039
+ // 1px stroke can fall fully between samples with antialiasing disabled.
1040
+ // Clamp and snap each strip to physical renderer pixels before outer CSS scaling.
1041
+ const minorWidth = Math.max(renderPixel, MINOR_GRID_SCREEN_WIDTH / scaleX)
1042
+ const minorHeight = Math.max(renderPixel, MINOR_GRID_SCREEN_WIDTH / scaleY)
1043
+ const showMinorGrid =
1044
+ Math.min(cellWidth, cellHeight) * scaleX >= MIN_MINOR_GRID_CELL_SCREEN_SIZE
1045
+ if (showMinorGrid) {
1046
+ for (let x = 1; x < this.map.columns; x += 1) {
1047
+ if (x % GRID_BLOCK_SIZE === 0) continue
1048
+ const strip = pixelAlignedStrip(x * cellWidth, minorWidth)
1049
+ this.gridLayer.rect(strip.start, 0, strip.size, height)
1050
+ }
1051
+ for (let y = 1; y < this.map.rows; y += 1) {
1052
+ if (y % GRID_BLOCK_SIZE === 0) continue
1053
+ const strip = pixelAlignedStrip(y * cellHeight, minorHeight)
1054
+ this.gridLayer.rect(0, strip.start, width, strip.size)
1055
+ }
1056
+ this.gridLayer.fill({ color: 0x718096, alpha: 0.32 })
1057
+ }
1058
+
1059
+ const majorWidth = Math.max(renderPixel, MAJOR_GRID_SCREEN_WIDTH / scaleX)
1060
+ const majorHeight = Math.max(renderPixel, MAJOR_GRID_SCREEN_WIDTH / scaleY)
1061
+ for (let x = 0; x <= this.map.columns; x += GRID_BLOCK_SIZE) {
1062
+ const strip = pixelAlignedStrip(x * cellWidth, majorWidth)
1063
+ this.gridLayer.rect(strip.start, 0, strip.size, height)
1064
+ }
1065
+ if (this.map.columns % GRID_BLOCK_SIZE !== 0) {
1066
+ const strip = pixelAlignedStrip(width, majorWidth)
1067
+ this.gridLayer.rect(strip.start, 0, strip.size, height)
1068
+ }
1069
+ for (let y = 0; y <= this.map.rows; y += GRID_BLOCK_SIZE) {
1070
+ const strip = pixelAlignedStrip(y * cellHeight, majorHeight)
1071
+ this.gridLayer.rect(0, strip.start, width, strip.size)
1072
+ }
1073
+ if (this.map.rows % GRID_BLOCK_SIZE !== 0) {
1074
+ const strip = pixelAlignedStrip(height, majorHeight)
1075
+ this.gridLayer.rect(0, strip.start, width, strip.size)
1076
+ }
1077
+ this.gridLayer.fill({ color: 0xa0aec0, alpha: 0.56 })
1078
+ }
1079
+
1080
+ setOuterScale(value) {
1081
+ const next = Math.max(0.01, Math.abs(Number(value) || 1))
1082
+ if (Math.abs(next - this.outerScale) < 0.0001) return
1083
+ this.outerScale = next
1084
+ this.drawGrid()
1085
+ }
1086
+
1087
+ drawHover(point) {
1088
+ if (!this.app?.screen || !this.hoverLayer) return
1089
+ this.hoverLayer.clear()
1090
+ if (!point) return
1091
+ const color = this.map.activeTool === 'eraser' ? 0xff6b6b : 0x4fd1c5
1092
+ const brushColumns = this.map.activeTool === 'brush' ? this.map.activeBrush?.columns || 1 : 1
1093
+ const brushRows = this.map.activeTool === 'brush' ? this.map.activeBrush?.rows || 1 : 1
1094
+ const cellWidth = this.app.screen.width / this.map.columns
1095
+ const cellHeight = this.app.screen.height / this.map.rows
1096
+ const width = Math.min(brushColumns, this.map.columns - point.x) * cellWidth
1097
+ const height = Math.min(brushRows, this.map.rows - point.y) * cellHeight
1098
+ this.hoverLayer
1099
+ .rect(point.x * cellWidth, point.y * cellHeight, width, height)
1100
+ .fill({ color, alpha: 0.22 })
1101
+ .stroke({ color, alpha: 0.9, width: 2 / Math.max(0.2, this.outerScale || 1) })
1102
+ }
1103
+
1104
+ scheduleRendererResize() {
1105
+ if (this.resizeFrame || this.disposed) return
1106
+ this.resizeFrame = requestAnimationFrame(() => {
1107
+ this.resizeFrame = 0
1108
+ this.resizeRendererToHost()
1109
+ })
1110
+ }
1111
+
1112
+ resizeRendererToHost() {
1113
+ if (!this.app?.renderer || !this.stageHost) return
1114
+ const width = Math.max(1, Math.round(this.stageHost.clientWidth))
1115
+ const height = Math.max(1, Math.round(this.stageHost.clientHeight))
1116
+ if (this.app.screen.width !== width || this.app.screen.height !== height) {
1117
+ this.app.renderer.resize(width, height)
1118
+ }
1119
+ this.layoutMapToStage()
1120
+ }
1121
+
1122
+ layoutMapToStage() {
1123
+ if (!this.app) return
1124
+ this.drawGrid()
1125
+ this.resizeRenderedTiles()
1126
+ }
1127
+
1128
+ resizeRenderedTiles() {
1129
+ if (!this.app?.screen || !this.tileLayer) return
1130
+ const cellWidth = this.app.screen.width / this.map.columns
1131
+ const cellHeight = this.app.screen.height / this.map.rows
1132
+ for (const mapLayer of this.tileLayer.children) {
1133
+ for (const sourceLayer of mapLayer.children || []) {
1134
+ const sourceWidth = Number(sourceLayer.tileMapSourceWidth)
1135
+ const sourceHeight = Number(sourceLayer.tileMapSourceHeight)
1136
+ if (sourceWidth > 0 && sourceHeight > 0) {
1137
+ sourceLayer.scale.set(cellWidth / sourceWidth, cellHeight / sourceHeight)
1138
+ }
1139
+ }
1140
+ }
1141
+ }
1142
+
1143
+ lockNodeAspectRatio() {
1144
+ const width = Math.max(
1145
+ TILE_MAP_NODE_MIN_WIDTH,
1146
+ this.container.offsetWidth || Number(this.item.width) || TILE_MAP_NODE_WIDTH,
1147
+ )
1148
+ const height = tileMapNodeHeightForWidth(this.map, width)
1149
+ this.item.width = Math.round(width)
1150
+ this.item.height = height
1151
+ this.container.style.width = `${this.item.width}px`
1152
+ this.container.style.height = `${height}px`
1153
+ this.container.classList.add('is-manual-size')
1154
+ }
1155
+
1156
+ async sourceBaseTexture(source) {
1157
+ if (this.baseTextureCache.has(source.id)) return this.baseTextureCache.get(source.id)
1158
+ const PIXI = globalThis.PIXI
1159
+ const url = this.callbacks.resolveImageUrl(source.assetId)
1160
+ const promise = PIXI.Assets.load(url).then((texture) => {
1161
+ if (texture?.source) texture.source.scaleMode = 'nearest'
1162
+ return texture
1163
+ })
1164
+ this.baseTextureCache.set(source.id, promise)
1165
+ return promise
1166
+ }
1167
+
1168
+ async renderTiles() {
1169
+ if (!this.app?.screen || !this.tileLayer) return
1170
+ const sequence = ++this.renderSequence
1171
+ const layerEntries = this.map.layers.map((layer) => Object.entries(layer.cells))
1172
+ const sourceMap = new Map(this.map.sources.map((source) => [source.id, source]))
1173
+ const usedSourceIds = [
1174
+ ...new Set(layerEntries.flatMap((entries) => entries.map(([, cell]) => cell.sourceId))),
1175
+ ]
1176
+ const prepared = new Map()
1177
+ await Promise.all(
1178
+ usedSourceIds.map(async (sourceId) => {
1179
+ const source = sourceMap.get(sourceId)
1180
+ if (!source) return
1181
+ try {
1182
+ prepared.set(sourceId, await this.sourceBaseTexture(source))
1183
+ } catch {
1184
+ /* A missing source remains empty until the asset is restored. */
1185
+ }
1186
+ }),
1187
+ )
1188
+ if (this.disposed || sequence !== this.renderSequence || !this.tileLayer) return
1189
+ for (const child of this.tileLayer.removeChildren()) child.destroy?.({ children: true })
1190
+ const CompositeTilemap = globalThis.PIXI.tilemap.CompositeTilemap
1191
+ const cellWidth = this.app.screen.width / this.map.columns
1192
+ const cellHeight = this.app.screen.height / this.map.rows
1193
+ for (const entries of layerEntries) {
1194
+ const mapLayer = new globalThis.PIXI.Container()
1195
+ const layerSourceIds = [...new Set(entries.map(([, cell]) => cell.sourceId))]
1196
+ for (const sourceId of layerSourceIds) {
1197
+ const source = sourceMap.get(sourceId)
1198
+ const texture = prepared.get(sourceId)
1199
+ if (!source || !texture) continue
1200
+ const sourceLayer = new CompositeTilemap()
1201
+ sourceLayer.roundPixels = true
1202
+ sourceLayer.tileMapSourceWidth = source.tileWidth
1203
+ sourceLayer.tileMapSourceHeight = source.tileHeight
1204
+ sourceLayer.scale.set(cellWidth / source.tileWidth, cellHeight / source.tileHeight)
1205
+ for (const [key, cell] of entries) {
1206
+ if (cell.sourceId !== sourceId) continue
1207
+ const [x, y] = key.split(',').map(Number)
1208
+ const frame = sourceTileFrame(source, cell.tileIndex)
1209
+ sourceLayer.tile(texture, x * source.tileWidth, y * source.tileHeight, {
1210
+ u: frame.x,
1211
+ v: frame.y,
1212
+ tileWidth: source.tileWidth,
1213
+ tileHeight: source.tileHeight,
1214
+ })
1215
+ }
1216
+ mapLayer.addChild(sourceLayer)
1217
+ }
1218
+ this.tileLayer.addChild(mapLayer)
1219
+ }
1220
+ }
1221
+
1222
+ setAtlasZoom(value) {
1223
+ this.atlasZoom = Math.min(MAX_ATLAS_ZOOM, Math.max(MIN_ATLAS_ZOOM, Number(value) || 1))
1224
+ if (this.tilesetCanvas) this.tilesetCanvas.style.width = `${this.atlasZoom * 100}%`
1225
+ if (this.tilesetSelectionCanvas)
1226
+ this.tilesetSelectionCanvas.style.width = `${this.atlasZoom * 100}%`
1227
+ if (this.atlasZoomLabel)
1228
+ this.atlasZoomLabel.textContent = `${Math.round(this.atlasZoom * 100)}%`
1229
+ }
1230
+
1231
+ bindTilesetSelection() {
1232
+ const canvas = this.tilesetCanvas
1233
+ if (!canvas) return
1234
+ canvas.addEventListener('contextmenu', (event) => {
1235
+ event.preventDefault()
1236
+ event.stopPropagation()
1237
+ this.closeBrushContextMenu()
1238
+ const cell = this.atlasCellAtEvent(event)
1239
+ const bounds = this.atlasSelectionBounds()
1240
+ if (!cell || !bounds) return
1241
+ if (
1242
+ cell.column < bounds.startColumn ||
1243
+ cell.column > bounds.endColumn ||
1244
+ cell.row < bounds.startRow ||
1245
+ cell.row > bounds.endRow
1246
+ )
1247
+ return
1248
+ this.openBrushContextMenu(event.clientX, event.clientY, bounds)
1249
+ })
1250
+ canvas.addEventListener('pointerdown', (event) => {
1251
+ if (event.button !== 0) return
1252
+ this.closeBrushContextMenu()
1253
+ const cell = this.atlasCellAtEvent(event)
1254
+ if (!cell?.tile) return
1255
+ event.preventDefault()
1256
+ event.stopPropagation()
1257
+ this.activate()
1258
+ if (this.map.activeTool === 'terrain') {
1259
+ const brushImage = this.terrainBrushImageForTile(cell.tile)
1260
+ const bounds = brushImage ? this.terrainBrushPaletteBounds(brushImage) : null
1261
+ if (!brushImage || !bounds) {
1262
+ this.reportError(t('map.tileMapEditor.text030'))
1263
+ return
1264
+ }
1265
+ this.map.activeTerrainBrushId = brushImage.id
1266
+ this.drawAtlasSelection()
1267
+ this.notifyChange()
1268
+ return
1269
+ }
1270
+ this.atlasDrag = {
1271
+ pointerId: event.pointerId,
1272
+ startColumn: cell.column,
1273
+ startRow: cell.row,
1274
+ endColumn: cell.column,
1275
+ endRow: cell.row,
1276
+ }
1277
+ canvas.setPointerCapture?.(event.pointerId)
1278
+ this.drawAtlasSelection()
1279
+ })
1280
+ canvas.addEventListener('pointermove', (event) => {
1281
+ if (!this.atlasDrag || this.atlasDrag.pointerId !== event.pointerId) return
1282
+ const cell = this.atlasCellAtEvent(event)
1283
+ if (!cell) return
1284
+ this.atlasDrag.endColumn = cell.column
1285
+ this.atlasDrag.endRow = cell.row
1286
+ this.drawAtlasSelection()
1287
+ })
1288
+ const finish = (event, commit) => {
1289
+ if (!this.atlasDrag || this.atlasDrag.pointerId !== event.pointerId) return
1290
+ const drag = this.atlasDrag
1291
+ this.atlasDrag = null
1292
+ if (commit) {
1293
+ const startColumn = Math.min(drag.startColumn, drag.endColumn)
1294
+ const startRow = Math.min(drag.startRow, drag.endRow)
1295
+ const endColumn = Math.max(drag.startColumn, drag.endColumn)
1296
+ const endRow = Math.max(drag.startRow, drag.endRow)
1297
+ const columns = endColumn - startColumn + 1
1298
+ const rows = endRow - startRow + 1
1299
+ const tiles = []
1300
+ for (let row = startRow; row <= endRow; row += 1) {
1301
+ for (let column = startColumn; column <= endColumn; column += 1) {
1302
+ const tile = this.atlasCells[row * ATLAS_COLUMNS + column]
1303
+ tiles.push(tile ? { sourceId: tile.sourceId, tileIndex: tile.tileIndex } : null)
1304
+ }
1305
+ }
1306
+ const firstTile = tiles.find(Boolean)
1307
+ if (firstTile) {
1308
+ this.map.activeBrush = {
1309
+ sourceId: firstTile.sourceId,
1310
+ tileIndex: firstTile.tileIndex,
1311
+ columns,
1312
+ rows,
1313
+ atlasIndex: startRow * ATLAS_COLUMNS + startColumn,
1314
+ tiles,
1315
+ }
1316
+ this.setTool('brush', false)
1317
+ if (this.selectedStatus) {
1318
+ this.selectedStatus.textContent = `${t('map.tileMapEditor.text031')}${this.map.activeBrush.columns}${t('map.tileMapEditor.text031.part1')}${this.map.activeBrush.rows}${t('map.tileMapEditor.text031.part2')}`
1319
+ }
1320
+ this.notifyChange()
1321
+ }
1322
+ }
1323
+ this.drawAtlasSelection()
1324
+ }
1325
+ canvas.addEventListener('pointerup', (event) => finish(event, true))
1326
+ canvas.addEventListener('pointercancel', (event) => finish(event, false))
1327
+ }
1328
+
1329
+ atlasCellAtEvent(event) {
1330
+ const canvas = this.tilesetCanvas
1331
+ if (!canvas || !this.atlasCells.length) return null
1332
+ const rect = canvas.getBoundingClientRect()
1333
+ if (rect.width <= 0 || rect.height <= 0) return null
1334
+ const column = Math.floor(
1335
+ ((event.clientX - rect.left) * canvas.width) / rect.width / ATLAS_CELL_SIZE,
1336
+ )
1337
+ const row = Math.floor(
1338
+ ((event.clientY - rect.top) * canvas.height) / rect.height / ATLAS_CELL_SIZE,
1339
+ )
1340
+ const atlasRows = canvas.height / ATLAS_CELL_SIZE
1341
+ if (column < 0 || column >= ATLAS_COLUMNS || row < 0 || row >= atlasRows) return null
1342
+ return {
1343
+ tile: this.atlasCells[row * ATLAS_COLUMNS + column] || null,
1344
+ column,
1345
+ row,
1346
+ }
1347
+ }
1348
+
1349
+ atlasSelectionBounds(brush = this.atlasDrag || this.map.activeBrush) {
1350
+ if (!this.atlasDrag && this.map.activeTool === 'terrain') {
1351
+ return this.terrainBrushPaletteBounds(this.activeTerrainBrushImage())
1352
+ }
1353
+ if (!brush) return null
1354
+ if (this.atlasDrag && brush === this.atlasDrag) {
1355
+ return {
1356
+ startColumn: Math.min(brush.startColumn, brush.endColumn),
1357
+ startRow: Math.min(brush.startRow, brush.endRow),
1358
+ endColumn: Math.max(brush.startColumn, brush.endColumn),
1359
+ endRow: Math.max(brush.startRow, brush.endRow),
1360
+ }
1361
+ }
1362
+ const savedAtlasIndex = Number(brush.atlasIndex)
1363
+ const fallbackAtlasIndex = this.atlasCells.findIndex(
1364
+ (tile) => tile?.sourceId === brush.sourceId && tile.tileIndex === brush.tileIndex,
1365
+ )
1366
+ const atlasIndex =
1367
+ brush.atlasIndex !== null &&
1368
+ brush.atlasIndex !== undefined &&
1369
+ Number.isInteger(savedAtlasIndex) &&
1370
+ savedAtlasIndex >= 0
1371
+ ? savedAtlasIndex
1372
+ : fallbackAtlasIndex
1373
+ if (atlasIndex < 0) return null
1374
+ const startColumn = atlasIndex % ATLAS_COLUMNS
1375
+ const startRow = Math.floor(atlasIndex / ATLAS_COLUMNS)
1376
+ return {
1377
+ startColumn,
1378
+ startRow,
1379
+ endColumn: startColumn + (brush.columns || 1) - 1,
1380
+ endRow: startRow + (brush.rows || 1) - 1,
1381
+ }
1382
+ }
1383
+
1384
+ terrainBrushPaletteBounds(brushImage) {
1385
+ if (
1386
+ !brushImage ||
1387
+ brushImage.columns !== AUTO_TERRAIN_ATLAS_SIDE ||
1388
+ brushImage.rows !== AUTO_TERRAIN_ATLAS_SIDE
1389
+ ) {
1390
+ return null
1391
+ }
1392
+ const sourceIds = new Set(brushImage.tileSourceIds.slice(0, AUTO_TERRAIN_ATLAS_SIDE ** 2))
1393
+ const positions = []
1394
+ for (let index = 0; index < this.atlasCells.length; index += 1) {
1395
+ if (!sourceIds.has(this.atlasCells[index]?.sourceId)) continue
1396
+ positions.push({ column: index % ATLAS_COLUMNS, row: Math.floor(index / ATLAS_COLUMNS) })
1397
+ }
1398
+ if (positions.length !== AUTO_TERRAIN_ATLAS_SIDE ** 2) return null
1399
+ const startColumn = Math.min(...positions.map((position) => position.column))
1400
+ const startRow = Math.min(...positions.map((position) => position.row))
1401
+ const endColumn = Math.max(...positions.map((position) => position.column))
1402
+ const endRow = Math.max(...positions.map((position) => position.row))
1403
+ if (
1404
+ endColumn - startColumn + 1 !== AUTO_TERRAIN_ATLAS_SIDE ||
1405
+ endRow - startRow + 1 !== AUTO_TERRAIN_ATLAS_SIDE
1406
+ )
1407
+ return null
1408
+ return { startColumn, startRow, endColumn, endRow }
1409
+ }
1410
+
1411
+ drawAtlasSelection() {
1412
+ const canvas = this.tilesetSelectionCanvas
1413
+ if (!canvas) return
1414
+ const context = canvas.getContext('2d')
1415
+ context.clearRect(0, 0, canvas.width, canvas.height)
1416
+ const bounds = this.atlasSelectionBounds()
1417
+ if (!bounds) return
1418
+ const atlasLineScale = ATLAS_CELL_SIZE / 32
1419
+ const x = bounds.startColumn * ATLAS_CELL_SIZE
1420
+ const y = bounds.startRow * ATLAS_CELL_SIZE
1421
+ const width = (bounds.endColumn - bounds.startColumn + 1) * ATLAS_CELL_SIZE
1422
+ const height = (bounds.endRow - bounds.startRow + 1) * ATLAS_CELL_SIZE
1423
+ const autoTerrain = this.map.activeTool === 'terrain' && !this.atlasDrag
1424
+ context.fillStyle = autoTerrain ? 'rgba(61, 214, 198, 0.18)' : 'rgba(255, 55, 55, 0.18)'
1425
+ context.strokeStyle = autoTerrain ? '#3dd6c6' : '#ff3535'
1426
+ context.lineWidth = 3 * atlasLineScale
1427
+ context.fillRect(x, y, width, height)
1428
+ context.strokeRect(
1429
+ x + 1.5 * atlasLineScale,
1430
+ y + 1.5 * atlasLineScale,
1431
+ width - 3 * atlasLineScale,
1432
+ height - 3 * atlasLineScale,
1433
+ )
1434
+ }
1435
+
1436
+ closeBrushContextMenu() {
1437
+ if (!this.brushContextMenu) return
1438
+ document.removeEventListener('pointerdown', this.brushContextMenu._outsideHandler, true)
1439
+ this.brushContextMenu.remove()
1440
+ this.brushContextMenu = null
1441
+ }
1442
+
1443
+ openBrushContextMenu(clientX, clientY, bounds) {
1444
+ this.closeBrushContextMenu()
1445
+ const menu = makeElement('div', 'tile-map-brush-context-menu')
1446
+ const remove = makeButton(t('map.tileMapEditor.text032'))
1447
+ menu.appendChild(remove)
1448
+ document.body.appendChild(menu)
1449
+ const rect = menu.getBoundingClientRect()
1450
+ menu.style.left = `${Math.max(8, Math.min(clientX, window.innerWidth - rect.width - 8))}px`
1451
+ menu.style.top = `${Math.max(8, Math.min(clientY, window.innerHeight - rect.height - 8))}px`
1452
+ menu._outsideHandler = (event) => {
1453
+ if (!menu.contains(event.target)) this.closeBrushContextMenu()
1454
+ }
1455
+ document.addEventListener('pointerdown', menu._outsideHandler, true)
1456
+ remove.addEventListener('click', async () => {
1457
+ this.closeBrushContextMenu()
1458
+ try {
1459
+ await this.deleteBrushesInBounds(bounds)
1460
+ } catch (error) {
1461
+ this.reportError(error)
1462
+ }
1463
+ })
1464
+ this.brushContextMenu = menu
1465
+ }
1466
+
1467
+ async deleteBrushesInBounds(bounds) {
1468
+ const deletedSourceIds = new Set()
1469
+ const nextPaletteCells = { ...this.map.palette.cells }
1470
+ for (let row = bounds.startRow; row <= bounds.endRow; row += 1) {
1471
+ for (let column = bounds.startColumn; column <= bounds.endColumn; column += 1) {
1472
+ const key = `${column},${row}`
1473
+ const tile = nextPaletteCells[key]
1474
+ if (!tile) continue
1475
+ deletedSourceIds.add(tile.sourceId)
1476
+ delete nextPaletteCells[key]
1477
+ }
1478
+ }
1479
+ if (!deletedSourceIds.size) return
1480
+
1481
+ const visibleSourceIds = new Set(Object.values(nextPaletteCells).map((tile) => tile.sourceId))
1482
+ const emptiedBrushImages = this.map.brushImages.filter(
1483
+ (brushImage) => !brushImage.tileSourceIds.some((sourceId) => visibleSourceIds.has(sourceId)),
1484
+ )
1485
+ const invalidatedTerrainBrushIds = new Set(
1486
+ this.map.brushImages
1487
+ .filter(
1488
+ (brushImage) =>
1489
+ brushImage.columns === AUTO_TERRAIN_ATLAS_SIDE &&
1490
+ brushImage.rows === AUTO_TERRAIN_ATLAS_SIDE &&
1491
+ brushImage.tileSourceIds.some((sourceId) => deletedSourceIds.has(sourceId)),
1492
+ )
1493
+ .map((brushImage) => brushImage.id),
1494
+ )
1495
+ const deletedSources = this.map.sources.filter((source) => deletedSourceIds.has(source.id))
1496
+ await this.callbacks.deleteTileset?.({
1497
+ map_id: this.item.id,
1498
+ tile_ids: deletedSources.map((source) => source.assetId),
1499
+ tileset_ids: emptiedBrushImages.map((brushImage) => brushImage.assetId),
1500
+ })
1501
+
1502
+ this.map.layers = this.map.layers.map((layer) => ({
1503
+ ...layer,
1504
+ cells: Object.fromEntries(
1505
+ Object.entries(layer.cells).filter(([, cell]) => !deletedSourceIds.has(cell.sourceId)),
1506
+ ),
1507
+ }))
1508
+ this.map.sources = this.map.sources.flatMap((source) => {
1509
+ if (!deletedSourceIds.has(source.id)) return [source]
1510
+ this.baseTextureCache.delete(source.id)
1511
+ this.imageCache.delete(source.id)
1512
+ return []
1513
+ })
1514
+ const emptiedIds = new Set(emptiedBrushImages.map((brushImage) => brushImage.id))
1515
+ this.map.brushImages = this.map.brushImages.filter(
1516
+ (brushImage) => !emptiedIds.has(brushImage.id),
1517
+ )
1518
+ this.map.palette = { ...this.map.palette, cells: nextPaletteCells }
1519
+ this.map.activeBrush = null
1520
+ if (invalidatedTerrainBrushIds.has(this.map.activeTerrainBrushId))
1521
+ this.map.activeTerrainBrushId = null
1522
+ this.atlasDrag = null
1523
+ this.history = []
1524
+ this.future = []
1525
+ await this.renderPalette()
1526
+ await this.renderTiles()
1527
+ this.renderLayerList()
1528
+ this.updateHistoryButtons()
1529
+ this.notifyChange()
1530
+ }
1531
+
1532
+ async renderPalette() {
1533
+ const canvas = this.tilesetCanvas
1534
+ if (!this.palette || !canvas) return
1535
+ const sequence = ++this.paletteRenderSequence
1536
+ if (this.paletteEmpty) this.paletteEmpty.hidden = false
1537
+ this.map.palette = normalizePalette(
1538
+ this.map.palette,
1539
+ this.map.sources,
1540
+ this.map.brushImages,
1541
+ this.map.cellSize,
1542
+ )
1543
+ const atlasRows = this.map.palette.rows
1544
+ this.atlasCells = Array.from({ length: atlasRows * ATLAS_COLUMNS }, (_, index) => {
1545
+ const column = index % ATLAS_COLUMNS
1546
+ const row = Math.floor(index / ATLAS_COLUMNS)
1547
+ return this.map.palette.cells[`${column},${row}`] || null
1548
+ })
1549
+ canvas.width = ATLAS_COLUMNS * ATLAS_CELL_SIZE
1550
+ canvas.height = atlasRows * ATLAS_CELL_SIZE
1551
+ if (this.tilesetSelectionCanvas) {
1552
+ this.tilesetSelectionCanvas.width = canvas.width
1553
+ this.tilesetSelectionCanvas.height = canvas.height
1554
+ }
1555
+ this.setAtlasZoom(this.atlasZoom)
1556
+ const context = canvas.getContext('2d')
1557
+ context.imageSmoothingEnabled = false
1558
+ context.clearRect(0, 0, canvas.width, canvas.height)
1559
+
1560
+ const sourceMap = new Map(this.map.sources.map((source) => [source.id, source]))
1561
+ const loadedImages = new Map()
1562
+ await Promise.all(
1563
+ this.map.sources.map(async (source) => {
1564
+ try {
1565
+ loadedImages.set(source.id, await this.htmlImageForSource(source))
1566
+ } catch {
1567
+ /* Missing source images leave empty atlas cells. */
1568
+ }
1569
+ }),
1570
+ )
1571
+ if (this.disposed || sequence !== this.paletteRenderSequence) return
1572
+ for (let atlasIndex = 0; atlasIndex < this.atlasCells.length; atlasIndex += 1) {
1573
+ const tile = this.atlasCells[atlasIndex]
1574
+ if (!tile) continue
1575
+ const source = sourceMap.get(tile.sourceId)
1576
+ const image = loadedImages.get(tile.sourceId)
1577
+ if (!source || !image) continue
1578
+ const frame = sourceTileFrame(source, tile.tileIndex)
1579
+ context.drawImage(
1580
+ image,
1581
+ frame.x,
1582
+ frame.y,
1583
+ frame.width,
1584
+ frame.height,
1585
+ (atlasIndex % ATLAS_COLUMNS) * ATLAS_CELL_SIZE,
1586
+ Math.floor(atlasIndex / ATLAS_COLUMNS) * ATLAS_CELL_SIZE,
1587
+ ATLAS_CELL_SIZE,
1588
+ ATLAS_CELL_SIZE,
1589
+ )
1590
+ }
1591
+
1592
+ const atlasLineScale = ATLAS_CELL_SIZE / 32
1593
+ const atlasLineOffset = atlasLineScale / 2
1594
+ context.strokeStyle = 'rgba(255,255,255,0.28)'
1595
+ context.lineWidth = atlasLineScale
1596
+ for (let column = 0; column <= ATLAS_COLUMNS; column += 1) {
1597
+ context.beginPath()
1598
+ context.moveTo(column * ATLAS_CELL_SIZE + atlasLineOffset, 0)
1599
+ context.lineTo(column * ATLAS_CELL_SIZE + atlasLineOffset, atlasRows * ATLAS_CELL_SIZE)
1600
+ context.stroke()
1601
+ }
1602
+ for (let row = 0; row <= atlasRows; row += 1) {
1603
+ context.beginPath()
1604
+ context.moveTo(0, row * ATLAS_CELL_SIZE + atlasLineOffset)
1605
+ context.lineTo(ATLAS_COLUMNS * ATLAS_CELL_SIZE, row * ATLAS_CELL_SIZE + atlasLineOffset)
1606
+ context.stroke()
1607
+ }
1608
+
1609
+ this.drawAtlasSelection()
1610
+ }
1611
+
1612
+ async openSliceDialog(reference) {
1613
+ const url = reference.url || this.callbacks.resolveImageUrl(reference.id)
1614
+ const image = await loadHtmlImage(url)
1615
+ if (this.disposed) return
1616
+ const existing = this.map.brushImages.find(
1617
+ (brushImage) =>
1618
+ brushImage.sourceAssetId === reference.id || brushImage.assetId === reference.id,
1619
+ )
1620
+ const guessed = suggestedSplit(image.naturalWidth, image.naturalHeight)
1621
+ const initial = existing || {
1622
+ columns: guessed.columns,
1623
+ rows: guessed.rows,
1624
+ marginX: 0,
1625
+ marginY: 0,
1626
+ spacingX: 0,
1627
+ spacingY: 0,
1628
+ }
1629
+ const overlay = makeElement('div', 'tile-map-slice-overlay')
1630
+ const dialog = makeElement('form', 'tile-map-slice-dialog')
1631
+ const head = makeElement('div', 'tile-map-slice-head')
1632
+ head.append(
1633
+ makeElement(
1634
+ 'div',
1635
+ null,
1636
+ existing ? t('map.tileMapEditor.text033') : t('map.tileMapEditor.text034'),
1637
+ ),
1638
+ makeElement('small', null, `${image.naturalWidth} × ${image.naturalHeight} px`),
1639
+ )
1640
+ const preview = document.createElement('canvas')
1641
+ preview.className = 'tile-map-slice-preview'
1642
+ const fields = makeElement('div', 'tile-map-slice-fields')
1643
+ const columns = this.numberField(t('map.tileMapEditor.text035'), initial.columns, 1, 8)
1644
+ const rows = this.numberField(t('map.tileMapEditor.text036'), initial.rows, 1, 8)
1645
+ const marginX = this.numberField(
1646
+ t('map.tileMapEditor.text037'),
1647
+ initial.marginX,
1648
+ 0,
1649
+ image.naturalWidth - 1,
1650
+ 'px',
1651
+ )
1652
+ const marginY = this.numberField(
1653
+ t('map.tileMapEditor.text038'),
1654
+ initial.marginY,
1655
+ 0,
1656
+ image.naturalHeight - 1,
1657
+ 'px',
1658
+ )
1659
+ const spacingX = this.numberField(
1660
+ t('map.tileMapEditor.text039'),
1661
+ initial.spacingX,
1662
+ 0,
1663
+ image.naturalWidth - 1,
1664
+ 'px',
1665
+ )
1666
+ const spacingY = this.numberField(
1667
+ t('map.tileMapEditor.text040'),
1668
+ initial.spacingY,
1669
+ 0,
1670
+ image.naturalHeight - 1,
1671
+ 'px',
1672
+ )
1673
+ fields.append(
1674
+ columns.label,
1675
+ rows.label,
1676
+ marginX.label,
1677
+ marginY.label,
1678
+ spacingX.label,
1679
+ spacingY.label,
1680
+ )
1681
+ const validation = makeElement('div', 'tile-map-slice-validation')
1682
+ const actions = makeElement('div', 'tile-map-slice-actions')
1683
+ const cancel = makeButton(t('map.tileMapEditor.text041'))
1684
+ const confirm = makeButton(
1685
+ existing ? t('map.tileMapEditor.text042') : t('map.tileMapEditor.text043'),
1686
+ 'tile-map-button tile-map-button-primary',
1687
+ )
1688
+ confirm.type = 'submit'
1689
+ actions.append(cancel, confirm)
1690
+ dialog.append(head, preview, fields, validation, actions)
1691
+ overlay.appendChild(dialog)
1692
+ this.inspectorRoot?.appendChild(overlay)
1693
+
1694
+ const readConfig = () => {
1695
+ const cols = clampInteger(columns.input.value, 1, 8, 1)
1696
+ const rowCount = clampInteger(rows.input.value, 1, 8, 1)
1697
+ const mx = clampInteger(marginX.input.value, 0, image.naturalWidth - 1, 0)
1698
+ const my = clampInteger(marginY.input.value, 0, image.naturalHeight - 1, 0)
1699
+ const sx = clampInteger(spacingX.input.value, 0, image.naturalWidth - 1, 0)
1700
+ const sy = clampInteger(spacingY.input.value, 0, image.naturalHeight - 1, 0)
1701
+ const availableWidth = image.naturalWidth - mx * 2 - sx * (cols - 1)
1702
+ const availableHeight = image.naturalHeight - my * 2 - sy * (rowCount - 1)
1703
+ const tileWidth = availableWidth / cols
1704
+ const tileHeight = availableHeight / rowCount
1705
+ const valid =
1706
+ tileWidth >= 1 &&
1707
+ tileHeight >= 1 &&
1708
+ Number.isInteger(tileWidth) &&
1709
+ Number.isInteger(tileHeight)
1710
+ const fit = valid
1711
+ ? containedTileSetLayout(
1712
+ availableWidth,
1713
+ availableHeight,
1714
+ cols,
1715
+ rowCount,
1716
+ this.map.cellSize,
1717
+ )
1718
+ : null
1719
+ return {
1720
+ columns: cols,
1721
+ rows: rowCount,
1722
+ marginX: mx,
1723
+ marginY: my,
1724
+ spacingX: sx,
1725
+ spacingY: sy,
1726
+ tileWidth,
1727
+ tileHeight,
1728
+ availableWidth,
1729
+ availableHeight,
1730
+ fit,
1731
+ valid,
1732
+ }
1733
+ }
1734
+
1735
+ const drawPreview = () => {
1736
+ const config = readConfig()
1737
+ const maxWidth = 430
1738
+ const maxHeight = 210
1739
+ const targetWidth = config.fit?.canvasWidth || image.naturalWidth
1740
+ const targetHeight = config.fit?.canvasHeight || image.naturalHeight
1741
+ const scale = Math.min(maxWidth / targetWidth, maxHeight / targetHeight)
1742
+ preview.width = Math.max(1, Math.round(targetWidth * scale))
1743
+ preview.height = Math.max(1, Math.round(targetHeight * scale))
1744
+ const context = preview.getContext('2d')
1745
+ context.imageSmoothingEnabled = false
1746
+ drawTransparencyGrid(context, preview.width, preview.height)
1747
+ if (config.valid) {
1748
+ const outputScaleX = preview.width / config.fit.canvasWidth
1749
+ const outputScaleY = preview.height / config.fit.canvasHeight
1750
+ const fittedScaleX = config.fit.width / config.availableWidth
1751
+ const fittedScaleY = config.fit.height / config.availableHeight
1752
+ for (let row = 0; row < config.rows; row += 1) {
1753
+ for (let column = 0; column < config.columns; column += 1) {
1754
+ const sourceX = config.marginX + column * (config.tileWidth + config.spacingX)
1755
+ const sourceY = config.marginY + row * (config.tileHeight + config.spacingY)
1756
+ const fittedLeft = config.fit.offsetX + column * config.tileWidth * fittedScaleX
1757
+ const fittedTop = config.fit.offsetY + row * config.tileHeight * fittedScaleY
1758
+ const fittedRight = config.fit.offsetX + (column + 1) * config.tileWidth * fittedScaleX
1759
+ const fittedBottom = config.fit.offsetY + (row + 1) * config.tileHeight * fittedScaleY
1760
+ context.drawImage(
1761
+ image,
1762
+ sourceX,
1763
+ sourceY,
1764
+ config.tileWidth,
1765
+ config.tileHeight,
1766
+ fittedLeft * outputScaleX,
1767
+ fittedTop * outputScaleY,
1768
+ (fittedRight - fittedLeft) * outputScaleX,
1769
+ (fittedBottom - fittedTop) * outputScaleY,
1770
+ )
1771
+ }
1772
+ }
1773
+ context.strokeStyle = '#4fd1c5'
1774
+ context.lineWidth = 1
1775
+ for (let column = 0; column <= config.columns; column += 1) {
1776
+ const x = Math.min(preview.width - 0.5, Math.max(0.5, column * preview.width / config.columns))
1777
+ context.beginPath()
1778
+ context.moveTo(x, 0)
1779
+ context.lineTo(x, preview.height)
1780
+ context.stroke()
1781
+ }
1782
+ for (let row = 0; row <= config.rows; row += 1) {
1783
+ const y = Math.min(preview.height - 0.5, Math.max(0.5, row * preview.height / config.rows))
1784
+ context.beginPath()
1785
+ context.moveTo(0, y)
1786
+ context.lineTo(preview.width, y)
1787
+ context.stroke()
1788
+ }
1789
+ validation.textContent = `${t('map.tileMapEditor.text044')}${config.fit.canvasWidth} × ${config.fit.canvasHeight}${t('map.tileMapEditor.text044.part1')}${config.columns * config.rows}${t('map.tileMapEditor.text044.part2')}${this.map.cellSize} × ${this.map.cellSize}${t('map.tileMapEditor.text044.part3')}`
1790
+ } else {
1791
+ validation.textContent = t('map.tileMapEditor.text045')
1792
+ }
1793
+ validation.classList.toggle('is-error', !config.valid)
1794
+ confirm.disabled = !config.valid
1795
+ }
1796
+ for (const input of dialog.querySelectorAll('input'))
1797
+ input.addEventListener('input', drawPreview)
1798
+ cancel.addEventListener('click', () => overlay.remove())
1799
+ overlay.addEventListener('pointerdown', (event) => event.stopPropagation())
1800
+ dialog.addEventListener('submit', async (event) => {
1801
+ event.preventDefault()
1802
+ const config = readConfig()
1803
+ if (!config.valid) return
1804
+ confirm.disabled = true
1805
+ confirm.textContent = t('map.tileMapEditor.text046')
1806
+ try {
1807
+ const result = await this.callbacks.processTileset?.({
1808
+ map_id: this.item.id,
1809
+ source_id: reference.id,
1810
+ columns: config.columns,
1811
+ rows: config.rows,
1812
+ margin_x: config.marginX,
1813
+ margin_y: config.marginY,
1814
+ spacing_x: config.spacingX,
1815
+ spacing_y: config.spacingY,
1816
+ tile_size: this.map.cellSize,
1817
+ })
1818
+ if (!result?.tileset?.id || !Array.isArray(result.tiles) || !result.tiles.length) {
1819
+ throw new Error(errorT('map.tileMapEditor.text047'))
1820
+ }
1821
+ const previousSourceIds = new Set(existing?.tileSourceIds || [])
1822
+ const nextSources = result.tiles
1823
+ .map((tile, index) =>
1824
+ normalizeSource({
1825
+ id: `tile_source_${String(tile.id).replace(/[^a-zA-Z0-9_-]/g, '_')}`,
1826
+ assetId: tile.id,
1827
+ name: `${reference.name || t('map.tileMapEditor.text048')} ${index + 1}`,
1828
+ naturalWidth: this.map.cellSize,
1829
+ naturalHeight: this.map.cellSize,
1830
+ columns: 1,
1831
+ rows: 1,
1832
+ marginX: 0,
1833
+ marginY: 0,
1834
+ spacingX: 0,
1835
+ spacingY: 0,
1836
+ tileWidth: this.map.cellSize,
1837
+ tileHeight: this.map.cellSize,
1838
+ }),
1839
+ )
1840
+ .filter(Boolean)
1841
+ const nextSourceIds = nextSources.map((source) => source.id)
1842
+ this.map.sources = [
1843
+ ...this.map.sources.filter((source) => !previousSourceIds.has(source.id)),
1844
+ ...nextSources,
1845
+ ]
1846
+ if (existing) {
1847
+ const brushImageIndex = this.map.brushImages.findIndex(
1848
+ (candidate) => candidate.id === existing.id,
1849
+ )
1850
+ this.map.brushImages[brushImageIndex] = {
1851
+ ...existing,
1852
+ type: 'map_brush_image',
1853
+ assetId: result.tileset.id,
1854
+ sourceAssetId: reference.id,
1855
+ name: reference.name || t('map.tileMapEditor.text049'),
1856
+ ...config,
1857
+ tileSourceIds: nextSourceIds,
1858
+ }
1859
+ for (const layer of this.map.layers) {
1860
+ for (const [key, cell] of Object.entries(layer.cells)) {
1861
+ if (!previousSourceIds.has(cell.sourceId)) continue
1862
+ const oldIndex = existing.tileSourceIds.indexOf(cell.sourceId)
1863
+ if (oldIndex < 0 || !nextSources[oldIndex]) delete layer.cells[key]
1864
+ else layer.cells[key] = { sourceId: nextSources[oldIndex].id, tileIndex: 0 }
1865
+ }
1866
+ }
1867
+ } else {
1868
+ this.map.brushImages.push({
1869
+ type: 'map_brush_image',
1870
+ id: `tileset_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`,
1871
+ assetId: result.tileset.id,
1872
+ sourceAssetId: reference.id,
1873
+ name: reference.name || t('map.tileMapEditor.text049'),
1874
+ ...config,
1875
+ tileSourceIds: nextSourceIds,
1876
+ })
1877
+ }
1878
+ this.map.palette = createPaletteFromSources(
1879
+ this.map.sources,
1880
+ this.map.brushImages,
1881
+ this.map.cellSize,
1882
+ )
1883
+ const source = nextSources[0]
1884
+ this.map.activeBrush = source ? { sourceId: source.id, tileIndex: 0 } : null
1885
+ this.map.activeTool = 'brush'
1886
+ for (const sourceId of previousSourceIds) {
1887
+ this.baseTextureCache.delete(sourceId)
1888
+ this.imageCache.delete(sourceId)
1889
+ }
1890
+ overlay.remove()
1891
+ this.setTool('brush', false)
1892
+ this.renderPalette()
1893
+ this.renderLayerList()
1894
+ this.renderTiles()
1895
+ this.notifyChange()
1896
+ } catch (error) {
1897
+ this.reportError(error)
1898
+ confirm.disabled = false
1899
+ confirm.textContent = existing
1900
+ ? t('map.tileMapEditor.text042')
1901
+ : t('map.tileMapEditor.text043')
1902
+ }
1903
+ })
1904
+ drawPreview()
1905
+ }
1906
+
1907
+ async htmlImageForSource(source) {
1908
+ if (this.imageCache.has(source.id)) return this.imageCache.get(source.id)
1909
+ const promise = loadHtmlImage(this.callbacks.resolveImageUrl(source.assetId))
1910
+ this.imageCache.set(source.id, promise)
1911
+ return promise
1912
+ }
1913
+
1914
+ async exportPng() {
1915
+ const width = this.map.columns * this.map.cellSize
1916
+ const height = this.map.rows * this.map.cellSize
1917
+ if (width > MAX_EXPORT_SIDE || height > MAX_EXPORT_SIDE) {
1918
+ throw new Error(
1919
+ `${t('map.tileMapEditor.text050')}${MAX_EXPORT_SIDE}${t('map.tileMapEditor.text050.part1')}${MAX_EXPORT_SIDE}${t('map.tileMapEditor.text050.part2')}`,
1920
+ )
1921
+ }
1922
+ const canvas = document.createElement('canvas')
1923
+ canvas.width = width
1924
+ canvas.height = height
1925
+ const context = canvas.getContext('2d')
1926
+ context.imageSmoothingEnabled = false
1927
+ const sourceMap = new Map(this.map.sources.map((source) => [source.id, source]))
1928
+ const loaded = new Map()
1929
+ await Promise.all(
1930
+ this.map.sources.map(async (source) => {
1931
+ loaded.set(source.id, await this.htmlImageForSource(source))
1932
+ }),
1933
+ )
1934
+ for (const layer of this.map.layers) {
1935
+ for (const [key, cell] of Object.entries(layer.cells)) {
1936
+ const source = sourceMap.get(cell.sourceId)
1937
+ const image = loaded.get(cell.sourceId)
1938
+ if (!source || !image) continue
1939
+ const [x, y] = key.split(',').map(Number)
1940
+ const frame = sourceTileFrame(source, cell.tileIndex)
1941
+ context.drawImage(
1942
+ image,
1943
+ frame.x,
1944
+ frame.y,
1945
+ frame.width,
1946
+ frame.height,
1947
+ x * this.map.cellSize,
1948
+ y * this.map.cellSize,
1949
+ this.map.cellSize,
1950
+ this.map.cellSize,
1951
+ )
1952
+ }
1953
+ }
1954
+ const blob = await new Promise((resolve, reject) => {
1955
+ canvas.toBlob(
1956
+ (value) => (value ? resolve(value) : reject(new Error(errorT('map.tileMapEditor.text051')))),
1957
+ 'image/png',
1958
+ )
1959
+ })
1960
+ downloadBlob(blob, `${safeFileStem(this.item.name)}.png`)
1961
+ }
1962
+
1963
+ destroy() {
1964
+ this.disposed = true
1965
+ this.closeBrushContextMenu()
1966
+ this.renderSequence += 1
1967
+ this.layerRenderSequence += 1
1968
+ this.resizeObserver?.disconnect()
1969
+ if (this.resizeFrame) cancelAnimationFrame(this.resizeFrame)
1970
+ this.resizeFrame = 0
1971
+ this.inspectorRoot?.remove()
1972
+ this.inspectorRoot = null
1973
+ if (this.app) {
1974
+ try {
1975
+ this.app.destroy(true, { children: true, texture: false, textureSource: false })
1976
+ } catch {
1977
+ /* Pixi may already be disposed during a canvas re-render. */
1978
+ }
1979
+ }
1980
+ this.app = null
1981
+ }
1982
+ }
1983
+
1984
+ export function mountTileMapNode(container, item, callbacks) {
1985
+ installFeatureStyles('tile-map-editor', mapStyles)
1986
+ const previous = mountedViews.get(item.id)
1987
+ previous?.destroy()
1988
+ const view = new TileMapView(container, item, callbacks)
1989
+ mountedViews.set(item.id, view)
1990
+ view.build()
1991
+ return view
1992
+ }
1993
+
1994
+ export function getMountedTileMapView(id) {
1995
+ return mountedViews.get(id) || null
1996
+ }
1997
+
1998
+ export function refreshTileMapGridScale(outerScale) {
1999
+ for (const view of mountedViews.values()) view.setOuterScale(outerScale)
2000
+ }
2001
+
2002
+ export function resizeTileMapView(id) {
2003
+ mountedViews.get(id)?.scheduleRendererResize()
2004
+ }
2005
+
2006
+ export function disposeTileMapViews() {
2007
+ for (const view of mountedViews.values()) view.destroy()
2008
+ mountedViews.clear()
2009
+ }
2010
+
2011
+ export function disposeTileMapView(id) {
2012
+ const view = mountedViews.get(id)
2013
+ if (!view) return
2014
+ view.destroy()
2015
+ mountedViews.delete(id)
2016
+ }
2017
+
2018
+ const mapStyles = `
2019
+ .tile-map-editor {
2020
+ position: relative;
2021
+ display: flex;
2022
+ flex: 1 1 auto;
2023
+ width: 100%;
2024
+ height: 100%;
2025
+ min-width: 0;
2026
+ min-height: 0;
2027
+ flex-direction: column;
2028
+ overflow: hidden;
2029
+ background: #0e131a;
2030
+ }
2031
+
2032
+ .tile-map-drag-handle {
2033
+ display: flex;
2034
+ flex: 0 0 22px;
2035
+ min-height: 22px;
2036
+ align-items: center;
2037
+ justify-content: space-between;
2038
+ gap: 12px;
2039
+ padding-block: 3px;
2040
+ border-top: 1px solid var(--line);
2041
+ background: rgba(26, 31, 39, 0.94);
2042
+ cursor: grab;
2043
+ }
2044
+
2045
+ .tile-map-footer-name {
2046
+ min-width: 0;
2047
+ overflow: hidden;
2048
+ text-overflow: ellipsis;
2049
+ }
2050
+
2051
+ .tile-map-footer-spec {
2052
+ flex: 0 0 auto;
2053
+ color: rgba(215, 224, 234, 0.62);
2054
+ }
2055
+
2056
+ .tile-map-drag-handle:active {
2057
+ cursor: grabbing;
2058
+ }
2059
+
2060
+ .tile-map-inspector {
2061
+ position: relative;
2062
+ display: flex;
2063
+ min-height: 100%;
2064
+ flex-direction: column;
2065
+ gap: 8px;
2066
+ }
2067
+
2068
+ .tile-map-inspector button,
2069
+ .tile-map-inspector input {
2070
+ font: inherit;
2071
+ }
2072
+
2073
+ .tile-map-layer-section {
2074
+ display: flex;
2075
+ flex: 0 0 auto;
2076
+ min-width: 0;
2077
+ height: 320px;
2078
+ min-height: 320px;
2079
+ max-height: 320px;
2080
+ flex-direction: column;
2081
+ gap: 6px;
2082
+ padding: 7px;
2083
+ border: 1px solid rgba(255, 255, 255, 0.1);
2084
+ border-radius: 7px;
2085
+ background: rgba(9, 13, 18, 0.44);
2086
+ }
2087
+
2088
+ .tile-map-layer-header {
2089
+ display: flex;
2090
+ align-items: center;
2091
+ justify-content: space-between;
2092
+ gap: 8px;
2093
+ font-size: 11px;
2094
+ }
2095
+
2096
+ .tile-map-layer-header .tile-map-button {
2097
+ min-height: 24px;
2098
+ padding: 2px 7px;
2099
+ }
2100
+
2101
+ .tile-map-layer-list {
2102
+ display: flex;
2103
+ min-height: 0;
2104
+ flex-direction: column;
2105
+ gap: 5px;
2106
+ overflow-y: auto;
2107
+ }
2108
+
2109
+ .tile-map-layer-item {
2110
+ display: grid;
2111
+ min-width: 0;
2112
+ grid-template-columns: 14px 56px minmax(0, 1fr) 24px;
2113
+ align-items: center;
2114
+ gap: 6px;
2115
+ padding: 4px;
2116
+ border: 1px solid transparent;
2117
+ border-radius: 6px;
2118
+ background: rgba(255, 255, 255, 0.035);
2119
+ cursor: pointer;
2120
+ transition: border-color 120ms ease, background 120ms ease, box-shadow 120ms ease, opacity 120ms ease;
2121
+ }
2122
+
2123
+ .tile-map-layer-item:hover {
2124
+ background: rgba(255, 255, 255, 0.065);
2125
+ }
2126
+
2127
+ .tile-map-layer-item.is-active {
2128
+ border-color: rgba(61, 214, 198, 0.62);
2129
+ background: rgba(61, 214, 198, 0.12);
2130
+ }
2131
+
2132
+ .tile-map-layer-item.is-dragging {
2133
+ opacity: 0.42;
2134
+ }
2135
+
2136
+ .tile-map-layer-item.is-drop-before {
2137
+ box-shadow: inset 0 2px 0 #3dd6c6;
2138
+ }
2139
+
2140
+ .tile-map-layer-item.is-drop-after {
2141
+ box-shadow: inset 0 -2px 0 #3dd6c6;
2142
+ }
2143
+
2144
+ .tile-map-layer-drag-handle {
2145
+ display: flex;
2146
+ width: 14px;
2147
+ height: 32px;
2148
+ align-items: center;
2149
+ justify-content: center;
2150
+ color: rgba(255, 255, 255, 0.42);
2151
+ cursor: grab;
2152
+ font-size: 18px;
2153
+ line-height: 1;
2154
+ user-select: none;
2155
+ }
2156
+
2157
+ .tile-map-layer-drag-handle:hover {
2158
+ color: #3dd6c6;
2159
+ }
2160
+
2161
+ .tile-map-layer-drag-handle:active {
2162
+ cursor: grabbing;
2163
+ }
2164
+
2165
+ .tile-map-layer-thumbnail {
2166
+ display: block;
2167
+ width: 56px;
2168
+ height: 40px;
2169
+ border: 1px solid rgba(255, 255, 255, 0.1);
2170
+ border-radius: 4px;
2171
+ background: #101720;
2172
+ image-rendering: pixelated;
2173
+ }
2174
+
2175
+ .tile-map-layer-name {
2176
+ min-width: 0;
2177
+ width: 100%;
2178
+ padding: 4px 5px;
2179
+ border: 1px solid transparent;
2180
+ border-radius: 4px;
2181
+ color: var(--text);
2182
+ background: transparent;
2183
+ font-size: 11px;
2184
+ }
2185
+
2186
+ .tile-map-layer-name:focus {
2187
+ border-color: rgba(61, 214, 198, 0.65);
2188
+ outline: none;
2189
+ background: rgba(0, 0, 0, 0.24);
2190
+ }
2191
+
2192
+ .tile-map-layer-delete {
2193
+ width: 24px;
2194
+ height: 24px;
2195
+ padding: 0;
2196
+ border: 0;
2197
+ border-radius: 4px;
2198
+ color: var(--muted);
2199
+ background: transparent;
2200
+ cursor: pointer;
2201
+ }
2202
+
2203
+ .tile-map-layer-delete:hover:not(:disabled) {
2204
+ color: #ff8585;
2205
+ background: rgba(255, 80, 80, 0.14);
2206
+ }
2207
+
2208
+ .tile-map-layer-delete:disabled {
2209
+ cursor: not-allowed;
2210
+ opacity: 0.28;
2211
+ }
2212
+
2213
+ .tile-map-grid-fields,
2214
+ .tile-map-slice-actions {
2215
+ display: flex;
2216
+ align-items: center;
2217
+ gap: 6px;
2218
+ }
2219
+
2220
+ .tile-map-button {
2221
+ min-height: 28px;
2222
+ padding: 4px 9px;
2223
+ border: 1px solid rgba(255, 255, 255, 0.12);
2224
+ border-radius: 6px;
2225
+ color: var(--text);
2226
+ background: rgba(255, 255, 255, 0.055);
2227
+ cursor: pointer;
2228
+ font-size: 11px;
2229
+ }
2230
+
2231
+ .tile-map-button:hover:not(:disabled),
2232
+ .tile-map-button.is-active {
2233
+ border-color: rgba(61, 214, 198, 0.65);
2234
+ background: rgba(61, 214, 198, 0.16);
2235
+ }
2236
+
2237
+ .tile-map-button:disabled {
2238
+ cursor: not-allowed;
2239
+ opacity: 0.42;
2240
+ }
2241
+
2242
+ .tile-map-button-primary {
2243
+ border-color: rgba(61, 214, 198, 0.58);
2244
+ background: rgba(61, 214, 198, 0.2);
2245
+ }
2246
+
2247
+ .tile-map-inspector.is-image-over .tile-map-palette {
2248
+ border-color: var(--accent);
2249
+ box-shadow: inset 0 0 0 2px rgba(61, 214, 198, 0.16);
2250
+ }
2251
+
2252
+ .tile-map-palette {
2253
+ position: relative;
2254
+ width: 100%;
2255
+ min-height: 0;
2256
+ aspect-ratio: 1;
2257
+ flex: 0 0 auto;
2258
+ overflow: auto;
2259
+ border: 1px solid rgba(255, 255, 255, 0.1);
2260
+ border-radius: 5px;
2261
+ background: #0b1016;
2262
+ }
2263
+
2264
+ .tile-map-tileset-canvas {
2265
+ position: relative;
2266
+ z-index: 1;
2267
+ display: block;
2268
+ min-width: 0;
2269
+ max-width: none;
2270
+ height: auto;
2271
+ image-rendering: pixelated;
2272
+ cursor: crosshair;
2273
+ touch-action: none;
2274
+ }
2275
+
2276
+ .tile-map-tileset-selection {
2277
+ position: absolute;
2278
+ z-index: 2;
2279
+ top: 0;
2280
+ left: 0;
2281
+ display: block;
2282
+ height: auto;
2283
+ image-rendering: pixelated;
2284
+ pointer-events: none;
2285
+ }
2286
+
2287
+ .tile-map-tileset-canvas[hidden] {
2288
+ display: none;
2289
+ }
2290
+
2291
+ .tile-map-palette-empty {
2292
+ position: absolute;
2293
+ z-index: 0;
2294
+ inset: auto 12px 14px;
2295
+ margin: 0;
2296
+ color: rgba(190, 201, 214, 0.38);
2297
+ font-size: 11px;
2298
+ text-align: center;
2299
+ pointer-events: none;
2300
+ }
2301
+
2302
+ .tile-map-brush-context-menu {
2303
+ position: fixed;
2304
+ z-index: 1000;
2305
+ min-width: 112px;
2306
+ padding: 5px;
2307
+ border: 1px solid rgba(255, 255, 255, 0.14);
2308
+ border-radius: 7px;
2309
+ background: rgba(20, 24, 31, 0.98);
2310
+ box-shadow: 0 10px 28px rgba(0, 0, 0, 0.42);
2311
+ }
2312
+
2313
+ .tile-map-brush-context-menu button {
2314
+ width: 100%;
2315
+ padding: 7px 9px;
2316
+ border: 0;
2317
+ border-radius: 5px;
2318
+ color: #ff9292;
2319
+ background: transparent;
2320
+ cursor: pointer;
2321
+ font: inherit;
2322
+ font-size: 11px;
2323
+ text-align: left;
2324
+ }
2325
+
2326
+ .tile-map-brush-context-menu button:hover {
2327
+ background: rgba(255, 80, 80, 0.14);
2328
+ }
2329
+
2330
+ .tile-map-tool-strip {
2331
+ display: grid;
2332
+ grid-template-columns: repeat(5, minmax(0, 1fr));
2333
+ gap: 6px;
2334
+ }
2335
+
2336
+ .tile-map-icon-button {
2337
+ display: inline-flex;
2338
+ min-width: 0;
2339
+ height: 34px;
2340
+ align-items: center;
2341
+ justify-content: center;
2342
+ padding: 0;
2343
+ border: 1px solid rgba(255, 255, 255, 0.12);
2344
+ border-radius: 6px;
2345
+ color: var(--muted);
2346
+ background: rgba(255, 255, 255, 0.055);
2347
+ cursor: pointer;
2348
+ }
2349
+
2350
+ .tile-map-icon-button svg {
2351
+ width: 17px;
2352
+ height: 17px;
2353
+ fill: none;
2354
+ stroke: currentColor;
2355
+ stroke-width: 1.8;
2356
+ stroke-linecap: round;
2357
+ stroke-linejoin: round;
2358
+ }
2359
+
2360
+ .tile-map-icon-button:hover:not(:disabled),
2361
+ .tile-map-icon-button.is-active {
2362
+ border-color: rgba(61, 214, 198, 0.65);
2363
+ color: var(--text);
2364
+ background: rgba(61, 214, 198, 0.16);
2365
+ }
2366
+
2367
+ .tile-map-icon-button:disabled {
2368
+ cursor: not-allowed;
2369
+ opacity: 0.35;
2370
+ }
2371
+
2372
+ .tile-map-grid-fields {
2373
+ display: grid;
2374
+ grid-template-columns: repeat(2, minmax(0, 1fr));
2375
+ margin-left: 0;
2376
+ }
2377
+
2378
+ .tile-map-zoom-label {
2379
+ min-width: 38px;
2380
+ color: var(--muted);
2381
+ font-family: 'JetBrains Mono', monospace;
2382
+ font-size: 10px;
2383
+ text-align: center;
2384
+ }
2385
+
2386
+ .tile-map-number-field {
2387
+ display: flex;
2388
+ align-items: center;
2389
+ gap: 4px;
2390
+ color: var(--muted);
2391
+ font-size: 10px;
2392
+ }
2393
+
2394
+ .tile-map-number-field input {
2395
+ width: 54px;
2396
+ height: 27px;
2397
+ padding: 3px 5px;
2398
+ border: 1px solid rgba(255, 255, 255, 0.11);
2399
+ border-radius: 5px;
2400
+ outline: none;
2401
+ color: var(--text);
2402
+ background: rgba(0, 0, 0, 0.28);
2403
+ font-family: 'JetBrains Mono', monospace;
2404
+ font-size: 10px;
2405
+ }
2406
+
2407
+ .tile-map-number-field input:focus {
2408
+ border-color: var(--accent);
2409
+ }
2410
+
2411
+ .tile-map-unit {
2412
+ margin-left: -2px;
2413
+ }
2414
+
2415
+ .tile-map-stage {
2416
+ position: relative;
2417
+ width: 100%;
2418
+ height: auto;
2419
+ min-width: 0;
2420
+ min-height: 0;
2421
+ flex: 1 1 auto;
2422
+ overflow: hidden;
2423
+ background: #17202b;
2424
+ }
2425
+
2426
+ .tile-map-pixi-canvas {
2427
+ position: absolute;
2428
+ inset: 0;
2429
+ display: block;
2430
+ width: 100%;
2431
+ height: 100%;
2432
+ image-rendering: pixelated;
2433
+ cursor: crosshair;
2434
+ }
2435
+
2436
+ .tile-map-slice-overlay {
2437
+ position: absolute;
2438
+ z-index: 20;
2439
+ inset: 0;
2440
+ display: grid;
2441
+ place-items: center;
2442
+ padding: 18px;
2443
+ background: rgba(4, 7, 10, 0.8);
2444
+ backdrop-filter: blur(4px);
2445
+ }
2446
+
2447
+ .tile-map-slice-dialog {
2448
+ display: grid;
2449
+ width: min(470px, 100%);
2450
+ max-height: 100%;
2451
+ gap: 12px;
2452
+ padding: 16px;
2453
+ overflow: auto;
2454
+ border: 1px solid rgba(61, 214, 198, 0.32);
2455
+ border-radius: 12px;
2456
+ background: #1b222c;
2457
+ box-shadow: 0 20px 48px rgba(0, 0, 0, 0.48);
2458
+ }
2459
+
2460
+ .tile-map-slice-head {
2461
+ display: flex;
2462
+ align-items: baseline;
2463
+ justify-content: space-between;
2464
+ gap: 12px;
2465
+ font-size: 13px;
2466
+ font-weight: 700;
2467
+ }
2468
+
2469
+ .tile-map-slice-head small {
2470
+ color: var(--muted);
2471
+ font-family: 'JetBrains Mono', monospace;
2472
+ font-size: 10px;
2473
+ font-weight: 400;
2474
+ }
2475
+
2476
+ .tile-map-slice-preview {
2477
+ display: block;
2478
+ max-width: 100%;
2479
+ max-height: 210px;
2480
+ margin: 0 auto;
2481
+ border: 1px solid rgba(255, 255, 255, 0.1);
2482
+ border-radius: 6px;
2483
+ background: #0d1117;
2484
+ image-rendering: pixelated;
2485
+ }
2486
+
2487
+ .tile-map-slice-fields {
2488
+ display: grid;
2489
+ grid-template-columns: repeat(2, minmax(0, 1fr));
2490
+ gap: 8px 12px;
2491
+ }
2492
+
2493
+ .tile-map-slice-fields .tile-map-number-field {
2494
+ justify-content: space-between;
2495
+ }
2496
+
2497
+ .tile-map-slice-fields .tile-map-number-field input {
2498
+ width: 74px;
2499
+ }
2500
+
2501
+ .tile-map-slice-validation {
2502
+ min-height: 18px;
2503
+ color: var(--muted);
2504
+ font-size: 11px;
2505
+ }
2506
+
2507
+ .tile-map-slice-validation.is-error {
2508
+ color: var(--danger);
2509
+ }
2510
+
2511
+ .tile-map-slice-actions {
2512
+ justify-content: flex-end;
2513
+ }
2514
+
2515
+ @media (max-width: 960px) {
2516
+ .app {
2517
+ display: block;
2518
+ }
2519
+ }
2520
+ `