evomeme 0.1.9 → 0.1.10

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