@nice2dev/ui-graphics 1.3.15 → 1.3.17

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 +13 -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,907 +1,866 @@
1
- 'use strict';
2
-
3
- var React = require('react');
4
- var editorAsk = require('../core/editorAsk.js');
5
- var vectorBooleanOps = require('./vectorBooleanOps.js');
6
- var vectorEditorExport = require('./vectorEditorExport.js');
7
- var vectorEditorImport = require('./vectorEditorImport.js');
8
- var vectorEditorTypes = require('./vectorEditorTypes.js');
9
-
10
- /**
11
- * useVectorEditor — extracted logic + state for VectorEditor.
12
- * Keeps every useState / useRef / useCallback / useEffect in a single hook
13
- * so the component file is pure layout.
14
- */
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const React = require("react");
4
+ const editorAsk = require("../core/editorAsk.js");
5
+ const vectorBooleanOps = require("./vectorBooleanOps.js");
6
+ const vectorEditorExport = require("./vectorEditorExport.js");
7
+ const vectorEditorImport = require("./vectorEditorImport.js");
8
+ const vectorEditorTypes = require("./vectorEditorTypes.js");
15
9
  function useVectorEditor({ artW, artH, onSaveToLibrary }) {
16
- // the library's dialogs for the text tool and the library name (not window.prompt)
17
- const { pendingAsk, ask } = editorAsk.useEditorAsk();
18
- /* ── state ── */
19
- const [shapes, setShapes] = React.useState([]);
20
- const [selId, setSelId] = React.useState(null);
21
- const [tool, setTool] = React.useState('pointer');
22
- // audit-allow: literal-color — the first fill of a new shape - document data
23
- const [fillCol, setFillCol] = React.useState('#5566cc');
24
- const [strokeCol, setStrokeCol] = React.useState('#000000');
25
- const [strokeW, setStrokeW] = React.useState(2);
26
- const [fontSize, setFontSize] = React.useState(24);
27
- const [fontFamily, setFontFamily] = React.useState('sans-serif');
28
- const [shapeOpacity, setShapeOpacity] = React.useState(1);
29
- const [cornerRadius, setCornerRadius] = React.useState(12);
30
- const [polygonSides, setPolygonSides] = React.useState(6);
31
- const [starPoints2, setStarPoints2] = React.useState(5);
32
- const [starInnerRatio, setStarInnerRatio] = React.useState(0.4);
33
- const [zoom, setZoom] = React.useState(1);
34
- const [px, setPx] = React.useState(0);
35
- const [py, setPy] = React.useState(0);
36
- const [dlgSvg, setDlgSvg] = React.useState(false);
37
- const [svgText, setSvgText] = React.useState('');
38
- const [snapGrid, setSnapGrid] = React.useState(false);
39
- const [gridSize, setGridSize] = React.useState(16);
40
- const [hasClipboard, setHasClipboard] = React.useState(false);
41
- /* ── refs ── */
42
- const boxRef = React.useRef(null);
43
- const drawing = React.useRef(false);
44
- /** Becomes true on first onMove after onDown that exceeds the drag-start threshold.
45
- * When false at onUp we treat the gesture as a click and apply default shape size,
46
- * rather than deleting a zero-sized "pending" shape (the disappearing-shape bug). */
47
- const dragStarted = React.useRef(false);
48
- const startPt = React.useRef({ x: 0, y: 0 });
49
- const pendingShape = React.useRef(null);
50
- const penPts = React.useRef([]);
51
- const panning = React.useRef(false);
52
- const panStart = React.useRef({ mx: 0, my: 0, px: 0, py: 0 });
53
- const hist = React.useRef([]);
54
- const hIdx = React.useRef(-1);
55
- const fInp = React.useRef(null);
56
- const clipboard = React.useRef(null);
57
- const shapesRef = React.useRef([]);
58
- shapesRef.current = shapes;
59
- /* ── derived ── */
60
- const selShape = React.useMemo(() => shapes.find((s) => s.id === selId) || null, [shapes, selId]);
61
- /* ── history ── */
62
- const snap = React.useCallback((s) => {
63
- hist.current = hist.current.slice(0, hIdx.current + 1);
64
- hist.current.push({ shapes: vectorEditorTypes.cloneShapes(s) });
65
- if (hist.current.length > 50) {
66
- hist.current.shift();
67
- hIdx.current--;
68
- }
69
- hIdx.current = hist.current.length - 1;
70
- }, []);
71
- React.useEffect(() => {
72
- if (hist.current.length === 0) {
73
- snap([]);
74
- }
75
- }, [snap]);
76
- const undo = React.useCallback(() => {
77
- if (hIdx.current <= 0) {
78
- return;
79
- }
80
- hIdx.current--;
81
- setShapes(vectorEditorTypes.cloneShapes(hist.current[hIdx.current].shapes));
82
- }, []);
83
- const redo = React.useCallback(() => {
84
- if (hIdx.current >= hist.current.length - 1) {
85
- return;
86
- }
87
- hIdx.current++;
88
- setShapes(vectorEditorTypes.cloneShapes(hist.current[hIdx.current].shapes));
89
- }, []);
90
- /* ── coordinate transform ── */
91
- const scr2art = React.useCallback((cx, cy) => {
92
- var _a;
93
- const r = (_a = boxRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
94
- if (!r) {
95
- return { x: 0, y: 0 };
96
- }
97
- // The <svg> fills the box and its viewBox fits the artboard into it (xMidYMid meet), so one
98
- // art unit is `fit` screen px before the zoom. Dividing by the zoom alone put every shape
99
- // closer to the centre than the pointer — off by half in the 900×520 showcase demo.
100
- const fit = Math.min(r.width / artW, r.height / artH) || 1;
101
- let x = (cx - r.left - r.width / 2 - px) / (zoom * fit) + artW / 2;
102
- let y = (cy - r.top - r.height / 2 - py) / (zoom * fit) + artH / 2;
103
- if (snapGrid) {
104
- x = vectorEditorTypes.snapToGrid(x, gridSize);
105
- y = vectorEditorTypes.snapToGrid(y, gridSize);
106
- }
107
- return { x, y };
108
- }, [px, py, zoom, artW, artH, snapGrid, gridSize]);
109
- /* ── mouse handlers ── */
110
- const onDown = React.useCallback((e) => {
111
- e.preventDefault();
112
- if (e.button === 1) {
113
- panning.current = true;
114
- panStart.current = { mx: e.clientX, my: e.clientY, px, py };
115
- return;
116
- }
117
- if (e.button !== 0) {
118
- return;
119
- }
120
- dragStarted.current = false;
121
- const p = scr2art(e.clientX, e.clientY);
122
- const defaultBase = {
123
- fill: fillCol,
124
- stroke: strokeCol,
125
- strokeWidth: strokeW,
126
- opacity: shapeOpacity,
127
- rotation: 0,
128
- };
129
- if (tool === 'pointer') {
130
- let hit = null;
131
- for (let i = shapes.length - 1; i >= 0; i--) {
132
- const s = shapes[i];
133
- if (s.locked || s.hidden) {
134
- continue;
135
- }
136
- if (p.x >= s.x && p.y >= s.y && p.x <= s.x + s.w && p.y <= s.y + s.h) {
137
- hit = s.id;
138
- break;
139
- }
140
- }
141
- setSelId(hit);
142
- if (hit) {
143
- drawing.current = true;
144
- startPt.current = p;
145
- }
146
- return;
147
- }
148
- if (tool === 'pen') {
149
- penPts.current.push(p);
150
- if (penPts.current.length === 1) {
151
- const ns = {
152
- id: vectorEditorTypes.sid(),
153
- type: 'polyline',
154
- x: p.x,
155
- y: p.y,
156
- w: 0,
157
- h: 0,
158
- points: [{ x: p.x, y: p.y }],
159
- fill: 'none',
160
- stroke: strokeCol,
161
- strokeWidth: strokeW,
162
- opacity: shapeOpacity,
163
- rotation: 0,
164
- };
165
- pendingShape.current = ns;
166
- setShapes((prev) => [...prev, ns]);
167
- }
168
- else {
169
- const pts = [...penPts.current];
170
- const xs = pts.map((pp) => pp.x);
171
- const ys = pts.map((pp) => pp.y);
172
- setShapes((prev) => prev.map((s) => {
173
- var _a;
174
- return s.id === ((_a = pendingShape.current) === null || _a === void 0 ? void 0 : _a.id)
175
- ? {
176
- ...s,
177
- points: pts,
178
- x: Math.min(...xs),
179
- y: Math.min(...ys),
180
- w: Math.max(...xs) - Math.min(...xs),
181
- h: Math.max(...ys) - Math.min(...ys),
182
- }
183
- : s;
184
- }));
185
- }
186
- return;
187
- }
188
- if (tool === 'text') {
189
- // the text comes from a dialog: the shape is added once it is entered
190
- void ask('prompt', 'Enter text:', 'Hello').then((txt) => {
191
- if (!txt) {
192
- return;
193
- }
194
- const ns = {
195
- id: vectorEditorTypes.sid(),
196
- type: 'text',
197
- x: p.x,
198
- y: p.y,
199
- w: txt.length * fontSize * 0.6,
200
- h: fontSize,
201
- ...defaultBase,
202
- stroke: 'none',
203
- strokeWidth: 0,
204
- text: txt,
205
- fontSize,
206
- fontFamily,
207
- };
208
- const next = [...shapes, ns];
209
- snap(next);
210
- setShapes(next);
211
- setSelId(ns.id);
212
- });
213
- return;
214
- }
215
- if (tool === 'polygon') {
216
- const r2 = 40;
217
- const ns = {
218
- id: vectorEditorTypes.sid(),
219
- type: 'polygon',
220
- x: p.x - r2,
221
- y: p.y - r2,
222
- w: r2 * 2,
223
- h: r2 * 2,
224
- ...defaultBase,
225
- sides: polygonSides,
226
- };
227
- drawing.current = true;
228
- startPt.current = p;
229
- pendingShape.current = ns;
230
- setShapes((prev) => [...prev, ns]);
231
- return;
232
- }
233
- if (tool === 'star') {
234
- const r2 = 40;
235
- const ns = {
236
- id: vectorEditorTypes.sid(),
237
- type: 'star',
238
- x: p.x - r2,
239
- y: p.y - r2,
240
- w: r2 * 2,
241
- h: r2 * 2,
242
- ...defaultBase,
243
- sides: starPoints2,
244
- innerRadius: starInnerRatio,
245
- };
246
- drawing.current = true;
247
- startPt.current = p;
248
- pendingShape.current = ns;
249
- setShapes((prev) => [...prev, ns]);
250
- return;
251
- }
252
- // rect / roundrect / ellipse / line / arrow — start drag
253
- drawing.current = true;
254
- startPt.current = p;
255
- const typeMap = {
256
- line: 'line',
257
- arrow: 'arrow',
258
- ellipse: 'ellipse',
259
- roundrect: 'roundrect',
260
- };
261
- const shapeType = typeMap[tool] || 'rect';
262
- const isLineLike = shapeType === 'line' || shapeType === 'arrow';
263
- const ns = {
10
+ const { pendingAsk, ask } = editorAsk.useEditorAsk();
11
+ const [shapes, setShapes] = React.useState([]);
12
+ const [selId, setSelId] = React.useState(null);
13
+ const [tool, setTool] = React.useState("pointer");
14
+ const [fillCol, setFillCol] = React.useState("#5566cc");
15
+ const [strokeCol, setStrokeCol] = React.useState("#000000");
16
+ const [strokeW, setStrokeW] = React.useState(2);
17
+ const [fontSize, setFontSize] = React.useState(24);
18
+ const [fontFamily, setFontFamily] = React.useState("sans-serif");
19
+ const [shapeOpacity, setShapeOpacity] = React.useState(1);
20
+ const [cornerRadius, setCornerRadius] = React.useState(12);
21
+ const [polygonSides, setPolygonSides] = React.useState(6);
22
+ const [starPoints2, setStarPoints2] = React.useState(5);
23
+ const [starInnerRatio, setStarInnerRatio] = React.useState(0.4);
24
+ const [zoom, setZoom] = React.useState(1);
25
+ const [px, setPx] = React.useState(0);
26
+ const [py, setPy] = React.useState(0);
27
+ const [dlgSvg, setDlgSvg] = React.useState(false);
28
+ const [svgText, setSvgText] = React.useState("");
29
+ const [snapGrid, setSnapGrid] = React.useState(false);
30
+ const [gridSize, setGridSize] = React.useState(16);
31
+ const [hasClipboard, setHasClipboard] = React.useState(false);
32
+ const boxRef = React.useRef(null);
33
+ const drawing = React.useRef(false);
34
+ const dragStarted = React.useRef(false);
35
+ const startPt = React.useRef({ x: 0, y: 0 });
36
+ const pendingShape = React.useRef(null);
37
+ const penPts = React.useRef([]);
38
+ const panning = React.useRef(false);
39
+ const panStart = React.useRef({ mx: 0, my: 0, px: 0, py: 0 });
40
+ const hist = React.useRef([]);
41
+ const hIdx = React.useRef(-1);
42
+ const fInp = React.useRef(null);
43
+ const clipboard = React.useRef(null);
44
+ const shapesRef = React.useRef([]);
45
+ shapesRef.current = shapes;
46
+ const selShape = React.useMemo(() => shapes.find((s) => s.id === selId) || null, [shapes, selId]);
47
+ const snap = React.useCallback((s) => {
48
+ hist.current = hist.current.slice(0, hIdx.current + 1);
49
+ hist.current.push({ shapes: vectorEditorTypes.cloneShapes(s) });
50
+ if (hist.current.length > 50) {
51
+ hist.current.shift();
52
+ hIdx.current--;
53
+ }
54
+ hIdx.current = hist.current.length - 1;
55
+ }, []);
56
+ React.useEffect(() => {
57
+ if (hist.current.length === 0) {
58
+ snap([]);
59
+ }
60
+ }, [snap]);
61
+ const undo = React.useCallback(() => {
62
+ if (hIdx.current <= 0) {
63
+ return;
64
+ }
65
+ hIdx.current--;
66
+ setShapes(vectorEditorTypes.cloneShapes(hist.current[hIdx.current].shapes));
67
+ }, []);
68
+ const redo = React.useCallback(() => {
69
+ if (hIdx.current >= hist.current.length - 1) {
70
+ return;
71
+ }
72
+ hIdx.current++;
73
+ setShapes(vectorEditorTypes.cloneShapes(hist.current[hIdx.current].shapes));
74
+ }, []);
75
+ const scr2art = React.useCallback(
76
+ (cx, cy) => {
77
+ const r = boxRef.current?.getBoundingClientRect();
78
+ if (!r) {
79
+ return { x: 0, y: 0 };
80
+ }
81
+ const fit = Math.min(r.width / artW, r.height / artH) || 1;
82
+ let x = (cx - r.left - r.width / 2 - px) / (zoom * fit) + artW / 2;
83
+ let y = (cy - r.top - r.height / 2 - py) / (zoom * fit) + artH / 2;
84
+ if (snapGrid) {
85
+ x = vectorEditorTypes.snapToGrid(x, gridSize);
86
+ y = vectorEditorTypes.snapToGrid(y, gridSize);
87
+ }
88
+ return { x, y };
89
+ },
90
+ [px, py, zoom, artW, artH, snapGrid, gridSize]
91
+ );
92
+ const onDown = React.useCallback(
93
+ (e) => {
94
+ e.preventDefault();
95
+ if (e.button === 1) {
96
+ panning.current = true;
97
+ panStart.current = { mx: e.clientX, my: e.clientY, px, py };
98
+ return;
99
+ }
100
+ if (e.button !== 0) {
101
+ return;
102
+ }
103
+ dragStarted.current = false;
104
+ const p = scr2art(e.clientX, e.clientY);
105
+ const defaultBase = {
106
+ fill: fillCol,
107
+ stroke: strokeCol,
108
+ strokeWidth: strokeW,
109
+ opacity: shapeOpacity,
110
+ rotation: 0
111
+ };
112
+ if (tool === "pointer") {
113
+ let hit = null;
114
+ for (let i = shapes.length - 1; i >= 0; i--) {
115
+ const s = shapes[i];
116
+ if (s.locked || s.hidden) {
117
+ continue;
118
+ }
119
+ if (p.x >= s.x && p.y >= s.y && p.x <= s.x + s.w && p.y <= s.y + s.h) {
120
+ hit = s.id;
121
+ break;
122
+ }
123
+ }
124
+ setSelId(hit);
125
+ if (hit) {
126
+ drawing.current = true;
127
+ startPt.current = p;
128
+ }
129
+ return;
130
+ }
131
+ if (tool === "pen") {
132
+ penPts.current.push(p);
133
+ if (penPts.current.length === 1) {
134
+ const ns2 = {
264
135
  id: vectorEditorTypes.sid(),
265
- type: shapeType,
136
+ type: "polyline",
266
137
  x: p.x,
267
138
  y: p.y,
268
139
  w: 0,
269
140
  h: 0,
270
- x2: p.x,
271
- y2: p.y,
272
- fill: isLineLike ? 'none' : fillCol,
141
+ points: [{ x: p.x, y: p.y }],
142
+ fill: "none",
273
143
  stroke: strokeCol,
274
144
  strokeWidth: strokeW,
275
145
  opacity: shapeOpacity,
276
- rotation: 0,
277
- rx: shapeType === 'roundrect' ? cornerRadius : undefined,
278
- };
279
- pendingShape.current = ns;
280
- setShapes((prev) => [...prev, ns]);
281
- }, [
282
- tool,
283
- shapes,
284
- scr2art,
285
- px,
286
- py,
287
- fillCol,
288
- strokeCol,
289
- strokeW,
290
- fontSize,
291
- fontFamily,
292
- snap,
293
- shapeOpacity,
294
- polygonSides,
295
- starPoints2,
296
- starInnerRatio,
297
- cornerRadius,
298
- ask,
299
- ]);
300
- const onMove = React.useCallback((e) => {
301
- if (panning.current) {
302
- setPx(panStart.current.px + (e.clientX - panStart.current.mx));
303
- setPy(panStart.current.py + (e.clientY - panStart.current.my));
304
- return;
305
- }
306
- if (!drawing.current) {
307
- return;
308
- }
309
- const p = scr2art(e.clientX, e.clientY);
310
- // Treat the gesture as a drag only once movement exceeds the threshold —
311
- // protects polygon/star/rect/ellipse from being shrunk to zero by the very
312
- // first mousemove event after a click that landed on the same screen pixel.
313
- if (!dragStarted.current) {
314
- const dxs = p.x - startPt.current.x;
315
- const dys = p.y - startPt.current.y;
316
- if (Math.hypot(dxs, dys) < 2) {
317
- return;
318
- }
319
- dragStarted.current = true;
320
- }
321
- if (tool === 'pointer' && selId) {
322
- const dx = p.x - startPt.current.x;
323
- const dy = p.y - startPt.current.y;
324
- startPt.current = p;
325
- setShapes((prev) => prev.map((s) => {
326
- var _a;
327
- return s.id === selId
328
- ? {
329
- ...s,
330
- x: s.x + dx,
331
- y: s.y + dy,
332
- x2: s.x2 != null ? s.x2 + dx : undefined,
333
- y2: s.y2 != null ? s.y2 + dy : undefined,
334
- points: (_a = s.points) === null || _a === void 0 ? void 0 : _a.map((pt) => ({ x: pt.x + dx, y: pt.y + dy })),
335
- }
336
- : s;
337
- }));
338
- return;
339
- }
340
- const pending = pendingShape.current;
341
- if (!pending) {
342
- return;
343
- }
344
- // The pending shape's ref follows the drag as well. onUp sizes the result from the ref,
345
- // which kept the 0×0 shape of the press — so every dragged rectangle, ellipse, line and
346
- // arrow counted as "tiny" and was deleted on release: nothing appeared on the canvas.
347
- const resize = (patch) => {
348
- pendingShape.current = { ...pending, ...patch };
349
- setShapes((prev) => prev.map((s) => (s.id === pending.id ? { ...s, ...patch } : s)));
350
- };
351
- const t = pending.type;
352
- if (t === 'polygon' || t === 'star') {
353
- const cx = startPt.current.x;
354
- const cy = startPt.current.y;
355
- const dx = p.x - cx;
356
- const dy = p.y - cy;
357
- const r2 = Math.sqrt(dx * dx + dy * dy);
358
- resize({ x: cx - r2, y: cy - r2, w: r2 * 2, h: r2 * 2 });
359
- return;
360
- }
361
- if (t === 'rect' || t === 'roundrect' || t === 'ellipse') {
362
- resize({
363
- x: Math.min(startPt.current.x, p.x),
364
- y: Math.min(startPt.current.y, p.y),
365
- w: Math.abs(p.x - startPt.current.x),
366
- h: Math.abs(p.y - startPt.current.y),
367
- });
368
- }
369
- else if (t === 'line' || t === 'arrow') {
370
- resize({ w: Math.abs(p.x - pending.x), h: Math.abs(p.y - pending.y), x2: p.x, y2: p.y });
371
- }
372
- }, [tool, scr2art, selId]);
373
- const onUp = React.useCallback(() => {
374
- var _a, _b;
375
- if (panning.current) {
376
- panning.current = false;
377
- return;
378
- }
379
- if (drawing.current) {
380
- drawing.current = false;
381
- if (pendingShape.current) {
382
- const ps = pendingShape.current;
383
- // Click without drag → apply a sensible default size so the user actually
384
- // sees the shape they just placed (the disappearing-shape fix).
385
- if (!dragStarted.current && ps.type !== 'polyline' && ps.type !== 'text') {
386
- const cx = startPt.current.x;
387
- const cy = startPt.current.y;
388
- let patch;
389
- if (ps.type === 'line' || ps.type === 'arrow') {
390
- patch = { x: cx, y: cy, w: 80, h: 0, x2: cx + 80, y2: cy };
391
- }
392
- else if (ps.type === 'polygon' || ps.type === 'star') {
393
- const r = 40;
394
- patch = { x: cx - r, y: cy - r, w: r * 2, h: r * 2 };
395
- }
396
- else {
397
- // rect / roundrect / ellipse — 64×64 centered on click point
398
- const s = 64;
399
- patch = { x: cx - s / 2, y: cy - s / 2, w: s, h: s };
400
- }
401
- const psId = ps.id;
402
- setShapes((prev) => prev.map((s) => (s.id === psId ? { ...s, ...patch } : s)));
403
- snap(shapesRef.current.map((s) => (s.id === psId ? { ...s, ...patch } : s)));
404
- setSelId(psId);
405
- pendingShape.current = null;
406
- dragStarted.current = false;
407
- return;
408
- }
409
- // Real drag — still guard against zero-sized result (e.g. snap-to-grid collisions)
410
- const minD = 3;
411
- const isTiny = ps.type === 'line' || ps.type === 'arrow'
412
- ? Math.abs(((_a = ps.x2) !== null && _a !== void 0 ? _a : ps.x) - ps.x) < minD && Math.abs(((_b = ps.y2) !== null && _b !== void 0 ? _b : ps.y) - ps.y) < minD
413
- : Math.abs(ps.w) < minD && Math.abs(ps.h) < minD;
414
- if (isTiny) {
415
- setShapes((prev) => prev.filter((s) => s.id !== ps.id));
416
- pendingShape.current = null;
417
- dragStarted.current = false;
418
- return;
419
- }
420
- // the ref holds the final geometry even when the last move has not rendered yet
421
- snap(shapesRef.current.map((s) => (s.id === ps.id ? ps : s)));
422
- setSelId(ps.id);
423
- pendingShape.current = null;
424
- dragStarted.current = false;
425
- }
426
- }
427
- }, [snap]);
428
- const onWheel = React.useCallback((e) => {
429
- setZoom((z) => {
430
- const n = e.deltaY < 0 ? z * 1.15 : z / 1.15;
431
- return Math.max(0.1, Math.min(10, n));
432
- });
433
- }, []);
434
- React.useEffect(() => {
435
- const el = boxRef.current;
436
- if (!el) {
437
- return;
438
- }
439
- const handler = (e) => e.preventDefault();
440
- el.addEventListener('wheel', handler, { passive: false });
441
- return () => el.removeEventListener('wheel', handler);
442
- }, []);
443
- const onDblClick = React.useCallback(() => {
444
- if (tool === 'pen' && pendingShape.current) {
445
- snap(shapesRef.current);
446
- setSelId(pendingShape.current.id);
447
- pendingShape.current = null;
448
- penPts.current = [];
449
- }
450
- }, [tool, snap]);
451
- /* ── shape operations ── */
452
- const deleteShape = React.useCallback(() => {
453
- if (!selId) {
454
- return;
455
- }
456
- const next = shapes.filter((s) => s.id !== selId);
457
- snap(next);
458
- setShapes(next);
459
- setSelId(null);
460
- }, [selId, shapes, snap]);
461
- const bringToFront = React.useCallback(() => {
462
- if (!selId) {
463
- return;
464
- }
465
- const next = [...shapes.filter((s) => s.id !== selId), shapes.find((s) => s.id === selId)];
466
- snap(next);
467
- setShapes(next);
468
- }, [selId, shapes, snap]);
469
- const sendToBack = React.useCallback(() => {
470
- if (!selId) {
471
- return;
472
- }
473
- const next = [shapes.find((s) => s.id === selId), ...shapes.filter((s) => s.id !== selId)];
474
- snap(next);
475
- setShapes(next);
476
- }, [selId, shapes, snap]);
477
- const updateSel = React.useCallback((patch) => {
478
- if (!selId) {
479
- return;
480
- }
481
- setShapes((prev) => prev.map((s) => (s.id === selId ? { ...s, ...patch } : s)));
482
- }, [selId]);
483
- /* ── clipboard ── */
484
- const copyShape = React.useCallback(() => {
485
- if (!selShape) {
486
- return;
487
- }
488
- clipboard.current = JSON.parse(JSON.stringify(selShape));
489
- setHasClipboard(true);
490
- }, [selShape]);
491
- const pasteShape = React.useCallback(() => {
492
- if (!clipboard.current) {
493
- return;
494
- }
495
- const ns = {
496
- ...JSON.parse(JSON.stringify(clipboard.current)),
146
+ rotation: 0
147
+ };
148
+ pendingShape.current = ns2;
149
+ setShapes((prev) => [...prev, ns2]);
150
+ } else {
151
+ const pts = [...penPts.current];
152
+ const xs = pts.map((pp) => pp.x);
153
+ const ys = pts.map((pp) => pp.y);
154
+ setShapes(
155
+ (prev) => prev.map(
156
+ (s) => s.id === pendingShape.current?.id ? {
157
+ ...s,
158
+ points: pts,
159
+ x: Math.min(...xs),
160
+ y: Math.min(...ys),
161
+ w: Math.max(...xs) - Math.min(...xs),
162
+ h: Math.max(...ys) - Math.min(...ys)
163
+ } : s
164
+ )
165
+ );
166
+ }
167
+ return;
168
+ }
169
+ if (tool === "text") {
170
+ void ask("prompt", "Enter text:", "Hello").then((txt) => {
171
+ if (!txt) {
172
+ return;
173
+ }
174
+ const ns2 = {
497
175
  id: vectorEditorTypes.sid(),
498
- x: clipboard.current.x + 20,
499
- y: clipboard.current.y + 20,
176
+ type: "text",
177
+ x: p.x,
178
+ y: p.y,
179
+ w: txt.length * fontSize * 0.6,
180
+ h: fontSize,
181
+ ...defaultBase,
182
+ stroke: "none",
183
+ strokeWidth: 0,
184
+ text: txt,
185
+ fontSize,
186
+ fontFamily
187
+ };
188
+ const next = [...shapes, ns2];
189
+ snap(next);
190
+ setShapes(next);
191
+ setSelId(ns2.id);
192
+ });
193
+ return;
194
+ }
195
+ if (tool === "polygon") {
196
+ const r2 = 40;
197
+ const ns2 = {
198
+ id: vectorEditorTypes.sid(),
199
+ type: "polygon",
200
+ x: p.x - r2,
201
+ y: p.y - r2,
202
+ w: r2 * 2,
203
+ h: r2 * 2,
204
+ ...defaultBase,
205
+ sides: polygonSides
500
206
  };
501
- const next = [...shapes, ns];
502
- snap(next);
503
- setShapes(next);
504
- setSelId(ns.id);
505
- }, [shapes, snap]);
506
- const duplicateShape = React.useCallback(() => {
507
- if (!selShape) {
508
- return;
509
- }
510
- const ns = {
511
- ...JSON.parse(JSON.stringify(selShape)),
512
- id: vectorEditorTypes.sid(),
513
- x: selShape.x + 20,
514
- y: selShape.y + 20,
207
+ drawing.current = true;
208
+ startPt.current = p;
209
+ pendingShape.current = ns2;
210
+ setShapes((prev) => [...prev, ns2]);
211
+ return;
212
+ }
213
+ if (tool === "star") {
214
+ const r2 = 40;
215
+ const ns2 = {
216
+ id: vectorEditorTypes.sid(),
217
+ type: "star",
218
+ x: p.x - r2,
219
+ y: p.y - r2,
220
+ w: r2 * 2,
221
+ h: r2 * 2,
222
+ ...defaultBase,
223
+ sides: starPoints2,
224
+ innerRadius: starInnerRatio
515
225
  };
516
- const next = [...shapes, ns];
517
- snap(next);
518
- setShapes(next);
519
- setSelId(ns.id);
520
- }, [selShape, shapes, snap]);
521
- /* ── flip ── */
522
- const flipH = React.useCallback(() => {
523
- if (!selId || !selShape) {
524
- return;
525
- }
526
- if (selShape.points) {
527
- const cx = selShape.x + selShape.w / 2;
528
- const next = shapes.map((s) => {
529
- var _a;
530
- return s.id === selId
531
- ? { ...s, points: (_a = s.points) === null || _a === void 0 ? void 0 : _a.map((pt) => ({ x: cx * 2 - pt.x, y: pt.y })) }
532
- : s;
533
- });
534
- snap(next);
535
- setShapes(next);
536
- }
537
- else if (selShape.type === 'line' || selShape.type === 'arrow') {
538
- const next = shapes.map((s) => { var _a; return (s.id === selId ? { ...s, x: (_a = s.x2) !== null && _a !== void 0 ? _a : s.x, x2: s.x } : s); });
539
- snap(next);
540
- setShapes(next);
541
- }
542
- else {
543
- const newRot = selShape.rotation ? -selShape.rotation : 0;
544
- const next = shapes.map((s) => (s.id === selId ? { ...s, rotation: newRot } : s));
545
- snap(next);
546
- setShapes(next);
547
- }
548
- }, [selId, selShape, shapes, snap]);
549
- const flipV = React.useCallback(() => {
550
- if (!selId || !selShape) {
551
- return;
552
- }
553
- if (selShape.points) {
554
- const cy = selShape.y + selShape.h / 2;
555
- const next = shapes.map((s) => {
556
- var _a;
557
- return s.id === selId
558
- ? { ...s, points: (_a = s.points) === null || _a === void 0 ? void 0 : _a.map((pt) => ({ x: pt.x, y: cy * 2 - pt.y })) }
559
- : s;
560
- });
561
- snap(next);
562
- setShapes(next);
563
- }
564
- else if (selShape.type === 'line' || selShape.type === 'arrow') {
565
- const next = shapes.map((s) => { var _a; return (s.id === selId ? { ...s, y: (_a = s.y2) !== null && _a !== void 0 ? _a : s.y, y2: s.y } : s); });
566
- snap(next);
567
- setShapes(next);
568
- }
569
- else {
570
- const newRot = selShape.rotation ? (180 - selShape.rotation) % 360 : 180;
571
- const next = shapes.map((s) => (s.id === selId ? { ...s, rotation: newRot } : s));
572
- snap(next);
573
- setShapes(next);
574
- }
575
- }, [selId, selShape, shapes, snap]);
576
- /* ── alignment ── */
577
- const alignShapes = React.useCallback((mode) => {
578
- if (!selId || !selShape) {
579
- return;
580
- }
581
- let patch = {};
582
- switch (mode) {
583
- case 'left':
584
- patch = { x: 0 };
585
- break;
586
- case 'right':
587
- patch = { x: artW - selShape.w };
588
- break;
589
- case 'center':
590
- patch = { x: (artW - selShape.w) / 2 };
591
- break;
592
- case 'top':
593
- patch = { y: 0 };
594
- break;
595
- case 'bottom':
596
- patch = { y: artH - selShape.h };
597
- break;
598
- case 'middle':
599
- patch = { y: (artH - selShape.h) / 2 };
600
- break;
601
- }
602
- const next = shapes.map((s) => (s.id === selId ? { ...s, ...patch } : s));
603
- snap(next);
604
- setShapes(next);
605
- }, [selId, selShape, shapes, snap, artW, artH]);
606
- /* ── animations (per shape id) ── */
607
- const [animations, setAnimations] = React.useState({});
608
- const [isPlaying, setIsPlaying] = React.useState(false);
609
- const [previewTime, setPreviewTime] = React.useState(0);
610
- const setShapeAnimation = React.useCallback((shapeId, anim) => {
611
- setAnimations((prev) => {
612
- const next = { ...prev };
613
- if (anim) {
614
- next[shapeId] = anim;
615
- }
616
- else {
617
- delete next[shapeId];
618
- }
619
- return next;
226
+ drawing.current = true;
227
+ startPt.current = p;
228
+ pendingShape.current = ns2;
229
+ setShapes((prev) => [...prev, ns2]);
230
+ return;
231
+ }
232
+ drawing.current = true;
233
+ startPt.current = p;
234
+ const typeMap = {
235
+ line: "line",
236
+ arrow: "arrow",
237
+ ellipse: "ellipse",
238
+ roundrect: "roundrect"
239
+ };
240
+ const shapeType = typeMap[tool] || "rect";
241
+ const isLineLike = shapeType === "line" || shapeType === "arrow";
242
+ const ns = {
243
+ id: vectorEditorTypes.sid(),
244
+ type: shapeType,
245
+ x: p.x,
246
+ y: p.y,
247
+ w: 0,
248
+ h: 0,
249
+ x2: p.x,
250
+ y2: p.y,
251
+ fill: isLineLike ? "none" : fillCol,
252
+ stroke: strokeCol,
253
+ strokeWidth: strokeW,
254
+ opacity: shapeOpacity,
255
+ rotation: 0,
256
+ rx: shapeType === "roundrect" ? cornerRadius : void 0
257
+ };
258
+ pendingShape.current = ns;
259
+ setShapes((prev) => [...prev, ns]);
260
+ },
261
+ [
262
+ tool,
263
+ shapes,
264
+ scr2art,
265
+ px,
266
+ py,
267
+ fillCol,
268
+ strokeCol,
269
+ strokeW,
270
+ fontSize,
271
+ fontFamily,
272
+ snap,
273
+ shapeOpacity,
274
+ polygonSides,
275
+ starPoints2,
276
+ starInnerRatio,
277
+ cornerRadius,
278
+ ask
279
+ ]
280
+ );
281
+ const onMove = React.useCallback(
282
+ (e) => {
283
+ if (panning.current) {
284
+ setPx(panStart.current.px + (e.clientX - panStart.current.mx));
285
+ setPy(panStart.current.py + (e.clientY - panStart.current.my));
286
+ return;
287
+ }
288
+ if (!drawing.current) {
289
+ return;
290
+ }
291
+ const p = scr2art(e.clientX, e.clientY);
292
+ if (!dragStarted.current) {
293
+ const dxs = p.x - startPt.current.x;
294
+ const dys = p.y - startPt.current.y;
295
+ if (Math.hypot(dxs, dys) < 2) {
296
+ return;
297
+ }
298
+ dragStarted.current = true;
299
+ }
300
+ if (tool === "pointer" && selId) {
301
+ const dx = p.x - startPt.current.x;
302
+ const dy = p.y - startPt.current.y;
303
+ startPt.current = p;
304
+ setShapes(
305
+ (prev) => prev.map(
306
+ (s) => s.id === selId ? {
307
+ ...s,
308
+ x: s.x + dx,
309
+ y: s.y + dy,
310
+ x2: s.x2 != null ? s.x2 + dx : void 0,
311
+ y2: s.y2 != null ? s.y2 + dy : void 0,
312
+ points: s.points?.map((pt) => ({ x: pt.x + dx, y: pt.y + dy }))
313
+ } : s
314
+ )
315
+ );
316
+ return;
317
+ }
318
+ const pending = pendingShape.current;
319
+ if (!pending) {
320
+ return;
321
+ }
322
+ const resize = (patch) => {
323
+ pendingShape.current = { ...pending, ...patch };
324
+ setShapes((prev) => prev.map((s) => s.id === pending.id ? { ...s, ...patch } : s));
325
+ };
326
+ const t = pending.type;
327
+ if (t === "polygon" || t === "star") {
328
+ const cx = startPt.current.x;
329
+ const cy = startPt.current.y;
330
+ const dx = p.x - cx;
331
+ const dy = p.y - cy;
332
+ const r2 = Math.sqrt(dx * dx + dy * dy);
333
+ resize({ x: cx - r2, y: cy - r2, w: r2 * 2, h: r2 * 2 });
334
+ return;
335
+ }
336
+ if (t === "rect" || t === "roundrect" || t === "ellipse") {
337
+ resize({
338
+ x: Math.min(startPt.current.x, p.x),
339
+ y: Math.min(startPt.current.y, p.y),
340
+ w: Math.abs(p.x - startPt.current.x),
341
+ h: Math.abs(p.y - startPt.current.y)
620
342
  });
621
- }, []);
622
- React.useEffect(() => {
623
- if (!isPlaying) {
624
- return;
625
- }
626
- let raf = 0;
627
- const start = performance.now() - previewTime;
628
- const tick = (now) => {
629
- setPreviewTime(now - start);
630
- raf = requestAnimationFrame(tick);
631
- };
632
- raf = requestAnimationFrame(tick);
633
- return () => cancelAnimationFrame(raf);
634
- // eslint-disable-next-line react-hooks/exhaustive-deps
635
- }, [isPlaying]);
636
- /* ── boolean operations ── */
637
- const [boolSecondShape, setBoolSecondShape] = React.useState(null);
638
- const applyBoolean = React.useCallback((operation) => {
639
- var _a;
640
- if (!selId || !boolSecondShape) {
641
- return;
642
- }
643
- const shapeA = shapes.find((s) => s.id === selId);
644
- const shapeB = shapes.find((s) => s.id === boolSecondShape);
645
- if (!shapeA || !shapeB) {
646
- return;
647
- }
648
- const result = vectorBooleanOps.booleanOperation(shapeA, shapeB, operation);
649
- if (!result) {
650
- console.warn('[VectorEditor] Boolean operation resulted in empty shape');
651
- return;
652
- }
653
- // Remove original shapes, add result(s)
654
- let next = shapes.filter((s) => s.id !== selId && s.id !== boolSecondShape);
655
- if (Array.isArray(result)) {
656
- next = [...next, ...result];
657
- setSelId(((_a = result[0]) === null || _a === void 0 ? void 0 : _a.id) || null);
658
- }
659
- else {
660
- next = [...next, result];
661
- setSelId(result.id);
662
- }
663
- snap(next);
664
- setShapes(next);
665
- setBoolSecondShape(null);
666
- }, [selId, boolSecondShape, shapes, snap]);
667
- const unionShapes = React.useCallback(() => applyBoolean('union'), [applyBoolean]);
668
- const subtractShapes = React.useCallback(() => applyBoolean('difference'), [applyBoolean]);
669
- const intersectShapes = React.useCallback(() => applyBoolean('intersection'), [applyBoolean]);
670
- const excludeShapes = React.useCallback(() => applyBoolean('exclusion'), [applyBoolean]);
671
- const outlineStroke = React.useCallback(() => {
672
- if (!selId || !selShape) {
673
- return;
674
- }
675
- const result = vectorBooleanOps.flattenToOutline(selShape);
676
- const next = shapes.map((s) => (s.id === selId ? result : s));
343
+ } else if (t === "line" || t === "arrow") {
344
+ resize({ w: Math.abs(p.x - pending.x), h: Math.abs(p.y - pending.y), x2: p.x, y2: p.y });
345
+ }
346
+ },
347
+ [tool, scr2art, selId]
348
+ );
349
+ const onUp = React.useCallback(() => {
350
+ if (panning.current) {
351
+ panning.current = false;
352
+ return;
353
+ }
354
+ if (drawing.current) {
355
+ drawing.current = false;
356
+ if (pendingShape.current) {
357
+ const ps = pendingShape.current;
358
+ if (!dragStarted.current && ps.type !== "polyline" && ps.type !== "text") {
359
+ const cx = startPt.current.x;
360
+ const cy = startPt.current.y;
361
+ let patch;
362
+ if (ps.type === "line" || ps.type === "arrow") {
363
+ patch = { x: cx, y: cy, w: 80, h: 0, x2: cx + 80, y2: cy };
364
+ } else if (ps.type === "polygon" || ps.type === "star") {
365
+ const r = 40;
366
+ patch = { x: cx - r, y: cy - r, w: r * 2, h: r * 2 };
367
+ } else {
368
+ const s = 64;
369
+ patch = { x: cx - s / 2, y: cy - s / 2, w: s, h: s };
370
+ }
371
+ const psId = ps.id;
372
+ setShapes((prev) => prev.map((s) => s.id === psId ? { ...s, ...patch } : s));
373
+ snap(shapesRef.current.map((s) => s.id === psId ? { ...s, ...patch } : s));
374
+ setSelId(psId);
375
+ pendingShape.current = null;
376
+ dragStarted.current = false;
377
+ return;
378
+ }
379
+ const minD = 3;
380
+ const isTiny = ps.type === "line" || ps.type === "arrow" ? Math.abs((ps.x2 ?? ps.x) - ps.x) < minD && Math.abs((ps.y2 ?? ps.y) - ps.y) < minD : Math.abs(ps.w) < minD && Math.abs(ps.h) < minD;
381
+ if (isTiny) {
382
+ setShapes((prev) => prev.filter((s) => s.id !== ps.id));
383
+ pendingShape.current = null;
384
+ dragStarted.current = false;
385
+ return;
386
+ }
387
+ snap(shapesRef.current.map((s) => s.id === ps.id ? ps : s));
388
+ setSelId(ps.id);
389
+ pendingShape.current = null;
390
+ dragStarted.current = false;
391
+ }
392
+ }
393
+ }, [snap]);
394
+ const onWheel = React.useCallback((e) => {
395
+ setZoom((z) => {
396
+ const n = e.deltaY < 0 ? z * 1.15 : z / 1.15;
397
+ return Math.max(0.1, Math.min(10, n));
398
+ });
399
+ }, []);
400
+ React.useEffect(() => {
401
+ const el = boxRef.current;
402
+ if (!el) {
403
+ return;
404
+ }
405
+ const handler = (e) => e.preventDefault();
406
+ el.addEventListener("wheel", handler, { passive: false });
407
+ return () => el.removeEventListener("wheel", handler);
408
+ }, []);
409
+ const onDblClick = React.useCallback(() => {
410
+ if (tool === "pen" && pendingShape.current) {
411
+ snap(shapesRef.current);
412
+ setSelId(pendingShape.current.id);
413
+ pendingShape.current = null;
414
+ penPts.current = [];
415
+ }
416
+ }, [tool, snap]);
417
+ const deleteShape = React.useCallback(() => {
418
+ if (!selId) {
419
+ return;
420
+ }
421
+ const next = shapes.filter((s) => s.id !== selId);
422
+ snap(next);
423
+ setShapes(next);
424
+ setSelId(null);
425
+ }, [selId, shapes, snap]);
426
+ const bringToFront = React.useCallback(() => {
427
+ if (!selId) {
428
+ return;
429
+ }
430
+ const next = [...shapes.filter((s) => s.id !== selId), shapes.find((s) => s.id === selId)];
431
+ snap(next);
432
+ setShapes(next);
433
+ }, [selId, shapes, snap]);
434
+ const sendToBack = React.useCallback(() => {
435
+ if (!selId) {
436
+ return;
437
+ }
438
+ const next = [shapes.find((s) => s.id === selId), ...shapes.filter((s) => s.id !== selId)];
439
+ snap(next);
440
+ setShapes(next);
441
+ }, [selId, shapes, snap]);
442
+ const updateSel = React.useCallback(
443
+ (patch) => {
444
+ if (!selId) {
445
+ return;
446
+ }
447
+ setShapes((prev) => prev.map((s) => s.id === selId ? { ...s, ...patch } : s));
448
+ },
449
+ [selId]
450
+ );
451
+ const copyShape = React.useCallback(() => {
452
+ if (!selShape) {
453
+ return;
454
+ }
455
+ clipboard.current = JSON.parse(JSON.stringify(selShape));
456
+ setHasClipboard(true);
457
+ }, [selShape]);
458
+ const pasteShape = React.useCallback(() => {
459
+ if (!clipboard.current) {
460
+ return;
461
+ }
462
+ const ns = {
463
+ ...JSON.parse(JSON.stringify(clipboard.current)),
464
+ id: vectorEditorTypes.sid(),
465
+ x: clipboard.current.x + 20,
466
+ y: clipboard.current.y + 20
467
+ };
468
+ const next = [...shapes, ns];
469
+ snap(next);
470
+ setShapes(next);
471
+ setSelId(ns.id);
472
+ }, [shapes, snap]);
473
+ const duplicateShape = React.useCallback(() => {
474
+ if (!selShape) {
475
+ return;
476
+ }
477
+ const ns = {
478
+ ...JSON.parse(JSON.stringify(selShape)),
479
+ id: vectorEditorTypes.sid(),
480
+ x: selShape.x + 20,
481
+ y: selShape.y + 20
482
+ };
483
+ const next = [...shapes, ns];
484
+ snap(next);
485
+ setShapes(next);
486
+ setSelId(ns.id);
487
+ }, [selShape, shapes, snap]);
488
+ const flipH = React.useCallback(() => {
489
+ if (!selId || !selShape) {
490
+ return;
491
+ }
492
+ if (selShape.points) {
493
+ const cx = selShape.x + selShape.w / 2;
494
+ const next = shapes.map(
495
+ (s) => s.id === selId ? { ...s, points: s.points?.map((pt) => ({ x: cx * 2 - pt.x, y: pt.y })) } : s
496
+ );
497
+ snap(next);
498
+ setShapes(next);
499
+ } else if (selShape.type === "line" || selShape.type === "arrow") {
500
+ const next = shapes.map((s) => s.id === selId ? { ...s, x: s.x2 ?? s.x, x2: s.x } : s);
501
+ snap(next);
502
+ setShapes(next);
503
+ } else {
504
+ const newRot = selShape.rotation ? -selShape.rotation : 0;
505
+ const next = shapes.map((s) => s.id === selId ? { ...s, rotation: newRot } : s);
506
+ snap(next);
507
+ setShapes(next);
508
+ }
509
+ }, [selId, selShape, shapes, snap]);
510
+ const flipV = React.useCallback(() => {
511
+ if (!selId || !selShape) {
512
+ return;
513
+ }
514
+ if (selShape.points) {
515
+ const cy = selShape.y + selShape.h / 2;
516
+ const next = shapes.map(
517
+ (s) => s.id === selId ? { ...s, points: s.points?.map((pt) => ({ x: pt.x, y: cy * 2 - pt.y })) } : s
518
+ );
519
+ snap(next);
520
+ setShapes(next);
521
+ } else if (selShape.type === "line" || selShape.type === "arrow") {
522
+ const next = shapes.map((s) => s.id === selId ? { ...s, y: s.y2 ?? s.y, y2: s.y } : s);
523
+ snap(next);
524
+ setShapes(next);
525
+ } else {
526
+ const newRot = selShape.rotation ? (180 - selShape.rotation) % 360 : 180;
527
+ const next = shapes.map((s) => s.id === selId ? { ...s, rotation: newRot } : s);
528
+ snap(next);
529
+ setShapes(next);
530
+ }
531
+ }, [selId, selShape, shapes, snap]);
532
+ const alignShapes = React.useCallback(
533
+ (mode) => {
534
+ if (!selId || !selShape) {
535
+ return;
536
+ }
537
+ let patch = {};
538
+ switch (mode) {
539
+ case "left":
540
+ patch = { x: 0 };
541
+ break;
542
+ case "right":
543
+ patch = { x: artW - selShape.w };
544
+ break;
545
+ case "center":
546
+ patch = { x: (artW - selShape.w) / 2 };
547
+ break;
548
+ case "top":
549
+ patch = { y: 0 };
550
+ break;
551
+ case "bottom":
552
+ patch = { y: artH - selShape.h };
553
+ break;
554
+ case "middle":
555
+ patch = { y: (artH - selShape.h) / 2 };
556
+ break;
557
+ }
558
+ const next = shapes.map((s) => s.id === selId ? { ...s, ...patch } : s);
559
+ snap(next);
560
+ setShapes(next);
561
+ },
562
+ [selId, selShape, shapes, snap, artW, artH]
563
+ );
564
+ const [animations, setAnimations] = React.useState({});
565
+ const [isPlaying, setIsPlaying] = React.useState(false);
566
+ const [previewTime, setPreviewTime] = React.useState(0);
567
+ const setShapeAnimation = React.useCallback((shapeId, anim) => {
568
+ setAnimations((prev) => {
569
+ const next = { ...prev };
570
+ if (anim) {
571
+ next[shapeId] = anim;
572
+ } else {
573
+ delete next[shapeId];
574
+ }
575
+ return next;
576
+ });
577
+ }, []);
578
+ React.useEffect(() => {
579
+ if (!isPlaying) {
580
+ return;
581
+ }
582
+ let raf = 0;
583
+ const start = performance.now() - previewTime;
584
+ const tick = (now) => {
585
+ setPreviewTime(now - start);
586
+ raf = requestAnimationFrame(tick);
587
+ };
588
+ raf = requestAnimationFrame(tick);
589
+ return () => cancelAnimationFrame(raf);
590
+ }, [isPlaying]);
591
+ const [boolSecondShape, setBoolSecondShape] = React.useState(null);
592
+ const applyBoolean = React.useCallback(
593
+ (operation) => {
594
+ if (!selId || !boolSecondShape) {
595
+ return;
596
+ }
597
+ const shapeA = shapes.find((s) => s.id === selId);
598
+ const shapeB = shapes.find((s) => s.id === boolSecondShape);
599
+ if (!shapeA || !shapeB) {
600
+ return;
601
+ }
602
+ const result = vectorBooleanOps.booleanOperation(shapeA, shapeB, operation);
603
+ if (!result) {
604
+ console.warn("[VectorEditor] Boolean operation resulted in empty shape");
605
+ return;
606
+ }
607
+ let next = shapes.filter((s) => s.id !== selId && s.id !== boolSecondShape);
608
+ if (Array.isArray(result)) {
609
+ next = [...next, ...result];
610
+ setSelId(result[0]?.id || null);
611
+ } else {
612
+ next = [...next, result];
613
+ setSelId(result.id);
614
+ }
615
+ snap(next);
616
+ setShapes(next);
617
+ setBoolSecondShape(null);
618
+ },
619
+ [selId, boolSecondShape, shapes, snap]
620
+ );
621
+ const unionShapes = React.useCallback(() => applyBoolean("union"), [applyBoolean]);
622
+ const subtractShapes = React.useCallback(() => applyBoolean("difference"), [applyBoolean]);
623
+ const intersectShapes = React.useCallback(() => applyBoolean("intersection"), [applyBoolean]);
624
+ const excludeShapes = React.useCallback(() => applyBoolean("exclusion"), [applyBoolean]);
625
+ const outlineStroke = React.useCallback(() => {
626
+ if (!selId || !selShape) {
627
+ return;
628
+ }
629
+ const result = vectorBooleanOps.flattenToOutline(selShape);
630
+ const next = shapes.map((s) => s.id === selId ? result : s);
631
+ snap(next);
632
+ setShapes(next);
633
+ setSelId(result.id);
634
+ }, [selId, selShape, shapes, snap]);
635
+ const importSVGString = React.useCallback(
636
+ (raw) => {
637
+ try {
638
+ const imported = vectorEditorImport.parseSVGToShapes(raw);
639
+ if (imported.length === 0) {
640
+ console.warn("[VectorEditor] No supported shapes found in SVG.");
641
+ return;
642
+ }
643
+ const next = [...shapes, ...imported];
677
644
  snap(next);
678
645
  setShapes(next);
679
- setSelId(result.id);
680
- }, [selId, selShape, shapes, snap]);
681
- /* ── SVG import ── */
682
- const importSVGString = React.useCallback((raw) => {
683
- try {
684
- const imported = vectorEditorImport.parseSVGToShapes(raw);
685
- if (imported.length === 0) {
686
- console.warn('[VectorEditor] No supported shapes found in SVG.');
687
- return;
688
- }
689
- const next = [...shapes, ...imported];
690
- snap(next);
691
- setShapes(next);
692
- setDlgSvg(false);
693
- }
694
- catch (e) {
695
- console.warn('[VectorEditor] SVG parse error:', e instanceof Error ? e.message : 'Failed to parse SVG.');
696
- }
697
- }, [shapes, snap]);
698
- const openFile = React.useCallback(async (file) => {
699
- importSVGString(await file.text());
700
- }, [importSVGString]);
701
- /* ── new / reset ── */
702
- const newCanvas = React.useCallback(() => {
703
- snap([]);
704
- setShapes([]);
705
- setSelId(null);
706
- setZoom(1);
707
- setPx(0);
708
- setPy(0);
709
- }, [snap]);
710
- /* ── save to library ── */
711
- const saveToLibrary = React.useCallback(() => {
712
- if (!onSaveToLibrary) {
713
- return;
714
- }
715
- const svg = vectorEditorExport.buildSVGString(shapes, artW, artH);
716
- const blob = new Blob([svg], { type: 'image/svg+xml' });
717
- const reader = new FileReader();
718
- reader.onload = () => {
719
- const dataUrl = reader.result;
720
- void ask('prompt', 'Save to library as:', 'drawing.svg').then((name) => {
721
- if (name && name.trim()) {
722
- onSaveToLibrary(dataUrl, name.trim(), 'image/svg+xml');
723
- }
724
- });
725
- };
726
- reader.readAsDataURL(blob);
727
- }, [shapes, artW, artH, onSaveToLibrary, ask]);
728
- /* ── export shortcuts ── */
729
- const exportSVG = React.useCallback(() => vectorEditorExport.exportSVGFile(shapes, artW, artH, animations), [shapes, artW, artH, animations]);
730
- const exportPNG = React.useCallback(() => vectorEditorExport.exportPNGFile(shapes, artW, artH), [shapes, artW, artH]);
731
- const exportJPG = React.useCallback(() => vectorEditorExport.exportJPGFile(shapes, artW, artH), [shapes, artW, artH]);
732
- const exportWebP = React.useCallback(() => vectorEditorExport.exportWebPFile(shapes, artW, artH), [shapes, artW, artH]);
733
- const exportBMP = React.useCallback(() => vectorEditorExport.exportBMPFile(shapes, artW, artH), [shapes, artW, artH]);
734
- /* ── tool switch helper (finishes pen) ── */
735
- const switchTool = React.useCallback((newTool) => {
736
- if (tool === 'pen' && newTool !== 'pen' && pendingShape.current) {
737
- snap(shapes);
738
- pendingShape.current = null;
739
- penPts.current = [];
740
- }
741
- setTool(newTool);
742
- }, [tool, shapes, snap]);
743
- /* ── keyboard ── */
744
- React.useEffect(() => {
745
- const h = (e) => {
746
- const t = e.target;
747
- if (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA') {
748
- return;
749
- }
750
- if (e.ctrlKey && e.key === 'z') {
751
- e.preventDefault();
752
- undo();
753
- return;
754
- }
755
- if (e.ctrlKey && (e.key === 'y' || (e.shiftKey && e.key === 'Z'))) {
756
- e.preventDefault();
757
- redo();
758
- return;
759
- }
760
- if (e.ctrlKey && e.key === 'c') {
761
- e.preventDefault();
762
- copyShape();
763
- return;
764
- }
765
- if (e.ctrlKey && e.key === 'v') {
766
- e.preventDefault();
767
- pasteShape();
768
- return;
769
- }
770
- if (e.ctrlKey && e.key === 'd') {
771
- e.preventDefault();
772
- duplicateShape();
773
- return;
774
- }
775
- if (e.key === 'Escape') {
776
- if (pendingShape.current) {
777
- setShapes((prev) => prev.filter((s) => s.id !== pendingShape.current.id));
778
- pendingShape.current = null;
779
- drawing.current = false;
780
- penPts.current = [];
781
- }
782
- else {
783
- setSelId(null);
784
- }
785
- return;
786
- }
787
- const km = {
788
- v: 'pointer',
789
- r: 'rect',
790
- u: 'roundrect',
791
- c: 'ellipse',
792
- l: 'line',
793
- a: 'arrow',
794
- g: 'polygon',
795
- s: 'star',
796
- p: 'pen',
797
- t: 'text',
798
- };
799
- if (km[e.key.toLowerCase()] && !e.ctrlKey && !e.altKey) {
800
- setTool(km[e.key.toLowerCase()]);
801
- return;
802
- }
803
- if (e.key === 'Delete' || e.key === 'Backspace') {
804
- deleteShape();
805
- }
806
- };
807
- window.addEventListener('keydown', h);
808
- return () => window.removeEventListener('keydown', h);
809
- }, [undo, redo, deleteShape, copyShape, pasteShape, duplicateShape]);
810
- /* ── return ── */
811
- return {
812
- // state
813
- /** The question the editor waits on; render it with `EditorAskDialog`. */
814
- pendingAsk,
815
- shapes,
816
- selId,
817
- tool,
818
- fillCol,
819
- strokeCol,
820
- strokeW,
821
- fontSize,
822
- fontFamily,
823
- shapeOpacity,
824
- cornerRadius,
825
- polygonSides,
826
- starPoints2,
827
- starInnerRatio,
828
- zoom,
829
- px,
830
- py,
831
- dlgSvg,
832
- svgText,
833
- snapGrid,
834
- gridSize,
835
- hasClipboard,
836
- boolSecondShape,
837
- // derived
838
- selShape,
839
- // setters
840
- setTool,
841
- setFillCol,
842
- setStrokeCol,
843
- setStrokeW,
844
- setFontSize,
845
- setFontFamily,
846
- setShapeOpacity,
847
- setCornerRadius,
848
- setPolygonSides,
849
- setStarPoints2,
850
- setStarInnerRatio,
851
- setZoom,
852
- setDlgSvg,
853
- setSvgText,
854
- setSnapGrid,
855
- setGridSize,
856
- setSelId,
857
- setBoolSecondShape,
858
- setShapes,
859
- snap,
860
- // refs
861
- boxRef,
862
- fInp,
863
- // mouse handlers
864
- onDown,
865
- onMove,
866
- onUp,
867
- onWheel,
868
- onDblClick,
869
- // actions
870
- undo,
871
- redo,
872
- deleteShape,
873
- bringToFront,
874
- sendToBack,
875
- updateSel,
876
- copyShape,
877
- pasteShape,
878
- duplicateShape,
879
- flipH,
880
- flipV,
881
- alignShapes,
882
- unionShapes,
883
- subtractShapes,
884
- intersectShapes,
885
- excludeShapes,
886
- outlineStroke,
887
- importSVGString,
888
- openFile,
889
- newCanvas,
890
- exportSVG,
891
- exportPNG,
892
- exportJPG,
893
- exportWebP,
894
- exportBMP,
895
- switchTool,
896
- saveToLibrary,
897
- // animations
898
- animations,
899
- isPlaying,
900
- previewTime,
901
- setShapeAnimation,
902
- setIsPlaying,
903
- setPreviewTime,
646
+ setDlgSvg(false);
647
+ } catch (e) {
648
+ console.warn(
649
+ "[VectorEditor] SVG parse error:",
650
+ e instanceof Error ? e.message : "Failed to parse SVG."
651
+ );
652
+ }
653
+ },
654
+ [shapes, snap]
655
+ );
656
+ const openFile = React.useCallback(
657
+ async (file) => {
658
+ importSVGString(await file.text());
659
+ },
660
+ [importSVGString]
661
+ );
662
+ const newCanvas = React.useCallback(() => {
663
+ snap([]);
664
+ setShapes([]);
665
+ setSelId(null);
666
+ setZoom(1);
667
+ setPx(0);
668
+ setPy(0);
669
+ }, [snap]);
670
+ const saveToLibrary = React.useCallback(() => {
671
+ if (!onSaveToLibrary) {
672
+ return;
673
+ }
674
+ const svg = vectorEditorExport.buildSVGString(shapes, artW, artH);
675
+ const blob = new Blob([svg], { type: "image/svg+xml" });
676
+ const reader = new FileReader();
677
+ reader.onload = () => {
678
+ const dataUrl = reader.result;
679
+ void ask("prompt", "Save to library as:", "drawing.svg").then((name) => {
680
+ if (name && name.trim()) {
681
+ onSaveToLibrary(dataUrl, name.trim(), "image/svg+xml");
682
+ }
683
+ });
684
+ };
685
+ reader.readAsDataURL(blob);
686
+ }, [shapes, artW, artH, onSaveToLibrary, ask]);
687
+ const exportSVG = React.useCallback(
688
+ () => vectorEditorExport.exportSVGFile(shapes, artW, artH, animations),
689
+ [shapes, artW, artH, animations]
690
+ );
691
+ const exportPNG = React.useCallback(() => vectorEditorExport.exportPNGFile(shapes, artW, artH), [shapes, artW, artH]);
692
+ const exportJPG = React.useCallback(() => vectorEditorExport.exportJPGFile(shapes, artW, artH), [shapes, artW, artH]);
693
+ const exportWebP = React.useCallback(() => vectorEditorExport.exportWebPFile(shapes, artW, artH), [shapes, artW, artH]);
694
+ const exportBMP = React.useCallback(() => vectorEditorExport.exportBMPFile(shapes, artW, artH), [shapes, artW, artH]);
695
+ const switchTool = React.useCallback(
696
+ (newTool) => {
697
+ if (tool === "pen" && newTool !== "pen" && pendingShape.current) {
698
+ snap(shapes);
699
+ pendingShape.current = null;
700
+ penPts.current = [];
701
+ }
702
+ setTool(newTool);
703
+ },
704
+ [tool, shapes, snap]
705
+ );
706
+ React.useEffect(() => {
707
+ const h = (e) => {
708
+ const t = e.target;
709
+ if (t.tagName === "INPUT" || t.tagName === "TEXTAREA") {
710
+ return;
711
+ }
712
+ if (e.ctrlKey && e.key === "z") {
713
+ e.preventDefault();
714
+ undo();
715
+ return;
716
+ }
717
+ if (e.ctrlKey && (e.key === "y" || e.shiftKey && e.key === "Z")) {
718
+ e.preventDefault();
719
+ redo();
720
+ return;
721
+ }
722
+ if (e.ctrlKey && e.key === "c") {
723
+ e.preventDefault();
724
+ copyShape();
725
+ return;
726
+ }
727
+ if (e.ctrlKey && e.key === "v") {
728
+ e.preventDefault();
729
+ pasteShape();
730
+ return;
731
+ }
732
+ if (e.ctrlKey && e.key === "d") {
733
+ e.preventDefault();
734
+ duplicateShape();
735
+ return;
736
+ }
737
+ if (e.key === "Escape") {
738
+ if (pendingShape.current) {
739
+ setShapes((prev) => prev.filter((s) => s.id !== pendingShape.current.id));
740
+ pendingShape.current = null;
741
+ drawing.current = false;
742
+ penPts.current = [];
743
+ } else {
744
+ setSelId(null);
745
+ }
746
+ return;
747
+ }
748
+ const km = {
749
+ v: "pointer",
750
+ r: "rect",
751
+ u: "roundrect",
752
+ c: "ellipse",
753
+ l: "line",
754
+ a: "arrow",
755
+ g: "polygon",
756
+ s: "star",
757
+ p: "pen",
758
+ t: "text"
759
+ };
760
+ if (km[e.key.toLowerCase()] && !e.ctrlKey && !e.altKey) {
761
+ setTool(km[e.key.toLowerCase()]);
762
+ return;
763
+ }
764
+ if (e.key === "Delete" || e.key === "Backspace") {
765
+ deleteShape();
766
+ }
904
767
  };
768
+ window.addEventListener("keydown", h);
769
+ return () => window.removeEventListener("keydown", h);
770
+ }, [undo, redo, deleteShape, copyShape, pasteShape, duplicateShape]);
771
+ return {
772
+ // state
773
+ /** The question the editor waits on; render it with `EditorAskDialog`. */
774
+ pendingAsk,
775
+ shapes,
776
+ selId,
777
+ tool,
778
+ fillCol,
779
+ strokeCol,
780
+ strokeW,
781
+ fontSize,
782
+ fontFamily,
783
+ shapeOpacity,
784
+ cornerRadius,
785
+ polygonSides,
786
+ starPoints2,
787
+ starInnerRatio,
788
+ zoom,
789
+ px,
790
+ py,
791
+ dlgSvg,
792
+ svgText,
793
+ snapGrid,
794
+ gridSize,
795
+ hasClipboard,
796
+ boolSecondShape,
797
+ // derived
798
+ selShape,
799
+ // setters
800
+ setTool,
801
+ setFillCol,
802
+ setStrokeCol,
803
+ setStrokeW,
804
+ setFontSize,
805
+ setFontFamily,
806
+ setShapeOpacity,
807
+ setCornerRadius,
808
+ setPolygonSides,
809
+ setStarPoints2,
810
+ setStarInnerRatio,
811
+ setZoom,
812
+ setDlgSvg,
813
+ setSvgText,
814
+ setSnapGrid,
815
+ setGridSize,
816
+ setSelId,
817
+ setBoolSecondShape,
818
+ setShapes,
819
+ snap,
820
+ // refs
821
+ boxRef,
822
+ fInp,
823
+ // mouse handlers
824
+ onDown,
825
+ onMove,
826
+ onUp,
827
+ onWheel,
828
+ onDblClick,
829
+ // actions
830
+ undo,
831
+ redo,
832
+ deleteShape,
833
+ bringToFront,
834
+ sendToBack,
835
+ updateSel,
836
+ copyShape,
837
+ pasteShape,
838
+ duplicateShape,
839
+ flipH,
840
+ flipV,
841
+ alignShapes,
842
+ unionShapes,
843
+ subtractShapes,
844
+ intersectShapes,
845
+ excludeShapes,
846
+ outlineStroke,
847
+ importSVGString,
848
+ openFile,
849
+ newCanvas,
850
+ exportSVG,
851
+ exportPNG,
852
+ exportJPG,
853
+ exportWebP,
854
+ exportBMP,
855
+ switchTool,
856
+ saveToLibrary,
857
+ // animations
858
+ animations,
859
+ isPlaying,
860
+ previewTime,
861
+ setShapeAnimation,
862
+ setIsPlaying,
863
+ setPreviewTime
864
+ };
905
865
  }
906
-
907
866
  exports.useVectorEditor = useVectorEditor;