@elixpo/lixsketch 5.6.9 → 5.6.11

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 (165) hide show
  1. package/dist/react/{AIRenderer-R5M2CIDO.js → AIRenderer-JNEZYDYQ.js} +4 -4
  2. package/dist/react/Arrow-QMR77LGQ.js +9 -0
  3. package/dist/react/Circle-5WL5HZA5.js +11 -0
  4. package/dist/react/{CopyPaste-PPDS3AD6.js → CopyPaste-V7AY56H3.js} +74 -30
  5. package/dist/react/CopyPaste-V7AY56H3.js.map +7 -0
  6. package/dist/react/{EventDispatcher-UV7LFSAS.js → EventDispatcher-E4OJCMY6.js} +154 -57
  7. package/dist/react/{EventDispatcher-UV7LFSAS.js.map → EventDispatcher-E4OJCMY6.js.map} +3 -3
  8. package/dist/react/Frame-OZCDVPMI.js +11 -0
  9. package/dist/react/FreehandStroke-QSSYWBFK.js +9 -0
  10. package/dist/react/IconShape-HXUV4VRT.js +10 -0
  11. package/dist/react/ImageShape-JGLT2UGG.js +10 -0
  12. package/dist/react/Line-YONPLUYT.js +11 -0
  13. package/dist/react/{MermaidFlowchartRenderer-EL3Q2MNW.js → MermaidFlowchartRenderer-FSH43RUP.js} +26 -7
  14. package/dist/react/{MermaidFlowchartRenderer-EL3Q2MNW.js.map → MermaidFlowchartRenderer-FSH43RUP.js.map} +2 -2
  15. package/dist/react/{MermaidSequenceRenderer-K4KHHEVC.js → MermaidSequenceRenderer-FRPNOF2V.js} +11 -9
  16. package/dist/react/{MermaidSequenceRenderer-K4KHHEVC.js.map → MermaidSequenceRenderer-FRPNOF2V.js.map} +2 -2
  17. package/dist/react/{MermaidStructuredRenderer-6LN3WISP.js → MermaidStructuredRenderer-BZGQLRIH.js} +13 -6
  18. package/dist/react/{MermaidStructuredRenderer-6LN3WISP.js.map → MermaidStructuredRenderer-BZGQLRIH.js.map} +2 -2
  19. package/dist/react/Rectangle-7KMA3SC6.js +11 -0
  20. package/dist/react/{SceneSerializer-H6KMQCO2.js → SceneSerializer-7O72LEZP.js} +13 -12
  21. package/dist/react/{SceneSerializer-H6KMQCO2.js.map → SceneSerializer-7O72LEZP.js.map} +1 -1
  22. package/dist/react/{Selection-CFWRGVKH.js → Selection-S4FYSZIF.js} +5 -5
  23. package/dist/react/{SketchEngine-UFLZJQXS.js → SketchEngine-VTWURP2K.js} +26 -26
  24. package/dist/react/{UndoRedo-RXO7YHA2.js → UndoRedo-YLQJ6DXW.js} +8 -2
  25. package/dist/react/{ZoomPan-RR7KUYZT.js → ZoomPan-AEKWKUCH.js} +2 -2
  26. package/dist/react/{arrowTool-DO327FKU.js → arrowTool-C4RDWQEX.js} +3 -3
  27. package/dist/react/{chunk-X5ENBMGL.js → chunk-2U47AU6Y.js} +3 -3
  28. package/dist/react/{chunk-X5ENBMGL.js.map → chunk-2U47AU6Y.js.map} +1 -1
  29. package/dist/react/{chunk-RK3NVAER.js → chunk-75OPJOWN.js} +7 -7
  30. package/dist/react/chunk-75OPJOWN.js.map +7 -0
  31. package/dist/react/{chunk-DODPOWNX.js → chunk-CWE5IFG7.js} +5 -5
  32. package/dist/react/{chunk-DODPOWNX.js.map → chunk-CWE5IFG7.js.map} +1 -1
  33. package/dist/react/{chunk-O7KXLFZN.js → chunk-IS2MKWGH.js} +8 -7
  34. package/dist/react/chunk-IS2MKWGH.js.map +7 -0
  35. package/dist/react/{chunk-ZNZAICWY.js → chunk-JEHOEQ6Z.js} +13 -5
  36. package/dist/react/chunk-JEHOEQ6Z.js.map +7 -0
  37. package/dist/react/{chunk-BOZEDUAF.js → chunk-JYBJXTLM.js} +40 -15
  38. package/dist/react/chunk-JYBJXTLM.js.map +7 -0
  39. package/dist/react/{chunk-6LWUR7XB.js → chunk-LXEHYCKH.js} +4 -4
  40. package/dist/react/{chunk-6LWUR7XB.js.map → chunk-LXEHYCKH.js.map} +1 -1
  41. package/dist/react/chunk-M7OYYJC6.js +17 -0
  42. package/dist/react/chunk-M7OYYJC6.js.map +7 -0
  43. package/dist/react/chunk-MS4TIQ6Z.js +131 -0
  44. package/dist/react/chunk-MS4TIQ6Z.js.map +7 -0
  45. package/dist/react/{chunk-2ALS5JGS.js → chunk-OCJN32IL.js} +2 -2
  46. package/dist/react/{chunk-KZBKYHZO.js → chunk-P7VIYR6R.js} +8 -5
  47. package/dist/react/chunk-P7VIYR6R.js.map +7 -0
  48. package/dist/react/{chunk-KC26KJW4.js → chunk-PX2635H5.js} +32 -10
  49. package/dist/react/chunk-PX2635H5.js.map +7 -0
  50. package/dist/react/{chunk-37PF7FG2.js → chunk-SDG4U7TP.js} +7 -6
  51. package/dist/react/chunk-SDG4U7TP.js.map +7 -0
  52. package/dist/react/{chunk-CGMUXYPJ.js → chunk-STJERZ25.js} +8 -4
  53. package/dist/react/chunk-STJERZ25.js.map +7 -0
  54. package/dist/react/{chunk-VZPH7OAM.js → chunk-TCWCC4Z4.js} +7 -3
  55. package/dist/react/chunk-TCWCC4Z4.js.map +7 -0
  56. package/dist/react/{chunk-VZX55VQL.js → chunk-TFCBCWL5.js} +90 -33
  57. package/dist/react/chunk-TFCBCWL5.js.map +7 -0
  58. package/dist/react/{chunk-GQLXUZHQ.js → chunk-TWHJ6PJK.js} +5 -3
  59. package/dist/react/chunk-TWHJ6PJK.js.map +7 -0
  60. package/dist/react/{chunk-NH3NVSWT.js → chunk-UCOK4V5Z.js} +4 -3
  61. package/dist/react/{chunk-NH3NVSWT.js.map → chunk-UCOK4V5Z.js.map} +2 -2
  62. package/dist/react/{chunk-W7XUY4GR.js → chunk-VJDNKJBX.js} +56 -4
  63. package/dist/react/chunk-VJDNKJBX.js.map +7 -0
  64. package/dist/react/{chunk-BT2EIJD4.js → chunk-WCA3FGWE.js} +6 -3
  65. package/dist/react/chunk-WCA3FGWE.js.map +7 -0
  66. package/dist/react/{chunk-I7TVGJFF.js → chunk-Y3FJMYPX.js} +4 -4
  67. package/dist/react/{chunk-I7TVGJFF.js.map → chunk-Y3FJMYPX.js.map} +1 -1
  68. package/dist/react/{chunk-7DD2JRBC.js → chunk-YCGLGY4C.js} +2 -2
  69. package/dist/react/{circleTool-NK4RVDAR.js → circleTool-QQV2QEVF.js} +4 -4
  70. package/dist/react/{codeTool-3WF4HEOW.js → codeTool-PTCPSZ4L.js} +5 -5
  71. package/dist/react/{frameTool-VCNV2NG2.js → frameTool-7GQPLU4Y.js} +4 -4
  72. package/dist/react/{freehandTool-TBAOD46K.js → freehandTool-3RJXTVAK.js} +4 -4
  73. package/dist/react/{iconTool-65JCAKYF.js → iconTool-DW5YJX7P.js} +5 -5
  74. package/dist/react/{imageTool-UQZUBOK2.js → imageTool-7GZQKWYT.js} +5 -5
  75. package/dist/react/index.js +68 -63
  76. package/dist/react/index.js.map +2 -2
  77. package/dist/react/{lineTool-762KBYOM.js → lineTool-5CY7BHND.js} +4 -4
  78. package/dist/react/{rectangleTool-IQK3HZK6.js → rectangleTool-DK4QL4QW.js} +4 -4
  79. package/dist/react/styles.css +4 -0
  80. package/dist/react/{textTool-ITG2SBJK.js → textTool-GP5QQYF3.js} +6 -6
  81. package/package.json +1 -1
  82. package/src/core/CopyPaste.js +66 -18
  83. package/src/core/EventDispatcher.js +71 -47
  84. package/src/core/MermaidFlowchartRenderer.js +34 -8
  85. package/src/core/MermaidSequenceRenderer.js +10 -8
  86. package/src/core/MermaidStructuredRenderer.js +12 -5
  87. package/src/core/ScreenSpaceControls.js +102 -13
  88. package/src/core/Selection.js +26 -7
  89. package/src/core/SelectionGesture.js +50 -0
  90. package/src/core/UndoRedo.js +108 -35
  91. package/src/react/components/sidebars/CircleSidebar.jsx +11 -5
  92. package/src/react/components/sidebars/IconSidebar.jsx +10 -4
  93. package/src/react/components/sidebars/RectangleSidebar.jsx +11 -5
  94. package/src/react/components/sidebars/ShapeSidebar.jsx +29 -37
  95. package/src/react/components/sidebars/TextSidebar.jsx +10 -4
  96. package/src/react/store/useUIStore.js +8 -1
  97. package/src/react/styles.css +13 -0
  98. package/src/react/utils/canvasBackgrounds.js +11 -2
  99. package/src/shapes/Arrow.js +4 -3
  100. package/src/shapes/Circle.js +58 -2
  101. package/src/shapes/Frame.js +11 -4
  102. package/src/shapes/FreehandStroke.js +2 -1
  103. package/src/shapes/Line.js +47 -20
  104. package/src/shapes/Rectangle.js +3 -1
  105. package/src/tools/arrowTool.js +1 -1
  106. package/src/tools/circleTool.js +5 -2
  107. package/src/tools/codeTool.js +1 -1
  108. package/src/tools/frameTool.js +3 -2
  109. package/src/tools/freehandTool.js +1 -1
  110. package/src/tools/iconTool.js +4 -3
  111. package/src/tools/imageTool.js +3 -2
  112. package/src/tools/lineTool.js +5 -1
  113. package/src/tools/rectangleTool.js +1 -1
  114. package/src/tools/shapeRecognitionTool.js +1 -1
  115. package/src/tools/textTool.js +2 -2
  116. package/src/utils/themeColors.js +13 -0
  117. package/dist/react/Arrow-EF7ALH2Y.js +0 -8
  118. package/dist/react/Circle-HMM5PKJ2.js +0 -10
  119. package/dist/react/CopyPaste-PPDS3AD6.js.map +0 -7
  120. package/dist/react/Frame-DELCMYV6.js +0 -11
  121. package/dist/react/FreehandStroke-WXIJKD5I.js +0 -9
  122. package/dist/react/IconShape-PZHMZSLR.js +0 -10
  123. package/dist/react/ImageShape-ZMBZARRI.js +0 -10
  124. package/dist/react/Line-VPVPL6ET.js +0 -10
  125. package/dist/react/Rectangle-4U4CD2ER.js +0 -10
  126. package/dist/react/chunk-37PF7FG2.js.map +0 -7
  127. package/dist/react/chunk-BOZEDUAF.js.map +0 -7
  128. package/dist/react/chunk-BT2EIJD4.js.map +0 -7
  129. package/dist/react/chunk-CGMUXYPJ.js.map +0 -7
  130. package/dist/react/chunk-GQLXUZHQ.js.map +0 -7
  131. package/dist/react/chunk-KC26KJW4.js.map +0 -7
  132. package/dist/react/chunk-KZBKYHZO.js.map +0 -7
  133. package/dist/react/chunk-O7KXLFZN.js.map +0 -7
  134. package/dist/react/chunk-PAKA6HUX.js +0 -59
  135. package/dist/react/chunk-PAKA6HUX.js.map +0 -7
  136. package/dist/react/chunk-RK3NVAER.js.map +0 -7
  137. package/dist/react/chunk-VZPH7OAM.js.map +0 -7
  138. package/dist/react/chunk-VZX55VQL.js.map +0 -7
  139. package/dist/react/chunk-W7XUY4GR.js.map +0 -7
  140. package/dist/react/chunk-ZNZAICWY.js.map +0 -7
  141. /package/dist/react/{AIRenderer-R5M2CIDO.js.map → AIRenderer-JNEZYDYQ.js.map} +0 -0
  142. /package/dist/react/{Arrow-EF7ALH2Y.js.map → Arrow-QMR77LGQ.js.map} +0 -0
  143. /package/dist/react/{Circle-HMM5PKJ2.js.map → Circle-5WL5HZA5.js.map} +0 -0
  144. /package/dist/react/{Frame-DELCMYV6.js.map → Frame-OZCDVPMI.js.map} +0 -0
  145. /package/dist/react/{FreehandStroke-WXIJKD5I.js.map → FreehandStroke-QSSYWBFK.js.map} +0 -0
  146. /package/dist/react/{IconShape-PZHMZSLR.js.map → IconShape-HXUV4VRT.js.map} +0 -0
  147. /package/dist/react/{ImageShape-ZMBZARRI.js.map → ImageShape-JGLT2UGG.js.map} +0 -0
  148. /package/dist/react/{Line-VPVPL6ET.js.map → Line-YONPLUYT.js.map} +0 -0
  149. /package/dist/react/{Rectangle-4U4CD2ER.js.map → Rectangle-7KMA3SC6.js.map} +0 -0
  150. /package/dist/react/{Selection-CFWRGVKH.js.map → Selection-S4FYSZIF.js.map} +0 -0
  151. /package/dist/react/{SketchEngine-UFLZJQXS.js.map → SketchEngine-VTWURP2K.js.map} +0 -0
  152. /package/dist/react/{UndoRedo-RXO7YHA2.js.map → UndoRedo-YLQJ6DXW.js.map} +0 -0
  153. /package/dist/react/{ZoomPan-RR7KUYZT.js.map → ZoomPan-AEKWKUCH.js.map} +0 -0
  154. /package/dist/react/{arrowTool-DO327FKU.js.map → arrowTool-C4RDWQEX.js.map} +0 -0
  155. /package/dist/react/{chunk-2ALS5JGS.js.map → chunk-OCJN32IL.js.map} +0 -0
  156. /package/dist/react/{chunk-7DD2JRBC.js.map → chunk-YCGLGY4C.js.map} +0 -0
  157. /package/dist/react/{circleTool-NK4RVDAR.js.map → circleTool-QQV2QEVF.js.map} +0 -0
  158. /package/dist/react/{codeTool-3WF4HEOW.js.map → codeTool-PTCPSZ4L.js.map} +0 -0
  159. /package/dist/react/{frameTool-VCNV2NG2.js.map → frameTool-7GQPLU4Y.js.map} +0 -0
  160. /package/dist/react/{freehandTool-TBAOD46K.js.map → freehandTool-3RJXTVAK.js.map} +0 -0
  161. /package/dist/react/{iconTool-65JCAKYF.js.map → iconTool-DW5YJX7P.js.map} +0 -0
  162. /package/dist/react/{imageTool-UQZUBOK2.js.map → imageTool-7GZQKWYT.js.map} +0 -0
  163. /package/dist/react/{lineTool-762KBYOM.js.map → lineTool-5CY7BHND.js.map} +0 -0
  164. /package/dist/react/{rectangleTool-IQK3HZK6.js.map → rectangleTool-DK4QL4QW.js.map} +0 -0
  165. /package/dist/react/{textTool-ITG2SBJK.js.map → textTool-GP5QQYF3.js.map} +0 -0
@@ -3,12 +3,12 @@ import {
3
3
  handleMouseDown,
4
4
  handleMouseMove,
5
5
  handleMouseUp
6
- } from "./chunk-CGMUXYPJ.js";
7
- import "./chunk-X5ENBMGL.js";
8
- import "./chunk-VZX55VQL.js";
6
+ } from "./chunk-STJERZ25.js";
7
+ import "./chunk-2U47AU6Y.js";
8
+ import "./chunk-TFCBCWL5.js";
9
9
  export {
10
10
  handleMouseDown as handleMouseDownLine,
11
11
  handleMouseMove as handleMouseMoveLine,
12
12
  handleMouseUp as handleMouseUpLine
13
13
  };
14
- //# sourceMappingURL=lineTool-762KBYOM.js.map
14
+ //# sourceMappingURL=lineTool-5CY7BHND.js.map
@@ -3,12 +3,12 @@ import {
3
3
  handleMouseDownRect,
4
4
  handleMouseMoveRect,
5
5
  handleMouseUpRect
6
- } from "./chunk-I7TVGJFF.js";
7
- import "./chunk-X5ENBMGL.js";
8
- import "./chunk-VZX55VQL.js";
6
+ } from "./chunk-Y3FJMYPX.js";
7
+ import "./chunk-2U47AU6Y.js";
8
+ import "./chunk-TFCBCWL5.js";
9
9
  export {
10
10
  handleMouseDownRect,
11
11
  handleMouseMoveRect,
12
12
  handleMouseUpRect
13
13
  };
14
- //# sourceMappingURL=rectangleTool-IQK3HZK6.js.map
14
+ //# sourceMappingURL=rectangleTool-DK4QL4QW.js.map
@@ -6499,6 +6499,10 @@
6499
6499
  }
6500
6500
  .theme-light .lixsketch-canvas-root {
6501
6501
  --lix-icon-filter: none;
6502
+ --lixsketch-bg: #ffffff;
6503
+ }
6504
+ .theme-light .lixsketch-canvas-root :is(.anchor, .resize-anchor, .resize-handle, .rotate-anchor, .rotation-anchor, .multi-selection-anchor, .multi-selection-rotation-anchor) {
6505
+ fill: #ffffff !important;
6502
6506
  }
6503
6507
  .theme-dark .lixsketch-canvas-root {
6504
6508
  --lix-icon-filter: brightness(0) invert(1);
@@ -6,11 +6,11 @@ import {
6
6
  handleTextMouseMove,
7
7
  handleTextMouseUp,
8
8
  updateCodeToggleForShape
9
- } from "./chunk-RK3NVAER.js";
10
- import "./chunk-DODPOWNX.js";
11
- import "./chunk-PAKA6HUX.js";
12
- import "./chunk-X5ENBMGL.js";
13
- import "./chunk-VZX55VQL.js";
9
+ } from "./chunk-75OPJOWN.js";
10
+ import "./chunk-CWE5IFG7.js";
11
+ import "./chunk-MS4TIQ6Z.js";
12
+ import "./chunk-2U47AU6Y.js";
13
+ import "./chunk-TFCBCWL5.js";
14
14
  export {
15
15
  deselectElement as deselectTextElement,
16
16
  enterEditMode,
@@ -19,4 +19,4 @@ export {
19
19
  handleTextMouseUp,
20
20
  updateCodeToggleForShape
21
21
  };
22
- //# sourceMappingURL=textTool-ITG2SBJK.js.map
22
+ //# sourceMappingURL=textTool-GP5QQYF3.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elixpo/lixsketch",
3
- "version": "5.6.9",
3
+ "version": "5.6.11",
4
4
  "description": "Open-source SVG whiteboard engine + React canvas component with hand-drawn aesthetics — the core of LixSketch",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -3,7 +3,7 @@
3
3
  // Supports single and multi-selection copy/paste
4
4
  // Pastes at mouse pointer position with offset to prevent stacking
5
5
 
6
- import { pushCreateAction } from './UndoRedo.js';
6
+ import { beginUndoBatch, endUndoBatch, pushCreateAction } from './UndoRedo.js';
7
7
  import { Rectangle } from '../shapes/Rectangle.js';
8
8
  import { Circle } from '../shapes/Circle.js';
9
9
  import { Line } from '../shapes/Line.js';
@@ -41,13 +41,15 @@ function getSVGCoordsFromMouse(e) {
41
41
  }
42
42
 
43
43
  function cloneOptions(options) {
44
- return JSON.parse(JSON.stringify(options));
44
+ return options ? JSON.parse(JSON.stringify(options)) : {};
45
45
  }
46
46
 
47
47
  // ============================================================
48
48
  // SERIALIZE: Extract copyable data from a shape
49
49
  // ============================================================
50
- function serializeShape(shape) {
50
+ export function serializeShape(shape, visited = new Set()) {
51
+ if (!shape || visited.has(shape)) return null;
52
+ visited.add(shape);
51
53
  switch (shape.shapeName) {
52
54
  case 'rectangle':
53
55
  return {
@@ -112,7 +114,17 @@ function serializeShape(shape) {
112
114
  height: shape.height,
113
115
  rotation: shape.rotation,
114
116
  frameName: shape.frameName,
115
- options: cloneOptions(shape.options)
117
+ frameType: shape._frameType || null,
118
+ webEmbedURL: shape._webEmbedURL || null,
119
+ diagramType: shape._diagramType || null,
120
+ fillStyle: shape.fillStyle,
121
+ fillColor: shape.fillColor,
122
+ gridSize: shape.gridSize,
123
+ gridColor: shape.gridColor,
124
+ options: cloneOptions(shape.options),
125
+ children: (shape.containedShapes || [])
126
+ .map(child => serializeShape(child, visited))
127
+ .filter(Boolean),
116
128
  };
117
129
 
118
130
  case 'text': {
@@ -180,6 +192,18 @@ function serializeShape(shape) {
180
192
  }
181
193
  }
182
194
 
195
+ export function getCopyRoots(selectedShapes) {
196
+ const selectedSet = new Set(selectedShapes);
197
+ return selectedShapes.filter(shape => {
198
+ let parent = shape?.parentFrame;
199
+ while (parent) {
200
+ if (selectedSet.has(parent)) return false;
201
+ parent = parent.parentFrame;
202
+ }
203
+ return true;
204
+ });
205
+ }
206
+
183
207
  // ============================================================
184
208
  // Get bounding center of a serialized shape
185
209
  // ============================================================
@@ -306,9 +330,20 @@ function createShapeFromData(data, offsetX, offsetY) {
306
330
  data.y + offsetY,
307
331
  data.width,
308
332
  data.height,
309
- { ...cloneOptions(data.options), frameName: data.frameName, rotation: data.rotation }
333
+ {
334
+ ...cloneOptions(data.options),
335
+ frameName: data.frameName,
336
+ rotation: data.rotation,
337
+ frameType: data.frameType,
338
+ webEmbedURL: data.webEmbedURL,
339
+ fillStyle: data.fillStyle,
340
+ fillColor: data.fillColor,
341
+ gridSize: data.gridSize,
342
+ gridColor: data.gridColor,
343
+ }
310
344
  );
311
345
  newFrame.rotation = data.rotation;
346
+ if (data.diagramType) newFrame._diagramType = data.diagramType;
312
347
  return newFrame;
313
348
  }
314
349
 
@@ -460,21 +495,23 @@ function createShapeFromData(data, offsetX, offsetY) {
460
495
  // COPY: Serialize currently selected shapes
461
496
  // ============================================================
462
497
  function copySelected() {
463
- const shapesToCopy = [];
498
+ let selectedShapes = [];
464
499
 
465
500
  // Check multi-selection first
466
501
  if (window.multiSelection && window.multiSelection.selectedShapes && window.multiSelection.selectedShapes.size > 0) {
467
- window.multiSelection.selectedShapes.forEach(shape => {
468
- const data = serializeShape(shape);
469
- if (data) shapesToCopy.push(data);
470
- });
502
+ selectedShapes = [...window.multiSelection.selectedShapes];
471
503
  }
472
504
  // Fall back to single currentShape
473
505
  else if (typeof currentShape !== 'undefined' && currentShape && currentShape.isSelected) {
474
- const data = serializeShape(currentShape);
475
- if (data) shapesToCopy.push(data);
506
+ selectedShapes = [currentShape];
476
507
  }
477
508
 
509
+ // If both a frame and one of its children are selected, serialize the
510
+ // child only through the frame. This preserves ownership without creating
511
+ // a second loose duplicate beside the pasted frame.
512
+ const copyRoots = getCopyRoots(selectedShapes);
513
+ const shapesToCopy = copyRoots.map(shape => serializeShape(shape)).filter(Boolean);
514
+
478
515
  if (shapesToCopy.length === 0) return false;
479
516
 
480
517
  // Calculate center of all copied shapes
@@ -532,17 +569,28 @@ function pasteClipboard() {
532
569
  }
533
570
 
534
571
  const pastedShapes = [];
572
+ const undoBatch = beginUndoBatch();
535
573
 
536
- clipboard.shapes.forEach(data => {
574
+ const pasteShapeTree = (data, parentFrame = null) => {
537
575
  const newShape = createShapeFromData(data, groupOffsetX, groupOffsetY);
538
- if (newShape) {
539
- if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
540
- shapes.push(newShape);
541
- }
576
+ if (!newShape) return null;
577
+ if (typeof shapes !== 'undefined' && Array.isArray(shapes)) shapes.push(newShape);
578
+ if (parentFrame?.addShapeToFrame) parentFrame.addShapeToFrame(newShape);
579
+
580
+ if (data.type === 'frame') {
581
+ pushCreateAction(newShape, { frameCreation: true, containedShapes: [] });
582
+ for (const childData of data.children || []) pasteShapeTree(childData, newShape);
583
+ } else {
542
584
  pushCreateAction(newShape);
543
- pastedShapes.push(newShape);
544
585
  }
586
+ return newShape;
587
+ };
588
+
589
+ clipboard.shapes.forEach(data => {
590
+ const newShape = pasteShapeTree(data);
591
+ if (newShape) pastedShapes.push(newShape);
545
592
  });
593
+ endUndoBatch(undoBatch, 'clipboard-paste');
546
594
 
547
595
  // Select the pasted shapes
548
596
  if (pastedShapes.length === 1) {
@@ -17,6 +17,37 @@ import { handleShapeRecognitionDown, handleShapeRecognitionMove, handleShapeReco
17
17
  import { handlePaintBucketDown } from '../tools/paintBucketTool.js';
18
18
  import { handleLassoDown, handleLassoMove, handleLassoUp } from '../tools/lassoTool.js';
19
19
  import { handleWebEmbedDown, handleWebEmbedMove, handleWebEmbedUp } from '../tools/webEmbedTool.js';
20
+ import { createSelectionGestureOwner } from './SelectionGesture.js';
21
+
22
+ const selectionGestureOwner = createSelectionGestureOwner();
23
+
24
+ const shapePointerHandlers = {
25
+ rectangle: { down: handleMouseDownRect, move: handleMouseMoveRect, up: handleMouseUpRect },
26
+ arrow: { down: handleMouseDownArrow, move: handleMouseMoveArrow, up: handleMouseUpArrow },
27
+ circle: { down: handleMouseDownCircle, move: handleMouseMoveCircle, up: handleMouseUpCircle },
28
+ image: { down: handleMouseDownImage, move: handleMouseMoveImage, up: handleMouseUpImage },
29
+ line: { down: handleMouseDownLine, move: handleMouseMoveLine, up: handleMouseUpLine },
30
+ freehandStroke: { down: handleFreehandMouseDown, move: handleFreehandMouseMove, up: handleFreehandMouseUp },
31
+ text: { down: handleTextMouseDown, move: handleTextMouseMove, up: handleTextMouseUp },
32
+ frame: { down: handleMouseDownFrame, move: handleMouseMoveFrame, up: handleMouseUpFrame },
33
+ icon: { down: handleMouseDownIcon, move: handleMouseMoveIcon, up: handleMouseUpIcon },
34
+ code: { down: handleCodeMouseDown, move: handleCodeMouseMove, up: handleCodeMouseUp },
35
+ };
36
+
37
+ function routeShapePointer(phase, event, shape = currentShape) {
38
+ const handler = shapePointerHandlers[shape?.shapeName]?.[phase];
39
+ if (!handler) return false;
40
+ handler(event);
41
+ return true;
42
+ }
43
+
44
+ function captureSelectionGesture(event) {
45
+ if (isMultiSelecting || multiSelection.isDragging || multiSelection.isResizing || multiSelection.isRotating) {
46
+ selectionGestureOwner.captureMulti(event.pointerId);
47
+ } else if (currentShape?.isSelected) {
48
+ selectionGestureOwner.captureShape(currentShape, event.pointerId);
49
+ }
50
+ }
20
51
 
21
52
  // === Auto-scroll when dragging near viewport edges ===
22
53
  const EDGE_THRESHOLD = 40; // px from edge to start scrolling
@@ -85,6 +116,7 @@ function _onDocumentDragMove(e) {
85
116
  const clampedX = Math.max(rect.left, Math.min(rect.right, e.clientX));
86
117
  const clampedY = Math.max(rect.top, Math.min(rect.bottom, e.clientY));
87
118
  const clampedEvent = new PointerEvent(e.type, {
119
+ pointerId: e.pointerId,
88
120
  clientX: clampedX,
89
121
  clientY: clampedY,
90
122
  buttons: e.buttons,
@@ -114,6 +146,7 @@ const handleMainMouseDown = (e) => {
114
146
  // simultaneously pans (handled in ZoomPan) and starts a selection
115
147
  // marquee.
116
148
  if (e.button === 1 || e.button === 2 || window.__spacePanActive) return;
149
+ selectionGestureOwner.clearAll();
117
150
  // Safety: remove any stray selection rectangle from a previous interrupted drag
118
151
  removeMultiSelectionRect();
119
152
 
@@ -178,6 +211,7 @@ const handleMainMouseDown = (e) => {
178
211
  else if (isSelectionToolActive) {
179
212
  // Try multi-selection first when selection tool is active
180
213
  if (handleMultiSelectionMouseDown(e)) {
214
+ captureSelectionGesture(e);
181
215
  return; // Multi-selection handled the event
182
216
  }
183
217
 
@@ -186,43 +220,7 @@ const handleMainMouseDown = (e) => {
186
220
  let handled = false;
187
221
 
188
222
  // If multi-selection didn't handle it, proceed with shape-specific selection
189
- if (currentShape?.shapeName === 'rectangle') {
190
- handleMouseDownRect(e);
191
- handled = true;
192
- } else if (currentShape?.shapeName === 'arrow') {
193
- handleMouseDownArrow(e);
194
- handled = true;
195
- } else if (currentShape?.shapeName === 'circle') {
196
- handleMouseDownCircle(e);
197
- handled = true;
198
- } else if (currentShape?.shapeName === 'image') {
199
- handleMouseDownImage(e);
200
- handled = true;
201
- }
202
- else if (currentShape?.shapeName === 'line') {
203
- handleMouseDownLine(e);
204
- handled = true;
205
- }
206
- else if (currentShape?.shapeName === 'freehandStroke') {
207
- handleFreehandMouseDown(e);
208
- handled = true;
209
- }
210
- else if (currentShape?.shapeName === 'text') {
211
- handleTextMouseDown(e);
212
- handled = true;
213
- }
214
- else if (currentShape?.shapeName === 'frame') {
215
- handleMouseDownFrame(e);
216
- handled = true;
217
- }
218
- else if (currentShape?.shapeName === 'icon') {
219
- handleMouseDownIcon(e);
220
- handled = true;
221
- }
222
- else if( currentShape?.shapeName === 'code') {
223
- handleCodeMouseDown(e);
224
- handled = true;
225
- }
223
+ handled = routeShapePointer('down', e);
226
224
 
227
225
  // If the handler deselected (currentShape cleared) but didn't select something new,
228
226
  // fall through to try all handlers so clicking another shape type works
@@ -233,25 +231,25 @@ const handleMainMouseDown = (e) => {
233
231
  if (!handled) {
234
232
  const originalCurrentShape = currentShape;
235
233
  handleMouseDownRect(e);
236
- if (currentShape && currentShape !== originalCurrentShape) return;
234
+ if (currentShape && currentShape !== originalCurrentShape) { captureSelectionGesture(e); return; }
237
235
  handleMouseDownCircle(e);
238
- if (currentShape && currentShape !== originalCurrentShape) return;
236
+ if (currentShape && currentShape !== originalCurrentShape) { captureSelectionGesture(e); return; }
239
237
  handleMouseDownArrow(e);
240
- if (currentShape && currentShape !== originalCurrentShape) return;
238
+ if (currentShape && currentShape !== originalCurrentShape) { captureSelectionGesture(e); return; }
241
239
  handleMouseDownImage(e);
242
- if (currentShape && currentShape !== originalCurrentShape) return;
240
+ if (currentShape && currentShape !== originalCurrentShape) { captureSelectionGesture(e); return; }
243
241
  handleMouseDownLine(e);
244
- if (currentShape && currentShape !== originalCurrentShape) return;
242
+ if (currentShape && currentShape !== originalCurrentShape) { captureSelectionGesture(e); return; }
245
243
  handleFreehandMouseDown(e);
246
- if (currentShape && currentShape !== originalCurrentShape) return;
244
+ if (currentShape && currentShape !== originalCurrentShape) { captureSelectionGesture(e); return; }
247
245
  handleTextMouseDown(e);
248
- if (currentShape && currentShape !== originalCurrentShape) return;
246
+ if (currentShape && currentShape !== originalCurrentShape) { captureSelectionGesture(e); return; }
249
247
  handleMouseDownFrame(e);
250
- if (currentShape && currentShape !== originalCurrentShape) return;
248
+ if (currentShape && currentShape !== originalCurrentShape) { captureSelectionGesture(e); return; }
251
249
  handleMouseDownIcon(e);
252
- if (currentShape && currentShape !== originalCurrentShape) return;
250
+ if (currentShape && currentShape !== originalCurrentShape) { captureSelectionGesture(e); return; }
253
251
  handleCodeMouseDown(e);
254
- if (currentShape && currentShape !== originalCurrentShape) return;
252
+ if (currentShape && currentShape !== originalCurrentShape) { captureSelectionGesture(e); return; }
255
253
  if (currentShape === originalCurrentShape) {
256
254
  if (currentShape) {
257
255
  currentShape.removeSelection();
@@ -259,6 +257,7 @@ const handleMainMouseDown = (e) => {
259
257
  }
260
258
  }
261
259
  }
260
+ captureSelectionGesture(e);
262
261
  }
263
262
  };
264
263
 
@@ -306,6 +305,16 @@ const handleMainMouseMove = (e) => {
306
305
  handleMouseMoveIcon(e);
307
306
  }
308
307
  else if (isSelectionToolActive) {
308
+ const activeGesture = selectionGestureOwner.get(e.pointerId);
309
+ if (activeGesture) {
310
+ if (activeGesture.kind === 'multi') {
311
+ handleMultiSelectionMouseMove(e);
312
+ } else if (currentShape === activeGesture.shape && currentShape?.isSelected) {
313
+ routeShapePointer('move', e, activeGesture.shape);
314
+ }
315
+ return;
316
+ }
317
+
309
318
  // Handle multi-selection operations first - these take priority
310
319
  if (isMultiSelecting || multiSelection.isDragging || multiSelection.isResizing || multiSelection.isRotating) {
311
320
  if (handleMultiSelectionMouseMove(e)) {
@@ -400,6 +409,20 @@ const handleMainMouseUp = (e) => {
400
409
  }
401
410
 
402
411
  else if (isSelectionToolActive) {
412
+ const activeGesture = selectionGestureOwner.get(e.pointerId);
413
+ if (activeGesture) {
414
+ try {
415
+ if (activeGesture.kind === 'multi') {
416
+ handleMultiSelectionMouseUp(e);
417
+ } else if (currentShape === activeGesture.shape) {
418
+ routeShapePointer('up', e, activeGesture.shape);
419
+ }
420
+ } finally {
421
+ selectionGestureOwner.clear(e.pointerId);
422
+ }
423
+ return;
424
+ }
425
+
403
426
  // Always try multi-selection cleanup first
404
427
  if (isMultiSelecting || multiSelection.isDragging || multiSelection.isResizing || multiSelection.isRotating) {
405
428
  handleMultiSelectionMouseUp(e);
@@ -526,6 +549,7 @@ function initEventDispatcher(svgEl) {
526
549
 
527
550
  function cleanupEventDispatcher() {
528
551
  _stopAutoScroll();
552
+ selectionGestureOwner.clearAll();
529
553
  if (_documentDragActive) {
530
554
  document.removeEventListener('pointermove', _onDocumentDragMove);
531
555
  document.removeEventListener('pointerup', _onDocumentDragUp);
@@ -348,6 +348,29 @@ function getEdgePoint(node, target) {
348
348
  return { x: node.x, y: node.cy };
349
349
  }
350
350
 
351
+ /**
352
+ * Resolve canvas connector endpoints on the sides facing the other node.
353
+ * Passing node centres to autoAttach leaves short rectangles outside its
354
+ * boundary tolerance, so the connector starts under the label instead.
355
+ */
356
+ export function getCanvasFlowchartEdgePoints(fromNode, toNode) {
357
+ const normalize = node => ({
358
+ x: node.x,
359
+ y: node.y,
360
+ w: node.width,
361
+ h: node.height,
362
+ cx: node.centerX,
363
+ cy: node.centerY,
364
+ type: node.type,
365
+ });
366
+ const from = normalize(fromNode);
367
+ const to = normalize(toNode);
368
+ return {
369
+ start: getEdgePoint(from, to),
370
+ end: getEdgePoint(to, from),
371
+ };
372
+ }
373
+
351
374
  function parseColor(hex) {
352
375
  if (!hex || hex === 'transparent' || hex === 'none') return null;
353
376
  let c = hex.replace('#', '');
@@ -432,6 +455,7 @@ export function renderFlowchartOnCanvas(diagram) {
432
455
  // instead of getting clipped at the corners.
433
456
  const PADDING = 90;
434
457
  const frameTitle = diagram.title || 'Mermaid diagram';
458
+ const undoBatch = window.beginUndoBatch?.();
435
459
  const frame = new window.Frame(
436
460
  vcx - dw / 2 - PADDING,
437
461
  vcy - dh / 2 - PADDING,
@@ -447,7 +471,7 @@ export function renderFlowchartOnCanvas(diagram) {
447
471
  );
448
472
  frame._diagramType = 'mermaid-flowchart';
449
473
  window.shapes.push(frame);
450
- if (window.pushCreateAction) window.pushCreateAction(frame);
474
+ if (window.pushCreateAction) window.pushCreateAction(frame, { frameCreation: true, containedShapes: [] });
451
475
 
452
476
  const nodeMap = new Map(); // id → shape
453
477
 
@@ -491,10 +515,10 @@ export function renderFlowchartOnCanvas(diagram) {
491
515
  if (!shape) continue;
492
516
 
493
517
  window.shapes.push(shape);
494
- if (window.pushCreateAction) window.pushCreateAction(shape);
495
518
  frame.addShapeToFrame(shape);
519
+ if (window.pushCreateAction) window.pushCreateAction(shape);
496
520
 
497
- nodeMap.set(n.id, { shape, x: nx, y: ny, width: nw, height: nh, centerX: cx, centerY: cy });
521
+ nodeMap.set(n.id, { shape, type: n.type, x: nx, y: ny, width: nw, height: nh, centerX: cx, centerY: cy });
498
522
  }
499
523
 
500
524
  // ── Edges ──────────────────────────────────────────────────────────
@@ -503,10 +527,10 @@ export function renderFlowchartOnCanvas(diagram) {
503
527
  const toNode = nodeMap.get(e.to);
504
528
  if (!fromNode || !toNode) continue;
505
529
 
506
- // Connect from the source center to the target center — autoAttach
507
- // will snap each endpoint to the appropriate edge of the shape.
508
- const sp = { x: fromNode.centerX, y: fromNode.centerY };
509
- const ep = { x: toNode.centerX, y: toNode.centerY };
530
+ // Start on the source side facing the target and finish on the
531
+ // reciprocal target side. For top-to-bottom diagrams this is the
532
+ // source's south centre and the target's north centre.
533
+ const { start: sp, end: ep } = getCanvasFlowchartEdgePoints(fromNode, toNode);
510
534
 
511
535
  const directed = e.directed !== false;
512
536
  const style = e.style || 'normal';
@@ -538,8 +562,8 @@ export function renderFlowchartOnCanvas(diagram) {
538
562
  if (!connector) continue;
539
563
 
540
564
  window.shapes.push(connector);
541
- if (window.pushCreateAction) window.pushCreateAction(connector);
542
565
  frame.addShapeToFrame(connector);
566
+ if (window.pushCreateAction) window.pushCreateAction(connector);
543
567
 
544
568
  // Wire arrow endpoints into the source/target shapes so moving a
545
569
  // node drags its connections along. window.__autoAttach is set up
@@ -563,5 +587,7 @@ export function renderFlowchartOnCanvas(diagram) {
563
587
  if (typeof first.shape.selectShape === 'function') first.shape.selectShape();
564
588
  }
565
589
 
590
+ window.endUndoBatch?.(undoBatch, 'mermaid-flowchart-create');
591
+
566
592
  return true;
567
593
  }
@@ -474,6 +474,7 @@ export function renderSequenceOnCanvas(diagram) {
474
474
  const PADDING = 60;
475
475
  const TK = themeColors();
476
476
  const frameTitle = diagram.title || 'Sequence diagram';
477
+ const undoBatch = window.beginUndoBatch?.();
477
478
  const frame = new window.Frame(
478
479
  ox - PADDING,
479
480
  oy - PADDING,
@@ -489,7 +490,7 @@ export function renderSequenceOnCanvas(diagram) {
489
490
  );
490
491
  frame._diagramType = 'mermaid-sequence';
491
492
  window.shapes.push(frame);
492
- if (window.pushCreateAction) window.pushCreateAction(frame);
493
+ if (window.pushCreateAction) window.pushCreateAction(frame, { frameCreation: true, containedShapes: [] });
493
494
 
494
495
  const created = [];
495
496
 
@@ -513,8 +514,8 @@ export function renderSequenceOnCanvas(diagram) {
513
514
  labelColor: TK.participantText,
514
515
  });
515
516
  window.shapes.push(topBox);
516
- if (window.pushCreateAction) window.pushCreateAction(topBox);
517
517
  frame.addShapeToFrame(topBox);
518
+ if (window.pushCreateAction) window.pushCreateAction(topBox);
518
519
  created.push(topBox);
519
520
 
520
521
  // Lifeline (dashed vertical line spanning the diagram height)
@@ -529,8 +530,8 @@ export function renderSequenceOnCanvas(diagram) {
529
530
  }
530
531
  );
531
532
  window.shapes.push(lifeline);
532
- if (window.pushCreateAction) window.pushCreateAction(lifeline);
533
533
  frame.addShapeToFrame(lifeline);
534
+ if (window.pushCreateAction) window.pushCreateAction(lifeline);
534
535
  created.push(lifeline);
535
536
 
536
537
  // Bottom participant box (mirrors top — Mermaid convention)
@@ -544,8 +545,8 @@ export function renderSequenceOnCanvas(diagram) {
544
545
  labelColor: TK.participantText,
545
546
  });
546
547
  window.shapes.push(bottomBox);
547
- if (window.pushCreateAction) window.pushCreateAction(bottomBox);
548
548
  frame.addShapeToFrame(bottomBox);
549
+ if (window.pushCreateAction) window.pushCreateAction(bottomBox);
549
550
  created.push(bottomBox);
550
551
  } catch (err) {
551
552
  console.warn('[SequenceRenderer] Participant creation failed:', p.name, err);
@@ -575,8 +576,8 @@ export function renderSequenceOnCanvas(diagram) {
575
576
  }
576
577
  );
577
578
  window.shapes.push(blockShape);
578
- if (window.pushCreateAction) window.pushCreateAction(blockShape);
579
579
  frame.addShapeToFrame(blockShape);
580
+ if (window.pushCreateAction) window.pushCreateAction(blockShape);
580
581
  created.push(blockShape);
581
582
  } catch (err) {
582
583
  console.warn('[SequenceRenderer] Block creation failed:', block, err);
@@ -626,8 +627,8 @@ export function renderSequenceOnCanvas(diagram) {
626
627
  ];
627
628
  segments.forEach((segment) => {
628
629
  window.shapes.push(segment);
629
- if (window.pushCreateAction) window.pushCreateAction(segment);
630
630
  frame.addShapeToFrame(segment);
631
+ if (window.pushCreateAction) window.pushCreateAction(segment);
631
632
  created.push(segment);
632
633
  });
633
634
  continue;
@@ -636,8 +637,8 @@ export function renderSequenceOnCanvas(diagram) {
636
637
  ? new window.Line(sp, ep, opts)
637
638
  : new window.Arrow(sp, ep, opts);
638
639
  window.shapes.push(connector);
639
- if (window.pushCreateAction) window.pushCreateAction(connector);
640
640
  frame.addShapeToFrame(connector);
641
+ if (window.pushCreateAction) window.pushCreateAction(connector);
641
642
  created.push(connector);
642
643
  } catch (err) {
643
644
  console.warn('[SequenceRenderer] Message creation failed:', m, err);
@@ -680,8 +681,8 @@ export function renderSequenceOnCanvas(diagram) {
680
681
  }
681
682
  );
682
683
  window.shapes.push(noteShape);
683
- if (window.pushCreateAction) window.pushCreateAction(noteShape);
684
684
  frame.addShapeToFrame(noteShape);
685
+ if (window.pushCreateAction) window.pushCreateAction(noteShape);
685
686
  created.push(noteShape);
686
687
  } catch (err) {
687
688
  console.warn('[SequenceRenderer] Note creation failed:', note, err);
@@ -698,6 +699,7 @@ export function renderSequenceOnCanvas(diagram) {
698
699
  if (typeof first.selectShape === 'function') first.selectShape();
699
700
  }
700
701
 
702
+ window.endUndoBatch?.(undoBatch, 'mermaid-sequence-create');
701
703
  console.log(`[SequenceRenderer] Done: ${pCount} participants, ${messages.length} messages`);
702
704
  return true;
703
705
  }