@nice2dev/ui-graphics 1.3.15 → 1.3.16

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 (294) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/cjs/animation/AnimatedPerson.js +470 -143
  3. package/dist/cjs/animation/AnimationEditor.js +289 -101
  4. package/dist/cjs/animation/Audience.js +83 -61
  5. package/dist/cjs/animation/BodyRenderer.js +69 -28
  6. package/dist/cjs/animation/animationHelper.js +281 -288
  7. package/dist/cjs/animation/characterTypes.js +200 -203
  8. package/dist/cjs/animation/choreoDSL.js +205 -219
  9. package/dist/cjs/animation/choreography.js +4 -32
  10. package/dist/cjs/animation/karaokeIntegration.js +68 -77
  11. package/dist/cjs/animation/lottieExport.js +108 -114
  12. package/dist/cjs/animation/motionPath.js +225 -248
  13. package/dist/cjs/animation/physics.js +227 -254
  14. package/dist/cjs/animation/rig/RigPlayer.js +53 -56
  15. package/dist/cjs/animation/rig/RiggedBody.js +71 -26
  16. package/dist/cjs/animation/rig/choreoRigBridge.js +72 -77
  17. package/dist/cjs/animation/rig/ik.js +13 -18
  18. package/dist/cjs/animation/rig/poseOps.js +34 -41
  19. package/dist/cjs/animation/rig/presets.js +64 -72
  20. package/dist/cjs/animation/rig/rigMath.js +25 -32
  21. package/dist/cjs/animation/rig/rigTypes.js +59 -63
  22. package/dist/cjs/animation/shapes/arms.js +31 -13
  23. package/dist/cjs/animation/shapes/eyes.js +129 -47
  24. package/dist/cjs/animation/shapes/hair.js +72 -45
  25. package/dist/cjs/animation/shapes/heads.js +131 -57
  26. package/dist/cjs/animation/shapes/headwear.js +47 -38
  27. package/dist/cjs/animation/shapes/legs.js +31 -13
  28. package/dist/cjs/animation/shapes/mouths.js +120 -44
  29. package/dist/cjs/animation/shapes/noses.js +80 -25
  30. package/dist/cjs/animation/shapes/outfits.js +101 -39
  31. package/dist/cjs/animation/shapes/torsos.js +15 -11
  32. package/dist/cjs/animation/spineExport.js +103 -121
  33. package/dist/cjs/core/LocalUI.js +108 -43
  34. package/dist/cjs/core/NiceBrushEngine.js +758 -552
  35. package/dist/cjs/core/collaboration.js +249 -251
  36. package/dist/cjs/core/colorBlindness.js +100 -115
  37. package/dist/cjs/core/dragDrop.js +155 -158
  38. package/dist/cjs/core/editorAsk.js +51 -41
  39. package/dist/cjs/core/fixes.js +140 -171
  40. package/dist/cjs/core/gridGuides.js +166 -201
  41. package/dist/cjs/core/historyVisual.js +156 -135
  42. package/dist/cjs/core/i18n.js +13 -14
  43. package/dist/cjs/core/integrations.js +238 -249
  44. package/dist/cjs/core/minimap.js +149 -151
  45. package/dist/cjs/core/plugins.js +98 -113
  46. package/dist/cjs/core/rtl.js +71 -83
  47. package/dist/cjs/core/shortcuts.js +196 -225
  48. package/dist/cjs/core/theme.js +142 -149
  49. package/dist/cjs/core/yjsCollaboration.js +232 -293
  50. package/dist/cjs/design/design-tools.js +12 -22
  51. package/dist/cjs/font/NiceFontEditor.js +877 -316
  52. package/dist/cjs/font/fontFormatUtils.js +388 -448
  53. package/dist/cjs/game/GameAsset2dEditor.js +1201 -392
  54. package/dist/cjs/game/GameAsset2dEditor.module.css.js +146 -5
  55. package/dist/cjs/game/gameAssetTypes.js +7 -16
  56. package/dist/cjs/game/gameAssetUtils.js +390 -413
  57. package/dist/cjs/game/useGameAssetEditor.js +820 -763
  58. package/dist/cjs/index.js +81 -84
  59. package/dist/cjs/nice2dev-ui-graphics.css +1 -1
  60. package/dist/cjs/node_modules/lib0/array.js +8 -70
  61. package/dist/cjs/node_modules/lib0/binary.js +3 -20
  62. package/dist/cjs/node_modules/lib0/broadcastchannel.js +24 -86
  63. package/dist/cjs/node_modules/lib0/buffer.js +15 -82
  64. package/dist/cjs/node_modules/lib0/conditions.js +3 -16
  65. package/dist/cjs/node_modules/lib0/decoding.js +108 -287
  66. package/dist/cjs/node_modules/lib0/dom.js +14 -44
  67. package/dist/cjs/node_modules/lib0/encoding.js +68 -418
  68. package/dist/cjs/node_modules/lib0/environment.js +26 -92
  69. package/dist/cjs/node_modules/lib0/error.js +5 -29
  70. package/dist/cjs/node_modules/lib0/function.js +27 -66
  71. package/dist/cjs/node_modules/lib0/iterator.js +8 -33
  72. package/dist/cjs/node_modules/lib0/logging.common.js +12 -22
  73. package/dist/cjs/node_modules/lib0/logging.js +32 -71
  74. package/dist/cjs/node_modules/lib0/map.js +16 -86
  75. package/dist/cjs/node_modules/lib0/math.js +3 -32
  76. package/dist/cjs/node_modules/lib0/number.js +4 -15
  77. package/dist/cjs/node_modules/lib0/object.js +14 -88
  78. package/dist/cjs/node_modules/lib0/observable.js +38 -78
  79. package/dist/cjs/node_modules/lib0/pair.js +3 -20
  80. package/dist/cjs/node_modules/lib0/prng.js +8 -77
  81. package/dist/cjs/node_modules/lib0/promise.js +6 -30
  82. package/dist/cjs/node_modules/lib0/random.js +8 -23
  83. package/dist/cjs/node_modules/lib0/schema.js +313 -687
  84. package/dist/cjs/node_modules/lib0/set.js +3 -10
  85. package/dist/cjs/node_modules/lib0/storage.js +20 -55
  86. package/dist/cjs/node_modules/lib0/string.js +17 -72
  87. package/dist/cjs/node_modules/lib0/symbol.js +2 -12
  88. package/dist/cjs/node_modules/lib0/time.js +2 -15
  89. package/dist/cjs/node_modules/lib0/trait/equality.js +3 -28
  90. package/dist/cjs/node_modules/lib0/url.js +4 -18
  91. package/dist/cjs/node_modules/lib0/webcrypto.js +2 -5
  92. package/dist/cjs/node_modules/y-protocols/auth.js +5 -19
  93. package/dist/cjs/node_modules/y-protocols/awareness.js +53 -116
  94. package/dist/cjs/node_modules/y-protocols/sync.js +16 -103
  95. package/dist/cjs/node_modules/y-websocket/src/y-websocket.js +77 -219
  96. package/dist/cjs/node_modules/yjs/dist/yjs.js +2128 -4868
  97. package/dist/cjs/photo/FilterThumb.js +37 -25
  98. package/dist/cjs/photo/NiceAITools.js +679 -467
  99. package/dist/cjs/photo/PhotoEditor.js +34 -20
  100. package/dist/cjs/photo/PhotoEditor.module.css.js +296 -5
  101. package/dist/cjs/photo/PhotoEditorCanvas.js +250 -91
  102. package/dist/cjs/photo/PhotoEditorLeftPanel.js +1086 -224
  103. package/dist/cjs/photo/PhotoEditorToolbar.js +135 -19
  104. package/dist/cjs/photo/photoEditorActions.js +89 -72
  105. package/dist/cjs/photo/photoEditorTypes.js +29 -32
  106. package/dist/cjs/photo/photoFilters.js +959 -985
  107. package/dist/cjs/photo/photoOverlays.js +899 -945
  108. package/dist/cjs/photo/photoSelection.js +427 -499
  109. package/dist/cjs/photo/usePhotoEditor.js +739 -741
  110. package/dist/cjs/pixel/HSVPicker.js +191 -175
  111. package/dist/cjs/pixel/PixelEditor.js +159 -48
  112. package/dist/cjs/pixel/PixelEditor.module.css.js +209 -5
  113. package/dist/cjs/pixel/PixelEditorCanvas.js +29 -16
  114. package/dist/cjs/pixel/PixelEditorMenuBar.js +160 -162
  115. package/dist/cjs/pixel/PixelEditorRightPanel.js +377 -45
  116. package/dist/cjs/pixel/PixelEditorStatusBar.js +47 -20
  117. package/dist/cjs/pixel/PixelEditorTimeline.js +106 -23
  118. package/dist/cjs/pixel/PixelEditorToolbar.js +99 -21
  119. package/dist/cjs/pixel/SpriteGeneratorPanel.js +483 -303
  120. package/dist/cjs/pixel/animalGeneratorPresets.js +776 -601
  121. package/dist/cjs/pixel/asepriteFormat.js +511 -560
  122. package/dist/cjs/pixel/buildingGeneratorPresets.js +693 -520
  123. package/dist/cjs/pixel/characterGeneratorPresets.js +644 -544
  124. package/dist/cjs/pixel/pixel-pro.js +52 -63
  125. package/dist/cjs/pixel/pixelEditorExports.js +388 -428
  126. package/dist/cjs/pixel/pixelEditorTypes.js +91 -108
  127. package/dist/cjs/pixel/pixelEditorUtils.js +519 -561
  128. package/dist/cjs/pixel/spriteGeneratorCore.js +206 -257
  129. package/dist/cjs/pixel/usePixelEditor.js +1321 -1343
  130. package/dist/cjs/texture/Nice3DTexturePainter.js +707 -440
  131. package/dist/cjs/ui/NiceUIDesigner.js +954 -527
  132. package/dist/cjs/vector/NiceSvgToCode.js +586 -446
  133. package/dist/cjs/vector/VectorEditor.js +290 -117
  134. package/dist/cjs/vector/VectorEditor.module.css.js +86 -5
  135. package/dist/cjs/vector/VectorEditorMenuBar.js +333 -275
  136. package/dist/cjs/vector/VectorEditorRightPanel.js +1094 -371
  137. package/dist/cjs/vector/VectorEditorShapeRenderer.js +198 -60
  138. package/dist/cjs/vector/VectorEditorStatusBar.js +39 -13
  139. package/dist/cjs/vector/useVectorEditor.js +848 -889
  140. package/dist/cjs/vector/vectorAnimation.js +110 -137
  141. package/dist/cjs/vector/vectorBooleanOps.js +398 -485
  142. package/dist/cjs/vector/vectorEditorExport.js +201 -203
  143. package/dist/cjs/vector/vectorEditorImport.js +173 -189
  144. package/dist/cjs/vector/vectorEditorTypes.js +42 -49
  145. package/dist/cjs/vector/vectorGradients.js +151 -174
  146. package/dist/cjs/vector/vectorIconExport.js +52 -59
  147. package/dist/cjs/vector/vectorIconImport.js +169 -219
  148. package/dist/esm/animation/AnimatedPerson.js +470 -141
  149. package/dist/esm/animation/AnimationEditor.js +290 -100
  150. package/dist/esm/animation/Audience.js +83 -59
  151. package/dist/esm/animation/BodyRenderer.js +71 -25
  152. package/dist/esm/animation/animationHelper.js +284 -287
  153. package/dist/esm/animation/characterTypes.js +204 -202
  154. package/dist/esm/animation/choreoDSL.js +212 -218
  155. package/dist/esm/animation/choreography.js +8 -31
  156. package/dist/esm/animation/karaokeIntegration.js +74 -76
  157. package/dist/esm/animation/lottieExport.js +112 -113
  158. package/dist/esm/animation/motionPath.js +235 -247
  159. package/dist/esm/animation/physics.js +235 -253
  160. package/dist/esm/animation/rig/RigPlayer.js +53 -54
  161. package/dist/esm/animation/rig/RiggedBody.js +71 -24
  162. package/dist/esm/animation/rig/choreoRigBridge.js +74 -76
  163. package/dist/esm/animation/rig/ik.js +14 -17
  164. package/dist/esm/animation/rig/poseOps.js +37 -40
  165. package/dist/esm/animation/rig/presets.js +70 -71
  166. package/dist/esm/animation/rig/rigMath.js +28 -31
  167. package/dist/esm/animation/rig/rigTypes.js +62 -62
  168. package/dist/esm/animation/shapes/arms.js +32 -12
  169. package/dist/esm/animation/shapes/eyes.js +135 -46
  170. package/dist/esm/animation/shapes/hair.js +80 -44
  171. package/dist/esm/animation/shapes/heads.js +140 -56
  172. package/dist/esm/animation/shapes/headwear.js +54 -37
  173. package/dist/esm/animation/shapes/legs.js +32 -12
  174. package/dist/esm/animation/shapes/mouths.js +127 -43
  175. package/dist/esm/animation/shapes/noses.js +87 -24
  176. package/dist/esm/animation/shapes/outfits.js +106 -38
  177. package/dist/esm/animation/shapes/torsos.js +16 -10
  178. package/dist/esm/animation/spineExport.js +106 -120
  179. package/dist/esm/core/LocalUI.js +111 -42
  180. package/dist/esm/core/NiceBrushEngine.js +761 -551
  181. package/dist/esm/core/collaboration.js +255 -250
  182. package/dist/esm/core/colorBlindness.js +106 -114
  183. package/dist/esm/core/dragDrop.js +157 -157
  184. package/dist/esm/core/editorAsk.js +53 -40
  185. package/dist/esm/core/fixes.js +145 -170
  186. package/dist/esm/core/gridGuides.js +178 -200
  187. package/dist/esm/core/historyVisual.js +158 -134
  188. package/dist/esm/core/i18n.js +15 -13
  189. package/dist/esm/core/integrations.js +244 -248
  190. package/dist/esm/core/minimap.js +151 -150
  191. package/dist/esm/core/plugins.js +102 -112
  192. package/dist/esm/core/rtl.js +77 -82
  193. package/dist/esm/core/shortcuts.js +200 -224
  194. package/dist/esm/core/theme.js +150 -148
  195. package/dist/esm/core/yjsCollaboration.js +240 -292
  196. package/dist/esm/design/design-tools.js +13 -21
  197. package/dist/esm/font/NiceFontEditor.js +880 -315
  198. package/dist/esm/font/fontFormatUtils.js +394 -447
  199. package/dist/esm/game/GameAsset2dEditor.js +1202 -391
  200. package/dist/esm/game/GameAsset2dEditor.module.css.js +146 -3
  201. package/dist/esm/game/gameAssetTypes.js +8 -15
  202. package/dist/esm/game/gameAssetUtils.js +408 -412
  203. package/dist/esm/game/useGameAssetEditor.js +821 -762
  204. package/dist/esm/index.js +332 -69
  205. package/dist/esm/nice2dev-ui-graphics.css +1 -1
  206. package/dist/esm/node_modules/lib0/array.js +15 -69
  207. package/dist/esm/node_modules/lib0/binary.js +14 -19
  208. package/dist/esm/node_modules/lib0/broadcastchannel.js +27 -85
  209. package/dist/esm/node_modules/lib0/buffer.js +20 -80
  210. package/dist/esm/node_modules/lib0/conditions.js +4 -15
  211. package/dist/esm/node_modules/lib0/decoding.js +128 -286
  212. package/dist/esm/node_modules/lib0/dom.js +20 -43
  213. package/dist/esm/node_modules/lib0/encoding.js +90 -415
  214. package/dist/esm/node_modules/lib0/environment.js +33 -91
  215. package/dist/esm/node_modules/lib0/error.js +8 -28
  216. package/dist/esm/node_modules/lib0/function.js +31 -65
  217. package/dist/esm/node_modules/lib0/iterator.js +11 -32
  218. package/dist/esm/node_modules/lib0/logging.common.js +22 -21
  219. package/dist/esm/node_modules/lib0/logging.js +44 -70
  220. package/dist/esm/node_modules/lib0/map.js +21 -85
  221. package/dist/esm/node_modules/lib0/math.js +9 -31
  222. package/dist/esm/node_modules/lib0/number.js +7 -14
  223. package/dist/esm/node_modules/lib0/object.js +26 -87
  224. package/dist/esm/node_modules/lib0/observable.js +40 -77
  225. package/dist/esm/node_modules/lib0/pair.js +5 -19
  226. package/dist/esm/node_modules/lib0/prng.js +15 -76
  227. package/dist/esm/node_modules/lib0/promise.js +7 -29
  228. package/dist/esm/node_modules/lib0/random.js +10 -22
  229. package/dist/esm/node_modules/lib0/schema.js +367 -686
  230. package/dist/esm/node_modules/lib0/set.js +4 -9
  231. package/dist/esm/node_modules/lib0/storage.js +23 -54
  232. package/dist/esm/node_modules/lib0/string.js +26 -71
  233. package/dist/esm/node_modules/lib0/symbol.js +3 -11
  234. package/dist/esm/node_modules/lib0/time.js +3 -14
  235. package/dist/esm/node_modules/lib0/trait/equality.js +5 -27
  236. package/dist/esm/node_modules/lib0/url.js +5 -17
  237. package/dist/esm/node_modules/lib0/webcrypto.js +3 -4
  238. package/dist/esm/node_modules/y-protocols/auth.js +7 -18
  239. package/dist/esm/node_modules/y-protocols/awareness.js +58 -115
  240. package/dist/esm/node_modules/y-protocols/sync.js +26 -102
  241. package/dist/esm/node_modules/y-websocket/src/y-websocket.js +82 -218
  242. package/dist/esm/node_modules/yjs/dist/yjs.js +2263 -4899
  243. package/dist/esm/photo/FilterThumb.js +37 -23
  244. package/dist/esm/photo/NiceAITools.js +685 -466
  245. package/dist/esm/photo/PhotoEditor.js +34 -18
  246. package/dist/esm/photo/PhotoEditor.module.css.js +296 -3
  247. package/dist/esm/photo/PhotoEditorCanvas.js +250 -89
  248. package/dist/esm/photo/PhotoEditorLeftPanel.js +1086 -222
  249. package/dist/esm/photo/PhotoEditorToolbar.js +135 -17
  250. package/dist/esm/photo/photoEditorActions.js +93 -71
  251. package/dist/esm/photo/photoEditorTypes.js +32 -31
  252. package/dist/esm/photo/photoFilters.js +967 -984
  253. package/dist/esm/photo/photoOverlays.js +913 -944
  254. package/dist/esm/photo/photoSelection.js +438 -498
  255. package/dist/esm/photo/usePhotoEditor.js +740 -740
  256. package/dist/esm/pixel/HSVPicker.js +192 -174
  257. package/dist/esm/pixel/PixelEditor.js +160 -47
  258. package/dist/esm/pixel/PixelEditor.module.css.js +209 -3
  259. package/dist/esm/pixel/PixelEditorCanvas.js +29 -14
  260. package/dist/esm/pixel/PixelEditorMenuBar.js +160 -160
  261. package/dist/esm/pixel/PixelEditorRightPanel.js +377 -43
  262. package/dist/esm/pixel/PixelEditorStatusBar.js +47 -18
  263. package/dist/esm/pixel/PixelEditorTimeline.js +106 -21
  264. package/dist/esm/pixel/PixelEditorToolbar.js +99 -19
  265. package/dist/esm/pixel/SpriteGeneratorPanel.js +483 -301
  266. package/dist/esm/pixel/animalGeneratorPresets.js +782 -600
  267. package/dist/esm/pixel/asepriteFormat.js +517 -559
  268. package/dist/esm/pixel/buildingGeneratorPresets.js +699 -519
  269. package/dist/esm/pixel/characterGeneratorPresets.js +650 -543
  270. package/dist/esm/pixel/pixel-pro.js +53 -62
  271. package/dist/esm/pixel/pixelEditorExports.js +397 -427
  272. package/dist/esm/pixel/pixelEditorTypes.js +103 -107
  273. package/dist/esm/pixel/pixelEditorUtils.js +551 -560
  274. package/dist/esm/pixel/spriteGeneratorCore.js +223 -256
  275. package/dist/esm/pixel/usePixelEditor.js +1322 -1342
  276. package/dist/esm/texture/Nice3DTexturePainter.js +709 -439
  277. package/dist/esm/ui/NiceUIDesigner.js +957 -526
  278. package/dist/esm/vector/NiceSvgToCode.js +589 -445
  279. package/dist/esm/vector/VectorEditor.js +291 -116
  280. package/dist/esm/vector/VectorEditor.module.css.js +86 -3
  281. package/dist/esm/vector/VectorEditorMenuBar.js +333 -273
  282. package/dist/esm/vector/VectorEditorRightPanel.js +1094 -369
  283. package/dist/esm/vector/VectorEditorShapeRenderer.js +199 -59
  284. package/dist/esm/vector/VectorEditorStatusBar.js +39 -11
  285. package/dist/esm/vector/useVectorEditor.js +849 -888
  286. package/dist/esm/vector/vectorAnimation.js +115 -136
  287. package/dist/esm/vector/vectorBooleanOps.js +401 -484
  288. package/dist/esm/vector/vectorEditorExport.js +207 -202
  289. package/dist/esm/vector/vectorEditorImport.js +174 -188
  290. package/dist/esm/vector/vectorEditorTypes.js +50 -48
  291. package/dist/esm/vector/vectorGradients.js +158 -173
  292. package/dist/esm/vector/vectorIconExport.js +53 -58
  293. package/dist/esm/vector/vectorIconImport.js +171 -218
  294. package/package.json +9 -15
@@ -1,777 +1,836 @@
1
- import { useState, useRef, useCallback } from 'react';
2
- import { createEmptyProject, generateId, serializeProject, downloadJson, deserializeProject, createTileFromImage, dataUrlToImageData, createEmptyTileMap, createEmptyTileLayer, loadImage, autoSliceSpriteSheet, detectFrames, createSpriteAnimation, applyEffects, exportToTiledJson, downloadPng } from './gameAssetUtils.js';
3
-
4
- /**
5
- * useGameAssetEditor.ts — State and logic hook for GameAsset2dEditor
6
- */
1
+ import { useState, useRef, useCallback } from "react";
2
+ import { createEmptyProject, generateId, serializeProject, downloadJson, deserializeProject, createTileFromImage, dataUrlToImageData, createEmptyTileMap, createEmptyTileLayer, loadImage, autoSliceSpriteSheet, detectFrames, createSpriteAnimation, applyEffects, exportToTiledJson, downloadPng } from "./gameAssetUtils.js";
7
3
  const DEFAULT_EDITOR_STATE = {
8
- mode: 'tilemap',
9
- tileMapTool: 'brush',
10
- spriteSheetTool: 'select',
11
- selectedTileId: null,
12
- selectedFrameIds: [],
13
- selectedStateId: null,
14
- activeLayerId: null,
15
- activeTileMapId: null,
16
- activeSpriteSheetId: null,
17
- activeStateMachineId: null,
18
- zoom: 1,
19
- panX: 0,
20
- panY: 0,
21
- showGrid: true,
22
- showCollision: false,
23
- snapToGrid: true,
4
+ mode: "tilemap",
5
+ tileMapTool: "brush",
6
+ spriteSheetTool: "select",
7
+ selectedTileId: null,
8
+ selectedFrameIds: [],
9
+ selectedStateId: null,
10
+ activeLayerId: null,
11
+ activeTileMapId: null,
12
+ activeSpriteSheetId: null,
13
+ activeStateMachineId: null,
14
+ zoom: 1,
15
+ panX: 0,
16
+ panY: 0,
17
+ showGrid: true,
18
+ showCollision: false,
19
+ snapToGrid: true
24
20
  };
25
21
  const MAX_HISTORY = 50;
26
22
  const useGameAssetEditor = (options = {}) => {
27
- const { onSave, onExport } = options;
28
- const [project, setProject] = useState(() => createEmptyProject('Untitled'));
29
- const [editorState, setEditorState] = useState(DEFAULT_EDITOR_STATE);
30
- const [history, setHistory] = useState([]);
31
- const [historyIndex, setHistoryIndex] = useState(-1);
32
- const [currentEffects, setCurrentEffects] = useState([]);
33
- const canvasRef = useRef(null);
34
- const fileInputRef = useRef(null);
35
- // ─────────────────────────────────────────────────────────────
36
- // History management
37
- // ─────────────────────────────────────────────────────────────
38
- const pushHistory = useCallback((description) => {
39
- const entry = {
40
- id: generateId('history'),
41
- description,
42
- timestamp: Date.now(),
43
- snapshot: JSON.parse(JSON.stringify(project)),
44
- };
45
- setHistory((prev) => {
46
- const newHistory = prev.slice(0, historyIndex + 1);
47
- newHistory.push(entry);
48
- if (newHistory.length > MAX_HISTORY) {
49
- newHistory.shift();
50
- }
51
- return newHistory;
52
- });
53
- setHistoryIndex((prev) => Math.min(prev + 1, MAX_HISTORY - 1));
54
- }, [project, historyIndex]);
55
- const undo = useCallback(() => {
56
- if (historyIndex > 0) {
57
- const entry = history[historyIndex - 1];
58
- setProject(entry.snapshot);
59
- setHistoryIndex((prev) => prev - 1);
60
- }
61
- }, [history, historyIndex]);
62
- const redo = useCallback(() => {
63
- if (historyIndex < history.length - 1) {
64
- const entry = history[historyIndex + 1];
65
- setProject(entry.snapshot);
66
- setHistoryIndex((prev) => prev + 1);
23
+ const { onSave, onExport } = options;
24
+ const [project, setProject] = useState(() => createEmptyProject("Untitled"));
25
+ const [editorState, setEditorState] = useState(DEFAULT_EDITOR_STATE);
26
+ const [history, setHistory] = useState([]);
27
+ const [historyIndex, setHistoryIndex] = useState(-1);
28
+ const [currentEffects, setCurrentEffects] = useState([]);
29
+ const canvasRef = useRef(null);
30
+ const fileInputRef = useRef(null);
31
+ const pushHistory = useCallback(
32
+ (description) => {
33
+ const entry = {
34
+ id: generateId("history"),
35
+ description,
36
+ timestamp: Date.now(),
37
+ snapshot: JSON.parse(JSON.stringify(project))
38
+ };
39
+ setHistory((prev) => {
40
+ const newHistory = prev.slice(0, historyIndex + 1);
41
+ newHistory.push(entry);
42
+ if (newHistory.length > MAX_HISTORY) {
43
+ newHistory.shift();
67
44
  }
68
- }, [history, historyIndex]);
69
- // ─────────────────────────────────────────────────────────────
70
- // Project operations
71
- // ─────────────────────────────────────────────────────────────
72
- const newProject = useCallback((name) => {
73
- const newProj = createEmptyProject(name);
74
- setProject(newProj);
75
- setEditorState(DEFAULT_EDITOR_STATE);
76
- setHistory([]);
77
- setHistoryIndex(-1);
78
- }, []);
79
- const saveProject = useCallback(() => {
80
- const data = serializeProject(project);
81
- downloadJson(JSON.parse(data), `${project.name}.json`);
82
- onSave === null || onSave === void 0 ? void 0 : onSave(project);
83
- }, [project, onSave]);
84
- const loadProject = useCallback(async (file) => {
85
- const text = await file.text();
86
- const loaded = await deserializeProject(text);
87
- setProject(loaded);
88
- setEditorState(DEFAULT_EDITOR_STATE);
89
- setHistory([]);
90
- setHistoryIndex(-1);
91
- }, []);
92
- // ─────────────────────────────────────────────────────────────
93
- // Editor state operations
94
- // ─────────────────────────────────────────────────────────────
95
- const setMode = useCallback((mode) => {
96
- setEditorState((prev) => ({ ...prev, mode }));
97
- }, []);
98
- const setTileMapTool = useCallback((tool) => {
99
- setEditorState((prev) => ({ ...prev, tileMapTool: tool }));
100
- }, []);
101
- const setSpriteSheetTool = useCallback((tool) => {
102
- setEditorState((prev) => ({ ...prev, spriteSheetTool: tool }));
103
- }, []);
104
- const setZoom = useCallback((zoom) => {
105
- setEditorState((prev) => ({ ...prev, zoom: Math.max(0.1, Math.min(10, zoom)) }));
106
- }, []);
107
- const setPan = useCallback((x, y) => {
108
- setEditorState((prev) => ({ ...prev, panX: x, panY: y }));
109
- }, []);
110
- const toggleGrid = useCallback(() => {
111
- setEditorState((prev) => ({ ...prev, showGrid: !prev.showGrid }));
112
- }, []);
113
- const toggleCollision = useCallback(() => {
114
- setEditorState((prev) => ({ ...prev, showCollision: !prev.showCollision }));
115
- }, []);
116
- const toggleSnap = useCallback(() => {
117
- setEditorState((prev) => ({ ...prev, snapToGrid: !prev.snapToGrid }));
118
- }, []);
119
- // ─────────────────────────────────────────────────────────────
120
- // Tile set operations
121
- // ─────────────────────────────────────────────────────────────
122
- const addTile = useCallback((imageData, name) => {
123
- const tile = createTileFromImage(imageData, name);
124
- setProject((prev) => ({ ...prev, tileSets: [...prev.tileSets, tile] }));
125
- pushHistory(`Add tile: ${name}`);
126
- }, [pushHistory]);
127
- const removeTile = useCallback((id) => {
128
- setProject((prev) => ({
129
- ...prev,
130
- tileSets: prev.tileSets.filter((t) => t.id !== id),
131
- }));
132
- pushHistory('Remove tile');
133
- }, [pushHistory]);
134
- const selectTile = useCallback((id) => {
135
- setEditorState((prev) => ({ ...prev, selectedTileId: id }));
136
- }, []);
137
- const importTileSet = useCallback(async (file) => {
138
- const dataUrl = await new Promise((resolve) => {
139
- const reader = new FileReader();
140
- reader.onload = () => resolve(reader.result);
141
- reader.readAsDataURL(file);
142
- });
143
- const imageData = await dataUrlToImageData(dataUrl);
144
- const name = file.name.replace(/\.[^/.]+$/, '');
145
- addTile(imageData, name);
146
- }, [addTile]);
147
- // ─────────────────────────────────────────────────────────────
148
- // Tile map operations
149
- // ─────────────────────────────────────────────────────────────
150
- const createTileMap = useCallback((name, width, height) => {
151
- const map = createEmptyTileMap(name, width, height, project.settings.defaultTileWidth, project.settings.defaultTileHeight);
152
- setProject((prev) => ({ ...prev, tileMaps: [...prev.tileMaps, map] }));
153
- setEditorState((prev) => ({
154
- ...prev,
155
- activeTileMapId: map.id,
156
- activeLayerId: map.layers[0].id,
157
- }));
158
- pushHistory(`Create tile map: ${name}`);
159
- }, [project.settings, pushHistory]);
160
- const removeTileMap = useCallback((id) => {
161
- setProject((prev) => ({
162
- ...prev,
163
- tileMaps: prev.tileMaps.filter((m) => m.id !== id),
164
- }));
165
- pushHistory('Remove tile map');
166
- }, [pushHistory]);
167
- const selectTileMap = useCallback((id) => {
168
- setEditorState((prev) => {
169
- var _a, _b;
170
- const map = project.tileMaps.find((m) => m.id === id);
171
- return {
172
- ...prev,
173
- activeTileMapId: id,
174
- activeLayerId: (_b = (_a = map === null || map === void 0 ? void 0 : map.layers[0]) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null,
175
- };
176
- });
177
- }, [project.tileMaps]);
178
- const setTile = useCallback((layerId, x, y, tileId) => {
179
- setProject((prev) => ({
180
- ...prev,
181
- tileMaps: prev.tileMaps.map((map) => ({
182
- ...map,
183
- layers: map.layers.map((layer) => {
184
- if (layer.id !== layerId) {
185
- return layer;
186
- }
187
- const newTiles = layer.tiles.map((row) => [...row]);
188
- if (y >= 0 && y < newTiles.length && x >= 0 && x < newTiles[0].length) {
189
- newTiles[y][x] = tileId;
190
- }
191
- return { ...layer, tiles: newTiles };
192
- }),
193
- })),
194
- }));
195
- }, []);
196
- const fillTiles = useCallback((layerId, startX, startY, newTileId) => {
197
- setProject((prev) => {
198
- var _a;
199
- const map = prev.tileMaps.find((m) => m.layers.some((l) => l.id === layerId));
200
- if (!map) {
201
- return prev;
202
- }
203
- const layer = map.layers.find((l) => l.id === layerId);
204
- if (!layer) {
205
- return prev;
206
- }
207
- const targetId = (_a = layer.tiles[startY]) === null || _a === void 0 ? void 0 : _a[startX];
208
- if (targetId === newTileId) {
209
- return prev;
210
- }
211
- const newTiles = layer.tiles.map((row) => [...row]);
212
- const stack = [[startX, startY]];
213
- const visited = new Set();
214
- while (stack.length > 0) {
215
- const [x, y] = stack.pop();
216
- const key = `${x},${y}`;
217
- if (visited.has(key)) {
218
- continue;
219
- }
220
- if (y < 0 || y >= newTiles.length || x < 0 || x >= newTiles[0].length) {
221
- continue;
222
- }
223
- if (newTiles[y][x] !== targetId) {
224
- continue;
225
- }
226
- visited.add(key);
227
- newTiles[y][x] = newTileId;
228
- stack.push([x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]);
229
- }
230
- return {
231
- ...prev,
232
- tileMaps: prev.tileMaps.map((m) => m.id === map.id
233
- ? {
234
- ...m,
235
- layers: m.layers.map((l) => (l.id === layerId ? { ...l, tiles: newTiles } : l)),
236
- }
237
- : m),
238
- };
239
- });
240
- pushHistory('Fill tiles');
241
- }, [pushHistory]);
242
- const addLayer = useCallback(() => {
243
- const map = project.tileMaps.find((m) => m.id === editorState.activeTileMapId);
244
- if (!map) {
245
- return;
246
- }
247
- const newLayer = createEmptyTileLayer(`Layer ${map.layers.length + 1}`, map.width, map.height);
248
- setProject((prev) => ({
249
- ...prev,
250
- tileMaps: prev.tileMaps.map((m) => m.id === map.id ? { ...m, layers: [...m.layers, newLayer] } : m),
251
- }));
252
- setEditorState((prev) => ({ ...prev, activeLayerId: newLayer.id }));
253
- pushHistory('Add layer');
254
- }, [project.tileMaps, editorState.activeTileMapId, pushHistory]);
255
- const removeLayer = useCallback((id) => {
256
- setProject((prev) => ({
257
- ...prev,
258
- tileMaps: prev.tileMaps.map((m) => ({
259
- ...m,
260
- layers: m.layers.filter((l) => l.id !== id),
261
- })),
262
- }));
263
- pushHistory('Remove layer');
264
- }, [pushHistory]);
265
- const selectLayer = useCallback((id) => {
266
- setEditorState((prev) => ({ ...prev, activeLayerId: id }));
267
- }, []);
268
- const toggleLayerVisibility = useCallback((id) => {
269
- setProject((prev) => ({
270
- ...prev,
271
- tileMaps: prev.tileMaps.map((m) => ({
272
- ...m,
273
- layers: m.layers.map((l) => (l.id === id ? { ...l, visible: !l.visible } : l)),
274
- })),
275
- }));
276
- }, []);
277
- const setLayerOpacity = useCallback((id, opacity) => {
278
- setProject((prev) => ({
279
- ...prev,
280
- tileMaps: prev.tileMaps.map((m) => ({
281
- ...m,
282
- layers: m.layers.map((l) => (l.id === id ? { ...l, opacity } : l)),
283
- })),
284
- }));
285
- }, []);
286
- const moveLayer = useCallback((id, direction) => {
287
- setProject((prev) => ({
288
- ...prev,
289
- tileMaps: prev.tileMaps.map((m) => {
290
- const idx = m.layers.findIndex((l) => l.id === id);
291
- if (idx === -1) {
292
- return m;
293
- }
294
- const newIdx = direction === 'up' ? idx - 1 : idx + 1;
295
- if (newIdx < 0 || newIdx >= m.layers.length) {
296
- return m;
297
- }
298
- const newLayers = [...m.layers];
299
- [newLayers[idx], newLayers[newIdx]] = [newLayers[newIdx], newLayers[idx]];
300
- return { ...m, layers: newLayers };
301
- }),
302
- }));
303
- pushHistory(`Move layer ${direction}`);
304
- }, [pushHistory]);
305
- // ─────────────────────────────────────────────────────────────
306
- // Sprite sheet operations
307
- // ─────────────────────────────────────────────────────────────
308
- const importSpriteSheet = useCallback(async (file) => {
309
- const dataUrl = await new Promise((resolve) => {
310
- const reader = new FileReader();
311
- reader.onload = () => resolve(reader.result);
312
- reader.readAsDataURL(file);
313
- });
314
- const image = await loadImage(dataUrl);
315
- const canvas = document.createElement('canvas');
316
- canvas.width = image.width;
317
- canvas.height = image.height;
318
- const ctx = canvas.getContext('2d');
319
- ctx.drawImage(image, 0, 0);
320
- const imageData = ctx.getImageData(0, 0, image.width, image.height);
321
- const sheet = {
322
- id: generateId('sheet'),
323
- name: file.name.replace(/\.[^/.]+$/, ''),
324
- image,
325
- imageData,
326
- dataUrl,
327
- width: image.width,
328
- height: image.height,
329
- frames: [],
330
- animations: [],
45
+ return newHistory;
46
+ });
47
+ setHistoryIndex((prev) => Math.min(prev + 1, MAX_HISTORY - 1));
48
+ },
49
+ [project, historyIndex]
50
+ );
51
+ const undo = useCallback(() => {
52
+ if (historyIndex > 0) {
53
+ const entry = history[historyIndex - 1];
54
+ setProject(entry.snapshot);
55
+ setHistoryIndex((prev) => prev - 1);
56
+ }
57
+ }, [history, historyIndex]);
58
+ const redo = useCallback(() => {
59
+ if (historyIndex < history.length - 1) {
60
+ const entry = history[historyIndex + 1];
61
+ setProject(entry.snapshot);
62
+ setHistoryIndex((prev) => prev + 1);
63
+ }
64
+ }, [history, historyIndex]);
65
+ const newProject = useCallback((name) => {
66
+ const newProj = createEmptyProject(name);
67
+ setProject(newProj);
68
+ setEditorState(DEFAULT_EDITOR_STATE);
69
+ setHistory([]);
70
+ setHistoryIndex(-1);
71
+ }, []);
72
+ const saveProject = useCallback(() => {
73
+ const data = serializeProject(project);
74
+ downloadJson(JSON.parse(data), `${project.name}.json`);
75
+ onSave?.(project);
76
+ }, [project, onSave]);
77
+ const loadProject = useCallback(async (file) => {
78
+ const text = await file.text();
79
+ const loaded = await deserializeProject(text);
80
+ setProject(loaded);
81
+ setEditorState(DEFAULT_EDITOR_STATE);
82
+ setHistory([]);
83
+ setHistoryIndex(-1);
84
+ }, []);
85
+ const setMode = useCallback((mode) => {
86
+ setEditorState((prev) => ({ ...prev, mode }));
87
+ }, []);
88
+ const setTileMapTool = useCallback((tool) => {
89
+ setEditorState((prev) => ({ ...prev, tileMapTool: tool }));
90
+ }, []);
91
+ const setSpriteSheetTool = useCallback((tool) => {
92
+ setEditorState((prev) => ({ ...prev, spriteSheetTool: tool }));
93
+ }, []);
94
+ const setZoom = useCallback((zoom) => {
95
+ setEditorState((prev) => ({ ...prev, zoom: Math.max(0.1, Math.min(10, zoom)) }));
96
+ }, []);
97
+ const setPan = useCallback((x, y) => {
98
+ setEditorState((prev) => ({ ...prev, panX: x, panY: y }));
99
+ }, []);
100
+ const toggleGrid = useCallback(() => {
101
+ setEditorState((prev) => ({ ...prev, showGrid: !prev.showGrid }));
102
+ }, []);
103
+ const toggleCollision = useCallback(() => {
104
+ setEditorState((prev) => ({ ...prev, showCollision: !prev.showCollision }));
105
+ }, []);
106
+ const toggleSnap = useCallback(() => {
107
+ setEditorState((prev) => ({ ...prev, snapToGrid: !prev.snapToGrid }));
108
+ }, []);
109
+ const addTile = useCallback(
110
+ (imageData, name) => {
111
+ const tile = createTileFromImage(imageData, name);
112
+ setProject((prev) => ({ ...prev, tileSets: [...prev.tileSets, tile] }));
113
+ pushHistory(`Add tile: ${name}`);
114
+ },
115
+ [pushHistory]
116
+ );
117
+ const removeTile = useCallback(
118
+ (id) => {
119
+ setProject((prev) => ({
120
+ ...prev,
121
+ tileSets: prev.tileSets.filter((t) => t.id !== id)
122
+ }));
123
+ pushHistory("Remove tile");
124
+ },
125
+ [pushHistory]
126
+ );
127
+ const selectTile = useCallback((id) => {
128
+ setEditorState((prev) => ({ ...prev, selectedTileId: id }));
129
+ }, []);
130
+ const importTileSet = useCallback(
131
+ async (file) => {
132
+ const dataUrl = await new Promise((resolve) => {
133
+ const reader = new FileReader();
134
+ reader.onload = () => resolve(reader.result);
135
+ reader.readAsDataURL(file);
136
+ });
137
+ const imageData = await dataUrlToImageData(dataUrl);
138
+ const name = file.name.replace(/\.[^/.]+$/, "");
139
+ addTile(imageData, name);
140
+ },
141
+ [addTile]
142
+ );
143
+ const createTileMap = useCallback(
144
+ (name, width, height) => {
145
+ const map = createEmptyTileMap(
146
+ name,
147
+ width,
148
+ height,
149
+ project.settings.defaultTileWidth,
150
+ project.settings.defaultTileHeight
151
+ );
152
+ setProject((prev) => ({ ...prev, tileMaps: [...prev.tileMaps, map] }));
153
+ setEditorState((prev) => ({
154
+ ...prev,
155
+ activeTileMapId: map.id,
156
+ activeLayerId: map.layers[0].id
157
+ }));
158
+ pushHistory(`Create tile map: ${name}`);
159
+ },
160
+ [project.settings, pushHistory]
161
+ );
162
+ const removeTileMap = useCallback(
163
+ (id) => {
164
+ setProject((prev) => ({
165
+ ...prev,
166
+ tileMaps: prev.tileMaps.filter((m) => m.id !== id)
167
+ }));
168
+ pushHistory("Remove tile map");
169
+ },
170
+ [pushHistory]
171
+ );
172
+ const selectTileMap = useCallback(
173
+ (id) => {
174
+ setEditorState((prev) => {
175
+ const map = project.tileMaps.find((m) => m.id === id);
176
+ return {
177
+ ...prev,
178
+ activeTileMapId: id,
179
+ activeLayerId: map?.layers[0]?.id ?? null
331
180
  };
332
- setProject((prev) => ({ ...prev, spriteSheets: [...prev.spriteSheets, sheet] }));
333
- setEditorState((prev) => ({ ...prev, activeSpriteSheetId: sheet.id }));
334
- pushHistory(`Import sprite sheet: ${sheet.name}`);
335
- }, [pushHistory]);
336
- const removeSpriteSheet = useCallback((id) => {
337
- setProject((prev) => ({
338
- ...prev,
339
- spriteSheets: prev.spriteSheets.filter((s) => s.id !== id),
340
- }));
341
- pushHistory('Remove sprite sheet');
342
- }, [pushHistory]);
343
- const selectSpriteSheet = useCallback((id) => {
344
- setEditorState((prev) => ({ ...prev, activeSpriteSheetId: id, selectedFrameIds: [] }));
345
- }, []);
346
- const autoSlice = useCallback((frameWidth, frameHeight) => {
347
- const sheet = project.spriteSheets.find((s) => s.id === editorState.activeSpriteSheetId);
348
- if (!(sheet === null || sheet === void 0 ? void 0 : sheet.imageData)) {
349
- return;
350
- }
351
- const frames = autoSliceSpriteSheet(sheet.imageData, frameWidth, frameHeight);
352
- setProject((prev) => ({
353
- ...prev,
354
- spriteSheets: prev.spriteSheets.map((s) => (s.id === sheet.id ? { ...s, frames } : s)),
355
- }));
356
- pushHistory('Auto-slice sprite sheet');
357
- }, [project.spriteSheets, editorState.activeSpriteSheetId, pushHistory]);
358
- const detectSprites = useCallback(() => {
359
- const sheet = project.spriteSheets.find((s) => s.id === editorState.activeSpriteSheetId);
360
- if (!(sheet === null || sheet === void 0 ? void 0 : sheet.imageData)) {
361
- return;
362
- }
363
- const frames = detectFrames(sheet.imageData);
364
- setProject((prev) => ({
365
- ...prev,
366
- spriteSheets: prev.spriteSheets.map((s) => (s.id === sheet.id ? { ...s, frames } : s)),
367
- }));
368
- pushHistory('Detect sprites');
369
- }, [project.spriteSheets, editorState.activeSpriteSheetId, pushHistory]);
370
- const addFrame = useCallback((frame) => {
371
- const newFrame = { ...frame, id: generateId('frame') };
372
- setProject((prev) => ({
373
- ...prev,
374
- spriteSheets: prev.spriteSheets.map((s) => s.id === editorState.activeSpriteSheetId ? { ...s, frames: [...s.frames, newFrame] } : s),
375
- }));
376
- pushHistory('Add frame');
377
- }, [editorState.activeSpriteSheetId, pushHistory]);
378
- const updateFrame = useCallback((id, updates) => {
379
- setProject((prev) => ({
380
- ...prev,
381
- spriteSheets: prev.spriteSheets.map((s) => ({
382
- ...s,
383
- frames: s.frames.map((f) => (f.id === id ? { ...f, ...updates } : f)),
384
- })),
385
- }));
386
- }, []);
387
- const removeFrame = useCallback((id) => {
388
- setProject((prev) => ({
389
- ...prev,
390
- spriteSheets: prev.spriteSheets.map((s) => ({
391
- ...s,
392
- frames: s.frames.filter((f) => f.id !== id),
393
- })),
394
- }));
395
- pushHistory('Remove frame');
396
- }, [pushHistory]);
397
- const selectFrames = useCallback((ids) => {
398
- setEditorState((prev) => ({ ...prev, selectedFrameIds: ids }));
399
- }, []);
400
- // ─────────────────────────────────────────────────────────────
401
- // Animation operations
402
- // ─────────────────────────────────────────────────────────────
403
- const createAnimation = useCallback((name, frameIds) => {
404
- const animation = createSpriteAnimation(name, frameIds);
405
- setProject((prev) => ({
406
- ...prev,
407
- spriteSheets: prev.spriteSheets.map((s) => s.id === editorState.activeSpriteSheetId
408
- ? { ...s, animations: [...s.animations, animation] }
409
- : s),
410
- }));
411
- pushHistory(`Create animation: ${name}`);
412
- }, [editorState.activeSpriteSheetId, pushHistory]);
413
- const removeAnimation = useCallback((id) => {
414
- setProject((prev) => ({
415
- ...prev,
416
- spriteSheets: prev.spriteSheets.map((s) => ({
417
- ...s,
418
- animations: s.animations.filter((a) => a.id !== id),
419
- })),
420
- }));
421
- pushHistory('Remove animation');
422
- }, [pushHistory]);
423
- const updateAnimation = useCallback((id, updates) => {
424
- setProject((prev) => ({
425
- ...prev,
426
- spriteSheets: prev.spriteSheets.map((s) => ({
427
- ...s,
428
- animations: s.animations.map((a) => (a.id === id ? { ...a, ...updates } : a)),
429
- })),
430
- }));
431
- }, []);
432
- // ─────────────────────────────────────────────────────────────
433
- // State machine operations
434
- // ─────────────────────────────────────────────────────────────
435
- const createStateMachine = useCallback((name) => {
436
- const sm = {
437
- id: generateId('sm'),
438
- name,
439
- states: [],
440
- transitions: [],
441
- parameters: [],
442
- entryStateId: '',
443
- anyStateTransitions: [],
444
- };
445
- setProject((prev) => ({ ...prev, stateMachines: [...prev.stateMachines, sm] }));
446
- setEditorState((prev) => ({ ...prev, activeStateMachineId: sm.id }));
447
- pushHistory(`Create state machine: ${name}`);
448
- }, [pushHistory]);
449
- const removeStateMachine = useCallback((id) => {
450
- setProject((prev) => ({
451
- ...prev,
452
- stateMachines: prev.stateMachines.filter((sm) => sm.id !== id),
453
- }));
454
- pushHistory('Remove state machine');
455
- }, [pushHistory]);
456
- const selectStateMachine = useCallback((id) => {
457
- setEditorState((prev) => ({ ...prev, activeStateMachineId: id, selectedStateId: null }));
458
- }, []);
459
- const addState = useCallback((name, animationId) => {
460
- const state = {
461
- id: generateId('state'),
462
- name,
463
- animationId,
464
- speed: 1,
465
- loop: true,
466
- position: { x: Math.random() * 400, y: Math.random() * 300 },
467
- };
468
- setProject((prev) => ({
469
- ...prev,
470
- stateMachines: prev.stateMachines.map((sm) => sm.id === editorState.activeStateMachineId
471
- ? {
472
- ...sm,
473
- states: [...sm.states, state],
474
- entryStateId: sm.entryStateId || state.id,
475
- }
476
- : sm),
477
- }));
478
- pushHistory(`Add state: ${name}`);
479
- }, [editorState.activeStateMachineId, pushHistory]);
480
- const removeState = useCallback((id) => {
481
- setProject((prev) => ({
482
- ...prev,
483
- stateMachines: prev.stateMachines.map((sm) => ({
484
- ...sm,
485
- states: sm.states.filter((s) => s.id !== id),
486
- transitions: sm.transitions.filter((t) => t.fromStateId !== id && t.toStateId !== id),
487
- })),
488
- }));
489
- pushHistory('Remove state');
490
- }, [pushHistory]);
491
- const selectState = useCallback((id) => {
492
- setEditorState((prev) => ({ ...prev, selectedStateId: id }));
493
- }, []);
494
- const updateState = useCallback((id, updates) => {
495
- setProject((prev) => ({
496
- ...prev,
497
- stateMachines: prev.stateMachines.map((sm) => ({
498
- ...sm,
499
- states: sm.states.map((s) => (s.id === id ? { ...s, ...updates } : s)),
500
- })),
501
- }));
502
- }, []);
503
- const addTransition = useCallback((fromId, toId) => {
504
- const transition = {
505
- id: generateId('trans'),
506
- fromStateId: fromId,
507
- toStateId: toId,
508
- condition: { type: 'trigger', paramName: '' },
509
- duration: 0.1,
510
- priority: 0,
511
- };
512
- setProject((prev) => ({
513
- ...prev,
514
- stateMachines: prev.stateMachines.map((sm) => sm.id === editorState.activeStateMachineId
515
- ? { ...sm, transitions: [...sm.transitions, transition] }
516
- : sm),
517
- }));
518
- pushHistory('Add transition');
519
- }, [editorState.activeStateMachineId, pushHistory]);
520
- const removeTransition = useCallback((id) => {
521
- setProject((prev) => ({
522
- ...prev,
523
- stateMachines: prev.stateMachines.map((sm) => ({
524
- ...sm,
525
- transitions: sm.transitions.filter((t) => t.id !== id),
526
- })),
527
- }));
528
- pushHistory('Remove transition');
529
- }, [pushHistory]);
530
- const addParameter = useCallback((name, type) => {
531
- const param = {
532
- id: generateId('param'),
533
- name,
534
- type,
535
- defaultValue: type === 'bool' ? false : 0,
536
- };
537
- setProject((prev) => ({
538
- ...prev,
539
- stateMachines: prev.stateMachines.map((sm) => sm.id === editorState.activeStateMachineId
540
- ? { ...sm, parameters: [...sm.parameters, param] }
541
- : sm),
542
- }));
543
- pushHistory(`Add parameter: ${name}`);
544
- }, [editorState.activeStateMachineId, pushHistory]);
545
- const removeParameter = useCallback((id) => {
546
- setProject((prev) => ({
547
- ...prev,
548
- stateMachines: prev.stateMachines.map((sm) => ({
549
- ...sm,
550
- parameters: sm.parameters.filter((p) => p.id !== id),
551
- })),
552
- }));
553
- pushHistory('Remove parameter');
554
- }, [pushHistory]);
555
- // ─────────────────────────────────────────────────────────────
556
- // Effects operations
557
- // ─────────────────────────────────────────────────────────────
558
- const addEffect = useCallback((type) => {
559
- const defaultParams = {
560
- // Effect colours are pixel values parsed as 6-digit hex — never theme tokens.
561
- outline: { color: '#000000', thickness: 1 },
562
- // audit-allow: literal-color +2
563
- shadow: { color: '#00000080', offsetX: 2, offsetY: 2, blur: 0 },
564
- glow: { color: '#ffffff', size: 4, intensity: 0.8 },
565
- 'palette-swap': { from: '#ffffff', to: '#ff0000' },
566
- pixelate: { size: 4 },
567
- blur: { radius: 2 },
568
- brightness: { value: 0 },
569
- contrast: { value: 0 },
570
- saturation: { value: 0 },
571
- 'hue-shift': { degrees: 0 },
572
- invert: {},
573
- tint: { color: '#ffffff', amount: 0.5 },
574
- };
575
- const effect = {
576
- id: generateId('effect'),
577
- type,
578
- enabled: true,
579
- params: { ...defaultParams[type] },
580
- };
581
- setCurrentEffects((prev) => [...prev, effect]);
582
- }, []);
583
- const removeEffect = useCallback((id) => {
584
- setCurrentEffects((prev) => prev.filter((e) => e.id !== id));
585
- }, []);
586
- const updateEffect = useCallback((id, updates) => {
587
- setCurrentEffects((prev) => prev.map((e) => (e.id === id ? { ...e, ...updates } : e)));
588
- }, []);
589
- const toggleEffect = useCallback((id) => {
590
- setCurrentEffects((prev) => prev.map((e) => (e.id === id ? { ...e, enabled: !e.enabled } : e)));
591
- }, []);
592
- const saveEffectPreset = useCallback((name) => {
593
- const preset = {
594
- id: generateId('preset'),
595
- name,
596
- effects: currentEffects.map((e) => ({ ...e, id: generateId('effect') })),
597
- };
598
- setProject((prev) => ({ ...prev, effectPresets: [...prev.effectPresets, preset] }));
599
- pushHistory(`Save effect preset: ${name}`);
600
- }, [currentEffects, pushHistory]);
601
- const loadEffectPreset = useCallback((id) => {
602
- const preset = project.effectPresets.find((p) => p.id === id);
603
- if (preset) {
604
- setCurrentEffects(preset.effects.map((e) => ({ ...e, id: generateId('effect') })));
605
- }
606
- }, [project.effectPresets]);
607
- const previewEffects = useCallback((imageData) => {
608
- return applyEffects(imageData, currentEffects);
609
- }, [currentEffects]);
610
- // ─────────────────────────────────────────────────────────────
611
- // Export operations
612
- // ─────────────────────────────────────────────────────────────
613
- const exportTileMap = useCallback((format) => {
614
- var _a;
615
- const map = project.tileMaps.find((m) => m.id === editorState.activeTileMapId);
181
+ });
182
+ },
183
+ [project.tileMaps]
184
+ );
185
+ const setTile = useCallback((layerId, x, y, tileId) => {
186
+ setProject((prev) => ({
187
+ ...prev,
188
+ tileMaps: prev.tileMaps.map((map) => ({
189
+ ...map,
190
+ layers: map.layers.map((layer) => {
191
+ if (layer.id !== layerId) {
192
+ return layer;
193
+ }
194
+ const newTiles = layer.tiles.map((row) => [...row]);
195
+ if (y >= 0 && y < newTiles.length && x >= 0 && x < newTiles[0].length) {
196
+ newTiles[y][x] = tileId;
197
+ }
198
+ return { ...layer, tiles: newTiles };
199
+ })
200
+ }))
201
+ }));
202
+ }, []);
203
+ const fillTiles = useCallback(
204
+ (layerId, startX, startY, newTileId) => {
205
+ setProject((prev) => {
206
+ const map = prev.tileMaps.find((m) => m.layers.some((l) => l.id === layerId));
616
207
  if (!map) {
617
- return;
618
- }
619
- if (format === 'json') {
620
- downloadJson(map, `${map.name}.json`);
208
+ return prev;
621
209
  }
622
- else if (format === 'tiled-json') {
623
- const tiledData = exportToTiledJson(map, project.tileSets);
624
- downloadJson(tiledData, `${map.name}.tmj`);
625
- }
626
- else if (format === 'png') {
627
- // Render map to canvas and export
628
- const canvas = document.createElement('canvas');
629
- canvas.width = map.width * map.tileWidth;
630
- canvas.height = map.height * map.tileHeight;
631
- const ctx = canvas.getContext('2d');
632
- ctx.fillStyle = map.backgroundColor;
633
- ctx.fillRect(0, 0, canvas.width, canvas.height);
634
- for (const layer of map.layers.filter((l) => l.visible)) {
635
- ctx.globalAlpha = layer.opacity;
636
- for (let y = 0; y < map.height; y++) {
637
- for (let x = 0; x < map.width; x++) {
638
- const tileId = (_a = layer.tiles[y]) === null || _a === void 0 ? void 0 : _a[x];
639
- if (tileId) {
640
- const tile = project.tileSets.find((t) => t.id === tileId);
641
- if (tile === null || tile === void 0 ? void 0 : tile.image) {
642
- const tempCanvas = document.createElement('canvas');
643
- tempCanvas.width = tile.image.width;
644
- tempCanvas.height = tile.image.height;
645
- tempCanvas.getContext('2d').putImageData(tile.image, 0, 0);
646
- ctx.drawImage(tempCanvas, x * map.tileWidth, y * map.tileHeight);
647
- }
648
- }
649
- }
650
- }
651
- }
652
- downloadPng(canvas, `${map.name}.png`);
210
+ const layer = map.layers.find((l) => l.id === layerId);
211
+ if (!layer) {
212
+ return prev;
653
213
  }
654
- onExport === null || onExport === void 0 ? void 0 : onExport(JSON.stringify(map), format);
655
- }, [project, editorState.activeTileMapId, onExport]);
656
- const exportSpriteSheet = useCallback((format) => {
657
- const sheet = project.spriteSheets.find((s) => s.id === editorState.activeSpriteSheetId);
658
- if (!sheet) {
659
- return;
214
+ const targetId = layer.tiles[startY]?.[startX];
215
+ if (targetId === newTileId) {
216
+ return prev;
660
217
  }
661
- if (format === 'json') {
662
- const exportData = {
663
- name: sheet.name,
664
- width: sheet.width,
665
- height: sheet.height,
666
- frames: sheet.frames,
667
- animations: sheet.animations,
668
- };
669
- downloadJson(exportData, `${sheet.name}.json`);
218
+ const newTiles = layer.tiles.map((row) => [...row]);
219
+ const stack = [[startX, startY]];
220
+ const visited = /* @__PURE__ */ new Set();
221
+ while (stack.length > 0) {
222
+ const [x, y] = stack.pop();
223
+ const key = `${x},${y}`;
224
+ if (visited.has(key)) {
225
+ continue;
226
+ }
227
+ if (y < 0 || y >= newTiles.length || x < 0 || x >= newTiles[0].length) {
228
+ continue;
229
+ }
230
+ if (newTiles[y][x] !== targetId) {
231
+ continue;
232
+ }
233
+ visited.add(key);
234
+ newTiles[y][x] = newTileId;
235
+ stack.push([x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]);
670
236
  }
671
- else if (format === 'atlas') {
672
- // TexturePacker-compatible format
673
- const framesObj = {};
674
- for (const f of sheet.frames) {
675
- framesObj[f.name] = {
676
- frame: { x: f.x, y: f.y, w: f.width, h: f.height },
677
- pivot: { x: f.pivotX, y: f.pivotY },
678
- sourceSize: { w: f.width, h: f.height },
679
- };
237
+ return {
238
+ ...prev,
239
+ tileMaps: prev.tileMaps.map(
240
+ (m) => m.id === map.id ? {
241
+ ...m,
242
+ layers: m.layers.map((l) => l.id === layerId ? { ...l, tiles: newTiles } : l)
243
+ } : m
244
+ )
245
+ };
246
+ });
247
+ pushHistory("Fill tiles");
248
+ },
249
+ [pushHistory]
250
+ );
251
+ const addLayer = useCallback(() => {
252
+ const map = project.tileMaps.find((m) => m.id === editorState.activeTileMapId);
253
+ if (!map) {
254
+ return;
255
+ }
256
+ const newLayer = createEmptyTileLayer(`Layer ${map.layers.length + 1}`, map.width, map.height);
257
+ setProject((prev) => ({
258
+ ...prev,
259
+ tileMaps: prev.tileMaps.map(
260
+ (m) => m.id === map.id ? { ...m, layers: [...m.layers, newLayer] } : m
261
+ )
262
+ }));
263
+ setEditorState((prev) => ({ ...prev, activeLayerId: newLayer.id }));
264
+ pushHistory("Add layer");
265
+ }, [project.tileMaps, editorState.activeTileMapId, pushHistory]);
266
+ const removeLayer = useCallback(
267
+ (id) => {
268
+ setProject((prev) => ({
269
+ ...prev,
270
+ tileMaps: prev.tileMaps.map((m) => ({
271
+ ...m,
272
+ layers: m.layers.filter((l) => l.id !== id)
273
+ }))
274
+ }));
275
+ pushHistory("Remove layer");
276
+ },
277
+ [pushHistory]
278
+ );
279
+ const selectLayer = useCallback((id) => {
280
+ setEditorState((prev) => ({ ...prev, activeLayerId: id }));
281
+ }, []);
282
+ const toggleLayerVisibility = useCallback((id) => {
283
+ setProject((prev) => ({
284
+ ...prev,
285
+ tileMaps: prev.tileMaps.map((m) => ({
286
+ ...m,
287
+ layers: m.layers.map((l) => l.id === id ? { ...l, visible: !l.visible } : l)
288
+ }))
289
+ }));
290
+ }, []);
291
+ const setLayerOpacity = useCallback((id, opacity) => {
292
+ setProject((prev) => ({
293
+ ...prev,
294
+ tileMaps: prev.tileMaps.map((m) => ({
295
+ ...m,
296
+ layers: m.layers.map((l) => l.id === id ? { ...l, opacity } : l)
297
+ }))
298
+ }));
299
+ }, []);
300
+ const moveLayer = useCallback(
301
+ (id, direction) => {
302
+ setProject((prev) => ({
303
+ ...prev,
304
+ tileMaps: prev.tileMaps.map((m) => {
305
+ const idx = m.layers.findIndex((l) => l.id === id);
306
+ if (idx === -1) {
307
+ return m;
308
+ }
309
+ const newIdx = direction === "up" ? idx - 1 : idx + 1;
310
+ if (newIdx < 0 || newIdx >= m.layers.length) {
311
+ return m;
312
+ }
313
+ const newLayers = [...m.layers];
314
+ [newLayers[idx], newLayers[newIdx]] = [newLayers[newIdx], newLayers[idx]];
315
+ return { ...m, layers: newLayers };
316
+ })
317
+ }));
318
+ pushHistory(`Move layer ${direction}`);
319
+ },
320
+ [pushHistory]
321
+ );
322
+ const importSpriteSheet = useCallback(
323
+ async (file) => {
324
+ const dataUrl = await new Promise((resolve) => {
325
+ const reader = new FileReader();
326
+ reader.onload = () => resolve(reader.result);
327
+ reader.readAsDataURL(file);
328
+ });
329
+ const image = await loadImage(dataUrl);
330
+ const canvas = document.createElement("canvas");
331
+ canvas.width = image.width;
332
+ canvas.height = image.height;
333
+ const ctx = canvas.getContext("2d");
334
+ ctx.drawImage(image, 0, 0);
335
+ const imageData = ctx.getImageData(0, 0, image.width, image.height);
336
+ const sheet = {
337
+ id: generateId("sheet"),
338
+ name: file.name.replace(/\.[^/.]+$/, ""),
339
+ image,
340
+ imageData,
341
+ dataUrl,
342
+ width: image.width,
343
+ height: image.height,
344
+ frames: [],
345
+ animations: []
346
+ };
347
+ setProject((prev) => ({ ...prev, spriteSheets: [...prev.spriteSheets, sheet] }));
348
+ setEditorState((prev) => ({ ...prev, activeSpriteSheetId: sheet.id }));
349
+ pushHistory(`Import sprite sheet: ${sheet.name}`);
350
+ },
351
+ [pushHistory]
352
+ );
353
+ const removeSpriteSheet = useCallback(
354
+ (id) => {
355
+ setProject((prev) => ({
356
+ ...prev,
357
+ spriteSheets: prev.spriteSheets.filter((s) => s.id !== id)
358
+ }));
359
+ pushHistory("Remove sprite sheet");
360
+ },
361
+ [pushHistory]
362
+ );
363
+ const selectSpriteSheet = useCallback((id) => {
364
+ setEditorState((prev) => ({ ...prev, activeSpriteSheetId: id, selectedFrameIds: [] }));
365
+ }, []);
366
+ const autoSlice = useCallback(
367
+ (frameWidth, frameHeight) => {
368
+ const sheet = project.spriteSheets.find((s) => s.id === editorState.activeSpriteSheetId);
369
+ if (!sheet?.imageData) {
370
+ return;
371
+ }
372
+ const frames = autoSliceSpriteSheet(sheet.imageData, frameWidth, frameHeight);
373
+ setProject((prev) => ({
374
+ ...prev,
375
+ spriteSheets: prev.spriteSheets.map((s) => s.id === sheet.id ? { ...s, frames } : s)
376
+ }));
377
+ pushHistory("Auto-slice sprite sheet");
378
+ },
379
+ [project.spriteSheets, editorState.activeSpriteSheetId, pushHistory]
380
+ );
381
+ const detectSprites = useCallback(() => {
382
+ const sheet = project.spriteSheets.find((s) => s.id === editorState.activeSpriteSheetId);
383
+ if (!sheet?.imageData) {
384
+ return;
385
+ }
386
+ const frames = detectFrames(sheet.imageData);
387
+ setProject((prev) => ({
388
+ ...prev,
389
+ spriteSheets: prev.spriteSheets.map((s) => s.id === sheet.id ? { ...s, frames } : s)
390
+ }));
391
+ pushHistory("Detect sprites");
392
+ }, [project.spriteSheets, editorState.activeSpriteSheetId, pushHistory]);
393
+ const addFrame = useCallback(
394
+ (frame) => {
395
+ const newFrame = { ...frame, id: generateId("frame") };
396
+ setProject((prev) => ({
397
+ ...prev,
398
+ spriteSheets: prev.spriteSheets.map(
399
+ (s) => s.id === editorState.activeSpriteSheetId ? { ...s, frames: [...s.frames, newFrame] } : s
400
+ )
401
+ }));
402
+ pushHistory("Add frame");
403
+ },
404
+ [editorState.activeSpriteSheetId, pushHistory]
405
+ );
406
+ const updateFrame = useCallback((id, updates) => {
407
+ setProject((prev) => ({
408
+ ...prev,
409
+ spriteSheets: prev.spriteSheets.map((s) => ({
410
+ ...s,
411
+ frames: s.frames.map((f) => f.id === id ? { ...f, ...updates } : f)
412
+ }))
413
+ }));
414
+ }, []);
415
+ const removeFrame = useCallback(
416
+ (id) => {
417
+ setProject((prev) => ({
418
+ ...prev,
419
+ spriteSheets: prev.spriteSheets.map((s) => ({
420
+ ...s,
421
+ frames: s.frames.filter((f) => f.id !== id)
422
+ }))
423
+ }));
424
+ pushHistory("Remove frame");
425
+ },
426
+ [pushHistory]
427
+ );
428
+ const selectFrames = useCallback((ids) => {
429
+ setEditorState((prev) => ({ ...prev, selectedFrameIds: ids }));
430
+ }, []);
431
+ const createAnimation = useCallback(
432
+ (name, frameIds) => {
433
+ const animation = createSpriteAnimation(name, frameIds);
434
+ setProject((prev) => ({
435
+ ...prev,
436
+ spriteSheets: prev.spriteSheets.map(
437
+ (s) => s.id === editorState.activeSpriteSheetId ? { ...s, animations: [...s.animations, animation] } : s
438
+ )
439
+ }));
440
+ pushHistory(`Create animation: ${name}`);
441
+ },
442
+ [editorState.activeSpriteSheetId, pushHistory]
443
+ );
444
+ const removeAnimation = useCallback(
445
+ (id) => {
446
+ setProject((prev) => ({
447
+ ...prev,
448
+ spriteSheets: prev.spriteSheets.map((s) => ({
449
+ ...s,
450
+ animations: s.animations.filter((a) => a.id !== id)
451
+ }))
452
+ }));
453
+ pushHistory("Remove animation");
454
+ },
455
+ [pushHistory]
456
+ );
457
+ const updateAnimation = useCallback((id, updates) => {
458
+ setProject((prev) => ({
459
+ ...prev,
460
+ spriteSheets: prev.spriteSheets.map((s) => ({
461
+ ...s,
462
+ animations: s.animations.map((a) => a.id === id ? { ...a, ...updates } : a)
463
+ }))
464
+ }));
465
+ }, []);
466
+ const createStateMachine = useCallback(
467
+ (name) => {
468
+ const sm = {
469
+ id: generateId("sm"),
470
+ name,
471
+ states: [],
472
+ transitions: [],
473
+ parameters: [],
474
+ entryStateId: "",
475
+ anyStateTransitions: []
476
+ };
477
+ setProject((prev) => ({ ...prev, stateMachines: [...prev.stateMachines, sm] }));
478
+ setEditorState((prev) => ({ ...prev, activeStateMachineId: sm.id }));
479
+ pushHistory(`Create state machine: ${name}`);
480
+ },
481
+ [pushHistory]
482
+ );
483
+ const removeStateMachine = useCallback(
484
+ (id) => {
485
+ setProject((prev) => ({
486
+ ...prev,
487
+ stateMachines: prev.stateMachines.filter((sm) => sm.id !== id)
488
+ }));
489
+ pushHistory("Remove state machine");
490
+ },
491
+ [pushHistory]
492
+ );
493
+ const selectStateMachine = useCallback((id) => {
494
+ setEditorState((prev) => ({ ...prev, activeStateMachineId: id, selectedStateId: null }));
495
+ }, []);
496
+ const addState = useCallback(
497
+ (name, animationId) => {
498
+ const state = {
499
+ id: generateId("state"),
500
+ name,
501
+ animationId,
502
+ speed: 1,
503
+ loop: true,
504
+ position: { x: Math.random() * 400, y: Math.random() * 300 }
505
+ };
506
+ setProject((prev) => ({
507
+ ...prev,
508
+ stateMachines: prev.stateMachines.map(
509
+ (sm) => sm.id === editorState.activeStateMachineId ? {
510
+ ...sm,
511
+ states: [...sm.states, state],
512
+ entryStateId: sm.entryStateId || state.id
513
+ } : sm
514
+ )
515
+ }));
516
+ pushHistory(`Add state: ${name}`);
517
+ },
518
+ [editorState.activeStateMachineId, pushHistory]
519
+ );
520
+ const removeState = useCallback(
521
+ (id) => {
522
+ setProject((prev) => ({
523
+ ...prev,
524
+ stateMachines: prev.stateMachines.map((sm) => ({
525
+ ...sm,
526
+ states: sm.states.filter((s) => s.id !== id),
527
+ transitions: sm.transitions.filter((t) => t.fromStateId !== id && t.toStateId !== id)
528
+ }))
529
+ }));
530
+ pushHistory("Remove state");
531
+ },
532
+ [pushHistory]
533
+ );
534
+ const selectState = useCallback((id) => {
535
+ setEditorState((prev) => ({ ...prev, selectedStateId: id }));
536
+ }, []);
537
+ const updateState = useCallback((id, updates) => {
538
+ setProject((prev) => ({
539
+ ...prev,
540
+ stateMachines: prev.stateMachines.map((sm) => ({
541
+ ...sm,
542
+ states: sm.states.map((s) => s.id === id ? { ...s, ...updates } : s)
543
+ }))
544
+ }));
545
+ }, []);
546
+ const addTransition = useCallback(
547
+ (fromId, toId) => {
548
+ const transition = {
549
+ id: generateId("trans"),
550
+ fromStateId: fromId,
551
+ toStateId: toId,
552
+ condition: { type: "trigger", paramName: "" },
553
+ duration: 0.1,
554
+ priority: 0
555
+ };
556
+ setProject((prev) => ({
557
+ ...prev,
558
+ stateMachines: prev.stateMachines.map(
559
+ (sm) => sm.id === editorState.activeStateMachineId ? { ...sm, transitions: [...sm.transitions, transition] } : sm
560
+ )
561
+ }));
562
+ pushHistory("Add transition");
563
+ },
564
+ [editorState.activeStateMachineId, pushHistory]
565
+ );
566
+ const removeTransition = useCallback(
567
+ (id) => {
568
+ setProject((prev) => ({
569
+ ...prev,
570
+ stateMachines: prev.stateMachines.map((sm) => ({
571
+ ...sm,
572
+ transitions: sm.transitions.filter((t) => t.id !== id)
573
+ }))
574
+ }));
575
+ pushHistory("Remove transition");
576
+ },
577
+ [pushHistory]
578
+ );
579
+ const addParameter = useCallback(
580
+ (name, type) => {
581
+ const param = {
582
+ id: generateId("param"),
583
+ name,
584
+ type,
585
+ defaultValue: type === "bool" ? false : 0
586
+ };
587
+ setProject((prev) => ({
588
+ ...prev,
589
+ stateMachines: prev.stateMachines.map(
590
+ (sm) => sm.id === editorState.activeStateMachineId ? { ...sm, parameters: [...sm.parameters, param] } : sm
591
+ )
592
+ }));
593
+ pushHistory(`Add parameter: ${name}`);
594
+ },
595
+ [editorState.activeStateMachineId, pushHistory]
596
+ );
597
+ const removeParameter = useCallback(
598
+ (id) => {
599
+ setProject((prev) => ({
600
+ ...prev,
601
+ stateMachines: prev.stateMachines.map((sm) => ({
602
+ ...sm,
603
+ parameters: sm.parameters.filter((p) => p.id !== id)
604
+ }))
605
+ }));
606
+ pushHistory("Remove parameter");
607
+ },
608
+ [pushHistory]
609
+ );
610
+ const addEffect = useCallback((type) => {
611
+ const defaultParams = {
612
+ // Effect colours are pixel values parsed as 6-digit hex — never theme tokens.
613
+ outline: { color: "#000000", thickness: 1 },
614
+ // audit-allow: literal-color +2
615
+ shadow: { color: "#00000080", offsetX: 2, offsetY: 2, blur: 0 },
616
+ glow: { color: "#ffffff", size: 4, intensity: 0.8 },
617
+ "palette-swap": { from: "#ffffff", to: "#ff0000" },
618
+ pixelate: { size: 4 },
619
+ blur: { radius: 2 },
620
+ brightness: { value: 0 },
621
+ contrast: { value: 0 },
622
+ saturation: { value: 0 },
623
+ "hue-shift": { degrees: 0 },
624
+ invert: {},
625
+ tint: { color: "#ffffff", amount: 0.5 }
626
+ };
627
+ const effect = {
628
+ id: generateId("effect"),
629
+ type,
630
+ enabled: true,
631
+ params: { ...defaultParams[type] }
632
+ };
633
+ setCurrentEffects((prev) => [...prev, effect]);
634
+ }, []);
635
+ const removeEffect = useCallback((id) => {
636
+ setCurrentEffects((prev) => prev.filter((e) => e.id !== id));
637
+ }, []);
638
+ const updateEffect = useCallback((id, updates) => {
639
+ setCurrentEffects((prev) => prev.map((e) => e.id === id ? { ...e, ...updates } : e));
640
+ }, []);
641
+ const toggleEffect = useCallback((id) => {
642
+ setCurrentEffects((prev) => prev.map((e) => e.id === id ? { ...e, enabled: !e.enabled } : e));
643
+ }, []);
644
+ const saveEffectPreset = useCallback(
645
+ (name) => {
646
+ const preset = {
647
+ id: generateId("preset"),
648
+ name,
649
+ effects: currentEffects.map((e) => ({ ...e, id: generateId("effect") }))
650
+ };
651
+ setProject((prev) => ({ ...prev, effectPresets: [...prev.effectPresets, preset] }));
652
+ pushHistory(`Save effect preset: ${name}`);
653
+ },
654
+ [currentEffects, pushHistory]
655
+ );
656
+ const loadEffectPreset = useCallback(
657
+ (id) => {
658
+ const preset = project.effectPresets.find((p) => p.id === id);
659
+ if (preset) {
660
+ setCurrentEffects(preset.effects.map((e) => ({ ...e, id: generateId("effect") })));
661
+ }
662
+ },
663
+ [project.effectPresets]
664
+ );
665
+ const previewEffects = useCallback(
666
+ (imageData) => {
667
+ return applyEffects(imageData, currentEffects);
668
+ },
669
+ [currentEffects]
670
+ );
671
+ const exportTileMap = useCallback(
672
+ (format) => {
673
+ const map = project.tileMaps.find((m) => m.id === editorState.activeTileMapId);
674
+ if (!map) {
675
+ return;
676
+ }
677
+ if (format === "json") {
678
+ downloadJson(map, `${map.name}.json`);
679
+ } else if (format === "tiled-json") {
680
+ const tiledData = exportToTiledJson(map, project.tileSets);
681
+ downloadJson(tiledData, `${map.name}.tmj`);
682
+ } else if (format === "png") {
683
+ const canvas = document.createElement("canvas");
684
+ canvas.width = map.width * map.tileWidth;
685
+ canvas.height = map.height * map.tileHeight;
686
+ const ctx = canvas.getContext("2d");
687
+ ctx.fillStyle = map.backgroundColor;
688
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
689
+ for (const layer of map.layers.filter((l) => l.visible)) {
690
+ ctx.globalAlpha = layer.opacity;
691
+ for (let y = 0; y < map.height; y++) {
692
+ for (let x = 0; x < map.width; x++) {
693
+ const tileId = layer.tiles[y]?.[x];
694
+ if (tileId) {
695
+ const tile = project.tileSets.find((t) => t.id === tileId);
696
+ if (tile?.image) {
697
+ const tempCanvas = document.createElement("canvas");
698
+ tempCanvas.width = tile.image.width;
699
+ tempCanvas.height = tile.image.height;
700
+ tempCanvas.getContext("2d").putImageData(tile.image, 0, 0);
701
+ ctx.drawImage(tempCanvas, x * map.tileWidth, y * map.tileHeight);
702
+ }
703
+ }
680
704
  }
681
- const atlas = {
682
- frames: framesObj,
683
- meta: {
684
- app: 'GameAsset2dEditor',
685
- version: '1.0',
686
- image: `${sheet.name}.png`,
687
- size: { w: sheet.width, h: sheet.height },
688
- },
689
- };
690
- downloadJson(atlas, `${sheet.name}.atlas.json`);
705
+ }
691
706
  }
692
- onExport === null || onExport === void 0 ? void 0 : onExport(JSON.stringify(sheet), format);
693
- }, [project.spriteSheets, editorState.activeSpriteSheetId, onExport]);
694
- const exportStateMachine = useCallback((format) => {
695
- const sm = project.stateMachines.find((s) => s.id === editorState.activeStateMachineId);
696
- if (!sm) {
697
- return;
707
+ downloadPng(canvas, `${map.name}.png`);
708
+ }
709
+ onExport?.(JSON.stringify(map), format);
710
+ },
711
+ [project, editorState.activeTileMapId, onExport]
712
+ );
713
+ const exportSpriteSheet = useCallback(
714
+ (format) => {
715
+ const sheet = project.spriteSheets.find((s) => s.id === editorState.activeSpriteSheetId);
716
+ if (!sheet) {
717
+ return;
718
+ }
719
+ if (format === "json") {
720
+ const exportData = {
721
+ name: sheet.name,
722
+ width: sheet.width,
723
+ height: sheet.height,
724
+ frames: sheet.frames,
725
+ animations: sheet.animations
726
+ };
727
+ downloadJson(exportData, `${sheet.name}.json`);
728
+ } else if (format === "atlas") {
729
+ const framesObj = {};
730
+ for (const f of sheet.frames) {
731
+ framesObj[f.name] = {
732
+ frame: { x: f.x, y: f.y, w: f.width, h: f.height },
733
+ pivot: { x: f.pivotX, y: f.pivotY },
734
+ sourceSize: { w: f.width, h: f.height }
735
+ };
698
736
  }
699
- downloadJson(sm, `${sm.name}.statemachine.json`);
700
- onExport === null || onExport === void 0 ? void 0 : onExport(JSON.stringify(sm), format);
701
- }, [project.stateMachines, editorState.activeStateMachineId, onExport]);
702
- // ─────────────────────────────────────────────────────────────
703
- // Return API
704
- // ─────────────────────────────────────────────────────────────
705
- return {
706
- project,
707
- setProject,
708
- newProject,
709
- saveProject,
710
- loadProject,
711
- editorState,
712
- setMode,
713
- setTileMapTool,
714
- setSpriteSheetTool,
715
- setZoom,
716
- setPan,
717
- toggleGrid,
718
- toggleCollision,
719
- toggleSnap,
720
- addTile,
721
- removeTile,
722
- selectTile,
723
- importTileSet,
724
- createTileMap,
725
- removeTileMap,
726
- selectTileMap,
727
- setTile,
728
- fillTiles,
729
- addLayer,
730
- removeLayer,
731
- selectLayer,
732
- toggleLayerVisibility,
733
- setLayerOpacity,
734
- moveLayer,
735
- importSpriteSheet,
736
- removeSpriteSheet,
737
- selectSpriteSheet,
738
- autoSlice,
739
- detectSprites,
740
- addFrame,
741
- updateFrame,
742
- removeFrame,
743
- selectFrames,
744
- createAnimation,
745
- removeAnimation,
746
- updateAnimation,
747
- createStateMachine,
748
- removeStateMachine,
749
- selectStateMachine,
750
- addState,
751
- removeState,
752
- selectState,
753
- updateState,
754
- addTransition,
755
- removeTransition,
756
- addParameter,
757
- removeParameter,
758
- addEffect,
759
- removeEffect,
760
- updateEffect,
761
- toggleEffect,
762
- saveEffectPreset,
763
- loadEffectPreset,
764
- previewEffects,
765
- undo,
766
- redo,
767
- canUndo: historyIndex > 0,
768
- canRedo: historyIndex < history.length - 1,
769
- exportTileMap,
770
- exportSpriteSheet,
771
- exportStateMachine,
772
- canvasRef,
773
- fileInputRef,
774
- };
737
+ const atlas = {
738
+ frames: framesObj,
739
+ meta: {
740
+ app: "GameAsset2dEditor",
741
+ version: "1.0",
742
+ image: `${sheet.name}.png`,
743
+ size: { w: sheet.width, h: sheet.height }
744
+ }
745
+ };
746
+ downloadJson(atlas, `${sheet.name}.atlas.json`);
747
+ }
748
+ onExport?.(JSON.stringify(sheet), format);
749
+ },
750
+ [project.spriteSheets, editorState.activeSpriteSheetId, onExport]
751
+ );
752
+ const exportStateMachine = useCallback(
753
+ (format) => {
754
+ const sm = project.stateMachines.find((s) => s.id === editorState.activeStateMachineId);
755
+ if (!sm) {
756
+ return;
757
+ }
758
+ downloadJson(sm, `${sm.name}.statemachine.json`);
759
+ onExport?.(JSON.stringify(sm), format);
760
+ },
761
+ [project.stateMachines, editorState.activeStateMachineId, onExport]
762
+ );
763
+ return {
764
+ project,
765
+ setProject,
766
+ newProject,
767
+ saveProject,
768
+ loadProject,
769
+ editorState,
770
+ setMode,
771
+ setTileMapTool,
772
+ setSpriteSheetTool,
773
+ setZoom,
774
+ setPan,
775
+ toggleGrid,
776
+ toggleCollision,
777
+ toggleSnap,
778
+ addTile,
779
+ removeTile,
780
+ selectTile,
781
+ importTileSet,
782
+ createTileMap,
783
+ removeTileMap,
784
+ selectTileMap,
785
+ setTile,
786
+ fillTiles,
787
+ addLayer,
788
+ removeLayer,
789
+ selectLayer,
790
+ toggleLayerVisibility,
791
+ setLayerOpacity,
792
+ moveLayer,
793
+ importSpriteSheet,
794
+ removeSpriteSheet,
795
+ selectSpriteSheet,
796
+ autoSlice,
797
+ detectSprites,
798
+ addFrame,
799
+ updateFrame,
800
+ removeFrame,
801
+ selectFrames,
802
+ createAnimation,
803
+ removeAnimation,
804
+ updateAnimation,
805
+ createStateMachine,
806
+ removeStateMachine,
807
+ selectStateMachine,
808
+ addState,
809
+ removeState,
810
+ selectState,
811
+ updateState,
812
+ addTransition,
813
+ removeTransition,
814
+ addParameter,
815
+ removeParameter,
816
+ addEffect,
817
+ removeEffect,
818
+ updateEffect,
819
+ toggleEffect,
820
+ saveEffectPreset,
821
+ loadEffectPreset,
822
+ previewEffects,
823
+ undo,
824
+ redo,
825
+ canUndo: historyIndex > 0,
826
+ canRedo: historyIndex < history.length - 1,
827
+ exportTileMap,
828
+ exportSpriteSheet,
829
+ exportStateMachine,
830
+ canvasRef,
831
+ fileInputRef
832
+ };
833
+ };
834
+ export {
835
+ useGameAssetEditor
775
836
  };
776
-
777
- export { useGameAssetEditor };