@elixpo/lixsketch 5.4.7 → 5.5.0

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 (218) hide show
  1. package/dist/react/AIRenderer-M4LMJ66J.js +1105 -0
  2. package/dist/react/AIRenderer-M4LMJ66J.js.map +7 -0
  3. package/dist/react/Arrow-BJDAXPMH.js +8 -0
  4. package/dist/react/Arrow-BJDAXPMH.js.map +7 -0
  5. package/dist/react/Circle-KPG7TKIX.js +8 -0
  6. package/dist/react/Circle-KPG7TKIX.js.map +7 -0
  7. package/dist/react/CodeShape-K4DIZAIV.js +8 -0
  8. package/dist/react/CodeShape-K4DIZAIV.js.map +7 -0
  9. package/dist/react/CopyPaste-4CKJRHJ3.js +569 -0
  10. package/dist/react/CopyPaste-4CKJRHJ3.js.map +7 -0
  11. package/dist/react/EraserTrail-PA6IQLQN.js +20 -0
  12. package/dist/react/EraserTrail-PA6IQLQN.js.map +7 -0
  13. package/dist/react/EventDispatcher-SRFH6LQZ.js +463 -0
  14. package/dist/react/EventDispatcher-SRFH6LQZ.js.map +7 -0
  15. package/dist/react/Frame-6UR5XPAX.js +10 -0
  16. package/dist/react/Frame-6UR5XPAX.js.map +7 -0
  17. package/dist/react/FreehandStroke-CK24NUT3.js +8 -0
  18. package/dist/react/FreehandStroke-CK24NUT3.js.map +7 -0
  19. package/dist/react/GraphEngine-QE5B2PMR.js +340 -0
  20. package/dist/react/GraphEngine-QE5B2PMR.js.map +7 -0
  21. package/dist/react/GraphMathParser-2WMTY4IE.js +10 -0
  22. package/dist/react/GraphMathParser-2WMTY4IE.js.map +7 -0
  23. package/dist/react/IconShape-KMIA6T2H.js +10 -0
  24. package/dist/react/IconShape-KMIA6T2H.js.map +7 -0
  25. package/dist/react/ImageShape-CHHGBXUQ.js +10 -0
  26. package/dist/react/ImageShape-CHHGBXUQ.js.map +7 -0
  27. package/dist/react/LayerOrder-XCHF4MSS.js +66 -0
  28. package/dist/react/LayerOrder-XCHF4MSS.js.map +7 -0
  29. package/dist/react/Line-2KOQEEGA.js +10 -0
  30. package/dist/react/Line-2KOQEEGA.js.map +7 -0
  31. package/dist/react/LixScriptParser-JYWJX375.js +1033 -0
  32. package/dist/react/LixScriptParser-JYWJX375.js.map +7 -0
  33. package/dist/react/MermaidFlowchartRenderer-4QBAZ5MB.js +412 -0
  34. package/dist/react/MermaidFlowchartRenderer-4QBAZ5MB.js.map +7 -0
  35. package/dist/react/MermaidSequenceParser-UKG2NSZY.js +8 -0
  36. package/dist/react/MermaidSequenceParser-UKG2NSZY.js.map +7 -0
  37. package/dist/react/MermaidSequenceRenderer-HPLHY5II.js +387 -0
  38. package/dist/react/MermaidSequenceRenderer-HPLHY5II.js.map +7 -0
  39. package/dist/react/Rectangle-6FE7RN6W.js +8 -0
  40. package/dist/react/Rectangle-6FE7RN6W.js.map +7 -0
  41. package/dist/react/ResizeCode-WAMDTKSW.js +2 -0
  42. package/dist/react/ResizeCode-WAMDTKSW.js.map +7 -0
  43. package/dist/react/ResizeShapes-LKEQTITP.js +250 -0
  44. package/dist/react/ResizeShapes-LKEQTITP.js.map +7 -0
  45. package/dist/react/SceneSerializer-4U4B24JM.js +739 -0
  46. package/dist/react/SceneSerializer-4U4B24JM.js.map +7 -0
  47. package/dist/react/Selection-VVUHY5YJ.js +34 -0
  48. package/dist/react/Selection-VVUHY5YJ.js.map +7 -0
  49. package/dist/react/SketchEngine-YA4RG3KX.js +413 -0
  50. package/dist/react/SketchEngine-YA4RG3KX.js.map +7 -0
  51. package/dist/react/TextShape-54ZJ66E3.js +8 -0
  52. package/dist/react/TextShape-54ZJ66E3.js.map +7 -0
  53. package/dist/react/UndoRedo-M2JZUBKN.js +30 -0
  54. package/dist/react/UndoRedo-M2JZUBKN.js.map +7 -0
  55. package/dist/react/ZoomPan-U3TXC5AJ.js +186 -0
  56. package/dist/react/ZoomPan-U3TXC5AJ.js.map +7 -0
  57. package/dist/react/arrowTool-CWEMIRXK.js +17 -0
  58. package/dist/react/arrowTool-CWEMIRXK.js.map +7 -0
  59. package/dist/react/boxicons-5JNJQPUP.woff +0 -0
  60. package/dist/react/boxicons-6NVWQSCQ.eot +0 -0
  61. package/dist/react/boxicons-AQ5N6WP7.woff2 +0 -0
  62. package/dist/react/boxicons-GDR4JU3X.ttf +0 -0
  63. package/dist/react/boxicons-NNFHLMOR.svg +1660 -0
  64. package/dist/react/chunk-2OQXI7QB.js +564 -0
  65. package/dist/react/chunk-2OQXI7QB.js.map +7 -0
  66. package/dist/react/chunk-5OUBTHHO.js +187 -0
  67. package/dist/react/chunk-5OUBTHHO.js.map +7 -0
  68. package/dist/react/chunk-6PFXZKL2.js +628 -0
  69. package/dist/react/chunk-6PFXZKL2.js.map +7 -0
  70. package/dist/react/chunk-6SFNWGGZ.js +453 -0
  71. package/dist/react/chunk-6SFNWGGZ.js.map +7 -0
  72. package/dist/react/chunk-7TZSIR6V.js +210 -0
  73. package/dist/react/chunk-7TZSIR6V.js.map +7 -0
  74. package/dist/react/chunk-AJAZC37E.js +662 -0
  75. package/dist/react/chunk-AJAZC37E.js.map +7 -0
  76. package/dist/react/chunk-BU4D2WYQ.js +1213 -0
  77. package/dist/react/chunk-BU4D2WYQ.js.map +7 -0
  78. package/dist/react/chunk-BZE4CEBG.js +747 -0
  79. package/dist/react/chunk-BZE4CEBG.js.map +7 -0
  80. package/dist/react/chunk-CB2RANCB.js +212 -0
  81. package/dist/react/chunk-CB2RANCB.js.map +7 -0
  82. package/dist/react/chunk-DNAG7ML6.js +128 -0
  83. package/dist/react/chunk-DNAG7ML6.js.map +7 -0
  84. package/dist/react/chunk-HZWLPXFN.js +198 -0
  85. package/dist/react/chunk-HZWLPXFN.js.map +7 -0
  86. package/dist/react/chunk-I6HCCIXB.js +572 -0
  87. package/dist/react/chunk-I6HCCIXB.js.map +7 -0
  88. package/dist/react/chunk-IZ5OGWGW.js +235 -0
  89. package/dist/react/chunk-IZ5OGWGW.js.map +7 -0
  90. package/dist/react/chunk-JJCJNLNN.js +1490 -0
  91. package/dist/react/chunk-JJCJNLNN.js.map +7 -0
  92. package/dist/react/chunk-JQ3IRFDP.js +222 -0
  93. package/dist/react/chunk-JQ3IRFDP.js.map +7 -0
  94. package/dist/react/chunk-L5OARZJ7.js +1134 -0
  95. package/dist/react/chunk-L5OARZJ7.js.map +7 -0
  96. package/dist/react/chunk-LHRTXNVE.js +1600 -0
  97. package/dist/react/chunk-LHRTXNVE.js.map +7 -0
  98. package/dist/react/chunk-Q2GA4MBZ.js +1653 -0
  99. package/dist/react/chunk-Q2GA4MBZ.js.map +7 -0
  100. package/dist/react/chunk-QNSE5BAY.js +519 -0
  101. package/dist/react/chunk-QNSE5BAY.js.map +7 -0
  102. package/dist/react/chunk-QXFBG7OM.js +191 -0
  103. package/dist/react/chunk-QXFBG7OM.js.map +7 -0
  104. package/dist/react/chunk-T6ZVV5ZO.js +714 -0
  105. package/dist/react/chunk-T6ZVV5ZO.js.map +7 -0
  106. package/dist/react/chunk-URB6MY46.js +974 -0
  107. package/dist/react/chunk-URB6MY46.js.map +7 -0
  108. package/dist/react/chunk-VN62KLOC.js +931 -0
  109. package/dist/react/chunk-VN62KLOC.js.map +7 -0
  110. package/dist/react/chunk-VQVEPJSA.js +158 -0
  111. package/dist/react/chunk-VQVEPJSA.js.map +7 -0
  112. package/dist/react/chunk-XISEAGBV.js +1632 -0
  113. package/dist/react/chunk-XISEAGBV.js.map +7 -0
  114. package/dist/react/chunk-YKJUBFHE.js +575 -0
  115. package/dist/react/chunk-YKJUBFHE.js.map +7 -0
  116. package/dist/react/circleTool-NUATSEI2.js +14 -0
  117. package/dist/react/circleTool-NUATSEI2.js.map +7 -0
  118. package/dist/react/codeTool-MHCB2FNB.js +38 -0
  119. package/dist/react/codeTool-MHCB2FNB.js.map +7 -0
  120. package/dist/react/eraserTool-2GS2LQCS.js +103 -0
  121. package/dist/react/eraserTool-2GS2LQCS.js.map +7 -0
  122. package/dist/react/frameTool-PWVKKXSP.js +14 -0
  123. package/dist/react/frameTool-PWVKKXSP.js.map +7 -0
  124. package/dist/react/freehandTool-SLYGASVP.js +14 -0
  125. package/dist/react/freehandTool-SLYGASVP.js.map +7 -0
  126. package/dist/react/iconTool-GPZ7O7NH.js +18 -0
  127. package/dist/react/iconTool-GPZ7O7NH.js.map +7 -0
  128. package/dist/react/imageTool-D5R4XGEU.js +15 -0
  129. package/dist/react/imageTool-D5R4XGEU.js.map +7 -0
  130. package/dist/react/index.js +5342 -0
  131. package/dist/react/index.js.map +7 -0
  132. package/dist/react/laserTool-XVMV63AV.js +239 -0
  133. package/dist/react/laserTool-XVMV63AV.js.map +7 -0
  134. package/dist/react/lineTool-WNI5EMZ5.js +14 -0
  135. package/dist/react/lineTool-WNI5EMZ5.js.map +7 -0
  136. package/dist/react/lixCode-OJMVUXXI.ttf +0 -0
  137. package/dist/react/lixDefault-JEAPAUJL.ttf +0 -0
  138. package/dist/react/lixFancy-BPXYM5CQ.ttf +0 -0
  139. package/dist/react/lixFont-CPKEUDVM.woff2 +0 -0
  140. package/dist/react/rectangleTool-U7DZ6ZTA.js +14 -0
  141. package/dist/react/rectangleTool-U7DZ6ZTA.js.map +7 -0
  142. package/dist/react/styles.css +6627 -0
  143. package/dist/react/textTool-M7S6F3VC.js +21 -0
  144. package/dist/react/textTool-M7S6F3VC.js.map +7 -0
  145. package/package.json +39 -5
  146. package/src/EngineShortcuts.js +83 -27
  147. package/src/core/AIRenderer.js +30 -18
  148. package/src/core/GraphEngine.js +64 -41
  149. package/src/core/MermaidFlowchartRenderer.js +153 -88
  150. package/src/core/MermaidSequenceRenderer.js +168 -93
  151. package/src/core/SceneSerializer.js +86 -2
  152. package/src/core/Selection.js +119 -19
  153. package/src/core/UndoRedo.js +82 -15
  154. package/src/core/ZoomPan.js +15 -5
  155. package/src/index.js +14 -1
  156. package/src/react/LixSketchCanvas.jsx +347 -0
  157. package/src/react/components/AppMenu.jsx +475 -0
  158. package/src/react/components/Footer.jsx +85 -0
  159. package/src/react/components/Toolbar.jsx +140 -0
  160. package/src/react/components/canvas/CanvasLoadingOverlay.jsx +76 -0
  161. package/src/react/components/canvas/ContextMenu.jsx +466 -0
  162. package/src/react/components/canvas/FindBar.jsx +177 -0
  163. package/src/react/components/canvas/ImageSourcePicker.jsx +133 -0
  164. package/src/react/components/canvas/MultiSelectActions.jsx +120 -0
  165. package/src/react/components/canvas/SVGCanvas.jsx +145 -0
  166. package/src/react/components/canvas/SaveStatusIndicator.jsx +29 -0
  167. package/src/react/components/modals/CanvasPropertiesModal.jsx +358 -0
  168. package/src/react/components/modals/CommandPalette.jsx +307 -0
  169. package/src/react/components/modals/ExportImageModal.jsx +287 -0
  170. package/src/react/components/modals/GraphModal.jsx +174 -0
  171. package/src/react/components/modals/HelpModal.jsx +208 -0
  172. package/src/react/components/modals/LixScriptModal.jsx +130 -0
  173. package/src/react/components/modals/ShortcutsModal.jsx +125 -0
  174. package/src/react/components/sidebars/ArrowSidebar.jsx +173 -0
  175. package/src/react/components/sidebars/CircleSidebar.jsx +134 -0
  176. package/src/react/components/sidebars/FrameSidebar.jsx +263 -0
  177. package/src/react/components/sidebars/IconSidebar.jsx +279 -0
  178. package/src/react/components/sidebars/ImageSidebar.jsx +69 -0
  179. package/src/react/components/sidebars/LineSidebar.jsx +109 -0
  180. package/src/react/components/sidebars/PaintbrushSidebar.jsx +119 -0
  181. package/src/react/components/sidebars/RectangleSidebar.jsx +165 -0
  182. package/src/react/components/sidebars/ShapeSidebar.jsx +130 -0
  183. package/src/react/components/sidebars/TextSidebar.jsx +174 -0
  184. package/src/react/hooks/inertStores.js +58 -0
  185. package/src/react/hooks/useSketchEngine.js +76 -0
  186. package/src/react/hooks/useTranslation.js +92 -0
  187. package/src/react/index.js +41 -0
  188. package/src/react/store/useSketchStore.js +234 -0
  189. package/src/react/store/useUIStore.js +196 -0
  190. package/src/react/styles/arrowSidebar.css +263 -0
  191. package/src/react/styles/canvasStroke.css +47 -0
  192. package/src/react/styles/canvasTools.css +769 -0
  193. package/src/react/styles/circleSidebar.css +206 -0
  194. package/src/react/styles/fonts/fonts.css +29 -0
  195. package/src/react/styles/fonts/lixCode.ttf +0 -0
  196. package/src/react/styles/fonts/lixDefault.ttf +0 -0
  197. package/src/react/styles/fonts/lixDocs.ttf +0 -0
  198. package/src/react/styles/fonts/lixFancy.ttf +0 -0
  199. package/src/react/styles/fonts/lixFont.woff2 +0 -0
  200. package/src/react/styles/lineSidebar.css +207 -0
  201. package/src/react/styles/paintbrushSidebar.css +247 -0
  202. package/src/react/styles/squareSidebar.css +206 -0
  203. package/src/react/styles/writeText.css +149 -0
  204. package/src/react/styles.css +175 -0
  205. package/src/react/toolsEnum.js +20 -0
  206. package/src/react/utils/toast.js +18 -0
  207. package/src/shapes/Frame.js +7 -1
  208. package/src/shapes/Line.js +11 -5
  209. package/src/tools/arrowTool.js +30 -4
  210. package/src/tools/circleTool.js +33 -9
  211. package/src/tools/codeTool.js +5 -3
  212. package/src/tools/frameTool.js +47 -7
  213. package/src/tools/freehandTool.js +12 -4
  214. package/src/tools/iconTool.js +7 -1
  215. package/src/tools/imageTool.js +7 -1
  216. package/src/tools/lineTool.js +27 -3
  217. package/src/tools/rectangleTool.js +45 -8
  218. package/src/tools/textTool.js +23 -6
@@ -122,8 +122,14 @@ function serializeShape(shape) {
122
122
  gridSize: shape.gridSize || 20,
123
123
  gridColor: shape.gridColor || 'rgba(255,255,255,0.06)',
124
124
  options: cloneOptions(shape.options),
125
+ // Issue #24 bug #10: filter out null / undefined / shapeID-less
126
+ // children. A stale reference would serialize as `null` and the
127
+ // load path would skip it (logging a misleading "missing shape"
128
+ // warning) — but also previously skewed the order tracking.
125
129
  containedShapeIDs: shape.containedShapes
126
- ? Array.from(shape.containedShapes).map(s => s.shapeID)
130
+ ? Array.from(shape.containedShapes)
131
+ .filter(s => s && s.shapeID)
132
+ .map(s => s.shapeID)
127
133
  : [],
128
134
  };
129
135
 
@@ -312,9 +318,31 @@ export function saveScene(workspaceName = 'Untitled') {
312
318
  const allShapes = window.shapes || [];
313
319
  const serialized = [];
314
320
 
321
+ // Issue #24 bug #10: rebuild containment from each shape's own
322
+ // `parentFrame` ref before serialising. The `containedShapes` array
323
+ // on the frame side drifts over time (eraser, partial deletes, drag
324
+ // out + drag back, etc.), and a missing entry there means the child
325
+ // gets serialised as a top-level shape and the frame restores empty.
326
+ // Each shape knows its own parent, so we trust THAT and override the
327
+ // frame's array at save time.
328
+ const childrenByFrame = new Map();
329
+ for (const s of allShapes) {
330
+ if (s && s.parentFrame && s.shapeID && s !== s.parentFrame) {
331
+ const arr = childrenByFrame.get(s.parentFrame.shapeID) || [];
332
+ arr.push(s.shapeID);
333
+ childrenByFrame.set(s.parentFrame.shapeID, arr);
334
+ }
335
+ }
336
+
315
337
  for (const shape of allShapes) {
316
338
  const data = serializeShape(shape);
317
- if (data) serialized.push(data);
339
+ if (!data) continue;
340
+ if (data.type === 'frame' && shape.shapeID && childrenByFrame.has(shape.shapeID)) {
341
+ const merged = new Set(data.containedShapeIDs || []);
342
+ for (const id of childrenByFrame.get(shape.shapeID)) merged.add(id);
343
+ data.containedShapeIDs = Array.from(merged);
344
+ }
345
+ serialized.push(data);
318
346
  }
319
347
 
320
348
  const scene = {
@@ -444,6 +472,62 @@ export function loadScene(sceneData) {
444
472
  }
445
473
  }
446
474
 
475
+ // ── Fourth pass: frame containment reconciliation (issue #24 bug #10) ──
476
+ //
477
+ // The pass above attaches each child via `frame.addShapeToFrame(child)`,
478
+ // which is supposed to move the child's <g> into the frame's clipGroup.
479
+ // In practice LixScript-rendered scenes were reloading with empty
480
+ // frames — children present in the shapes array, frame box drawn, but
481
+ // the children's <g> elements stranded at the svg root (so visually
482
+ // they look orphaned). The cause varies (transient `isBeingMovedByFrame`
483
+ // flag carried over from save time, ordering glitches when nested
484
+ // frames are restored, etc.). Rather than chase the source, repair the
485
+ // DOM state here from the authoritative `containedShapes` array.
486
+ for (const data of frameData) {
487
+ const frame = idMap.get(data.shapeID);
488
+ if (!frame) continue;
489
+
490
+ // Clear stale flags that suppress DOM mutation inside addShapeToFrame.
491
+ delete frame.isBeingMovedByFrame;
492
+ delete frame.isDraggedOutTemporarily;
493
+
494
+ // Ensure the frame's own clipGroup is direct child of svg — sub-frames
495
+ // that didn't get re-parented yet are handled in the next loop.
496
+ if (frame.clipGroup && frame.clipGroup.parentNode !== svgEl) {
497
+ svgEl.appendChild(frame.clipGroup);
498
+ }
499
+
500
+ for (const child of frame.containedShapes || []) {
501
+ if (!child) continue;
502
+ delete child.isBeingMovedByFrame;
503
+ delete child.isDraggedOutTemporarily;
504
+
505
+ const el = child.group || child.element;
506
+ if (el && el.parentNode !== frame.clipGroup) {
507
+ if (el.parentNode) el.parentNode.removeChild(el);
508
+ frame.clipGroup.appendChild(el);
509
+ }
510
+ // Sub-frames also carry their own clipGroup — nest it.
511
+ if (child.shapeName === 'frame' && child.clipGroup
512
+ && child.clipGroup.parentNode !== frame.clipGroup) {
513
+ if (child.clipGroup.parentNode) child.clipGroup.parentNode.removeChild(child.clipGroup);
514
+ frame.clipGroup.appendChild(child.clipGroup);
515
+ }
516
+ // Authoritative parent ref — `addShapeToFrame` should already
517
+ // have set this but the no-op early return when the child was
518
+ // already in containedShapes skips it.
519
+ child.parentFrame = frame;
520
+ }
521
+
522
+ // Force the clip rect + visual to match. Some saved frames came
523
+ // through with a stale clipRect from before the resize that lost
524
+ // the children visually.
525
+ if (typeof frame.updateClipPath === 'function') frame.updateClipPath();
526
+ if (typeof frame.draw === 'function') {
527
+ try { frame.draw(); } catch (err) { console.warn('[SceneSerializer] frame redraw failed:', err); }
528
+ }
529
+ }
530
+
447
531
  // Restore viewport
448
532
  if (sceneData.viewport && svgEl) {
449
533
  const vb = sceneData.viewport;
@@ -2,7 +2,7 @@
2
2
  // Multi-selection system - copied from selection.js
3
3
 
4
4
  import { cleanupAttachments } from '../tools/arrowTool.js';
5
- import { pushTransformAction } from './UndoRedo.js';
5
+ import { pushTransformAction, pushFrameAttachmentAction, pushDeleteAction } from './UndoRedo.js';
6
6
  import { calculateSnap, clearSnapGuides } from './SnapGuides.js';
7
7
 
8
8
  let isMultiSelecting = false;
@@ -1370,6 +1370,41 @@ createRotatedControls(angleDiff = 0) {
1370
1370
  shape.isSelected = true;
1371
1371
  });
1372
1372
 
1373
+ // Re-parent any selected shape that now sits inside a different frame
1374
+ // (Issue #22, bug #9). The per-tool drag handlers (rectangleTool, etc.)
1375
+ // do this for single-shape drags, but a multi-selection drag bypassed
1376
+ // them and left the shape geometrically inside the frame yet logically
1377
+ // outside its child list.
1378
+ if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
1379
+ this.selectedShapes.forEach(shape => {
1380
+ if (!shape || shape.shapeName === 'frame') return;
1381
+ // Find the deepest frame whose bounds contain the shape after the drag.
1382
+ let target = null;
1383
+ for (let i = shapes.length - 1; i >= 0; i--) {
1384
+ const f = shapes[i];
1385
+ if (f.shapeName !== 'frame') continue;
1386
+ if (this.selectedShapes.has(f)) continue; // moving with us — skip
1387
+ if (typeof f.isShapeInFrame === 'function' && f.isShapeInFrame(shape)) {
1388
+ target = f;
1389
+ break;
1390
+ }
1391
+ }
1392
+ const current = shape.parentFrame || null;
1393
+ if (target === current) return;
1394
+ if (current && typeof current.removeShapeFromFrame === 'function') {
1395
+ current.removeShapeFromFrame(shape);
1396
+ }
1397
+ if (target && typeof target.addShapeToFrame === 'function') {
1398
+ target.addShapeToFrame(shape);
1399
+ if (typeof pushFrameAttachmentAction === 'function') {
1400
+ pushFrameAttachmentAction(target, shape, 'attach', current);
1401
+ }
1402
+ } else if (current && typeof pushFrameAttachmentAction === 'function') {
1403
+ pushFrameAttachmentAction(current, shape, 'detach', current);
1404
+ }
1405
+ });
1406
+ }
1407
+
1373
1408
  this.initialPositions.clear();
1374
1409
 
1375
1410
  // Push undo for all moved shapes
@@ -1788,33 +1823,98 @@ function deleteSelectedShapes() {
1788
1823
  const shapesToDelete = Array.from(multiSelection.selectedShapes);
1789
1824
  multiSelection.clearSelection();
1790
1825
 
1791
- shapesToDelete.forEach(shape => {
1792
- // Remove from DOM
1793
- if (shape.group && shape.group.parentNode) {
1794
- shape.group.parentNode.removeChild(shape.group);
1795
- } else if (shape.element && shape.element.parentNode) {
1796
- shape.element.parentNode.removeChild(shape.element);
1797
- }
1826
+ // Issue #24 bug #3: when deleting a (regular, non-diagram) frame, its
1827
+ // children are released back to the canvas — capture them here so we
1828
+ // can re-select them and drop the user into the pointer tool. Diagram
1829
+ // frames destroy their children together, so we skip those.
1830
+ const releasedChildren = [];
1798
1831
 
1799
- // Remove from parent frame if any
1800
- if (shape.parentFrame && typeof shape.parentFrame.removeShapeFromFrame === 'function') {
1801
- shape.parentFrame.removeShapeFromFrame(shape);
1802
- }
1832
+ shapesToDelete.forEach(shape => {
1833
+ const isFrame = shape.shapeName === 'frame';
1834
+ const isDiagramFrame = isFrame && !!shape._diagramType;
1835
+
1836
+ // Issue #24 bug #7: for a regular (non-diagram) frame, we don't
1837
+ // want destroy() — it empties containedShapes and tears down the
1838
+ // clipGroup, leaving nothing for undo to rehydrate. Instead:
1839
+ // 1. snapshot the children references (for the undo action),
1840
+ // 2. release each child visually back to the svg root,
1841
+ // 3. detach the frame's <g>, clipGroup, clipPath from DOM.
1842
+ // The frame JS object stays alive in the undo action's `shape`
1843
+ // reference; on undo, the UndoRedo branch re-appends clipGroup +
1844
+ // clipPath and re-attaches each child via addShapeToFrame.
1845
+ let childSnapshot = null;
1846
+ if (isFrame && !isDiagramFrame) {
1847
+ childSnapshot = [...(shape.containedShapes || [])];
1848
+ for (const child of childSnapshot) {
1849
+ if (!child || child === shape) continue;
1850
+ const el = child.group || child.element;
1851
+ if (el) {
1852
+ if (shape.clipGroup && el.parentNode === shape.clipGroup) {
1853
+ shape.clipGroup.removeChild(el);
1854
+ }
1855
+ if (window.svg && el.parentNode !== window.svg) window.svg.appendChild(el);
1856
+ }
1857
+ child.parentFrame = null;
1858
+ delete child.isBeingMovedByFrame;
1859
+ releasedChildren.push(child);
1860
+ }
1803
1861
 
1804
- // Cleanup arrow attachments
1805
- cleanupAttachments(shape);
1862
+ // Clear containedShapes — undo will repopulate via
1863
+ // addShapeToFrame, which short-circuits on an existing entry.
1864
+ shape.containedShapes = [];
1806
1865
 
1807
- // Destroy frame and release children
1808
- if (shape.shapeName === 'frame' && typeof shape.destroy === 'function') {
1809
- shape.destroy();
1866
+ // Detach the frame's own DOM bits — but keep the JS object
1867
+ // and its clipGroup/clipPath references intact for undo.
1868
+ if (shape.group && shape.group.parentNode) shape.group.parentNode.removeChild(shape.group);
1869
+ if (shape.clipGroup && shape.clipGroup.parentNode) shape.clipGroup.parentNode.removeChild(shape.clipGroup);
1870
+ if (shape.clipPath && shape.clipPath.parentNode) shape.clipPath.parentNode.removeChild(shape.clipPath);
1871
+ } else {
1872
+ // Non-frame OR diagram frame: standard delete path.
1873
+ if (shape.group && shape.group.parentNode) {
1874
+ shape.group.parentNode.removeChild(shape.group);
1875
+ } else if (shape.element && shape.element.parentNode) {
1876
+ shape.element.parentNode.removeChild(shape.element);
1877
+ }
1878
+ if (shape.parentFrame && typeof shape.parentFrame.removeShapeFromFrame === 'function') {
1879
+ shape.parentFrame.removeShapeFromFrame(shape);
1880
+ }
1881
+ cleanupAttachments(shape);
1882
+ if (isDiagramFrame && typeof shape.destroy === 'function') {
1883
+ shape.destroy();
1884
+ }
1810
1885
  }
1811
1886
 
1812
1887
  // Remove from shapes array
1813
1888
  const idx = shapes.indexOf(shape);
1814
- if (idx !== -1) {
1815
- shapes.splice(idx, 1);
1889
+ if (idx !== -1) shapes.splice(idx, 1);
1890
+
1891
+ // Issue #24 bug #7: record the delete on the undo stack. Without
1892
+ // this the user can't undo a multi-selection delete at all —
1893
+ // the action just disappears. For frames we attach the child
1894
+ // snapshot so undo can re-attach them.
1895
+ try {
1896
+ if (childSnapshot) {
1897
+ pushDeleteAction(shape, { childSnapshot });
1898
+ } else {
1899
+ pushDeleteAction(shape);
1900
+ }
1901
+ } catch (err) {
1902
+ console.warn('[deleteSelectedShapes] pushDeleteAction failed:', err);
1816
1903
  }
1817
1904
  });
1905
+
1906
+ // Bug #3 follow-through: auto-select the released children and switch
1907
+ // to the pointer tool so the user can immediately keep manipulating
1908
+ // them. Skip if nothing was released (no frames deleted).
1909
+ if (releasedChildren.length > 0) {
1910
+ for (const child of releasedChildren) {
1911
+ if (shapes.indexOf(child) === -1) continue;
1912
+ multiSelection.addShape(child);
1913
+ }
1914
+ if (window.__sketchStoreApi && typeof window.__sketchStoreApi.setActiveTool === 'function') {
1915
+ window.__sketchStoreApi.setActiveTool('select');
1916
+ }
1917
+ }
1818
1918
  }
1819
1919
 
1820
1920
  /**
@@ -38,12 +38,15 @@ export function pushCreateAction(shape) {
38
38
  redoStack.length = 0;
39
39
  }
40
40
 
41
- export function pushDeleteAction(shape) {
42
- undoStack.push({
43
- type: 'delete',
44
- shape: shape
45
- });
46
-
41
+ export function pushDeleteAction(shape, meta = null) {
42
+ // Issue #24 bug #7: when the deleted shape is a frame, the caller may
43
+ // pass `meta.childSnapshot` — the contained shape REFERENCES at the
44
+ // time of delete. The undo branch uses this to re-attach the children
45
+ // when the frame is restored, instead of leaving an empty box.
46
+ const action = { type: 'delete', shape };
47
+ if (meta && meta.childSnapshot) action.childSnapshot = meta.childSnapshot;
48
+ undoStack.push(action);
49
+
47
50
  // Clear redo stack when new action is performed
48
51
  redoStack.length = 0;
49
52
  }
@@ -419,10 +422,22 @@ export function pushOptionsChangeAction(shape, oldOptions) {
419
422
  redoStack.length = 0;
420
423
  }
421
424
 
425
+ // Issue #34 bug #4: refresh the multi-selection outline + handles so it
426
+ // follows the reverted geometry. Without this the selection rect stays
427
+ // pinned to the pre-undo coordinates and the user sees a ghost rect
428
+ // floating away from the shapes it claims to wrap.
429
+ function refreshSelectionAfterAction() {
430
+ const ms = (typeof window !== 'undefined') ? window.multiSelection : null;
431
+ if (!ms) return;
432
+ if (typeof ms.updateControls !== 'function') return;
433
+ if (!ms.selectedShapes || ms.selectedShapes.size === 0) return;
434
+ try { ms.updateControls(); } catch (err) { console.warn('[UndoRedo] selection refresh failed:', err); }
435
+ }
436
+
422
437
  export function undo() {
423
438
  if (undoStack.length === 0) return;
424
439
  const action = undoStack.pop();
425
-
440
+ try {
426
441
  if (action.type === 'frameTransform') {
427
442
  // Handle frame transformation undo
428
443
  action.shape.x = action.oldPos.x;
@@ -604,6 +619,33 @@ export function undo() {
604
619
  if (svg) {
605
620
  svg.appendChild(action.shape.group);
606
621
  }
622
+
623
+ // Issue #24 bug #7: rehydrate frame state. The delete path
624
+ // orphaned the frame (removed <g>, clipGroup, clipPath from
625
+ // DOM) but kept the JS object intact. Re-attach clipGroup +
626
+ // clipPath, then re-parent every child from the snapshot via
627
+ // addShapeToFrame. Skip children that were subsequently
628
+ // deleted (they'll get re-added when their own delete action
629
+ // is undone in turn).
630
+ if (action.shape.shapeName === 'frame' && svg) {
631
+ if (action.shape.clipGroup && !action.shape.clipGroup.parentNode) {
632
+ svg.appendChild(action.shape.clipGroup);
633
+ }
634
+ if (action.shape.clipPath && !action.shape.clipPath.parentNode) {
635
+ const defs = svg.querySelector('defs');
636
+ if (defs) defs.appendChild(action.shape.clipPath);
637
+ else svg.insertBefore(action.shape.clipPath, svg.firstChild);
638
+ }
639
+ if (action.childSnapshot && Array.isArray(action.childSnapshot)) {
640
+ for (const child of action.childSnapshot) {
641
+ if (!child) continue;
642
+ if (shapes.indexOf(child) === -1) continue;
643
+ if (typeof action.shape.addShapeToFrame === 'function') {
644
+ action.shape.addShapeToFrame(child);
645
+ }
646
+ }
647
+ }
648
+ }
607
649
  }
608
650
  redoStack.push(action);
609
651
  return;
@@ -875,12 +917,13 @@ export function undo() {
875
917
  redoStack.push(action);
876
918
  return;
877
919
  }
920
+ } finally { refreshSelectionAfterAction(); }
878
921
  }
879
922
 
880
923
  export function redo() {
881
924
  if (redoStack.length === 0) return;
882
925
  const action = redoStack.pop();
883
-
926
+ try {
884
927
  if (action.type === 'frameTransform') {
885
928
  // Handle frame transformation redo
886
929
  action.shape.x = action.newPos.x;
@@ -1053,13 +1096,36 @@ export function redo() {
1053
1096
  // Handle other shape deletion redo
1054
1097
  const idx = shapes.indexOf(action.shape);
1055
1098
  if (idx !== -1) shapes.splice(idx, 1);
1056
- if (action.shape.group && action.shape.group.parentNode) {
1057
- action.shape.group.parentNode.removeChild(action.shape.group);
1058
- }
1059
-
1060
- // Handle frame cleanup
1061
- if (action.shape.shapeName === 'frame') {
1062
- action.shape.destroy();
1099
+
1100
+ // Issue #24 bug #7: for a regular frame, re-orphan instead of
1101
+ // destroy() — releases children visually, detaches the frame's
1102
+ // own DOM bits, but keeps the JS object and child references
1103
+ // intact so a subsequent undo can rehydrate again.
1104
+ if (action.shape.shapeName === 'frame' && !action.shape._diagramType && action.childSnapshot) {
1105
+ for (const child of action.childSnapshot) {
1106
+ if (!child) continue;
1107
+ const el = child.group || child.element;
1108
+ if (el && svg) {
1109
+ if (action.shape.clipGroup && el.parentNode === action.shape.clipGroup) {
1110
+ action.shape.clipGroup.removeChild(el);
1111
+ }
1112
+ if (el.parentNode !== svg) svg.appendChild(el);
1113
+ }
1114
+ child.parentFrame = null;
1115
+ delete child.isBeingMovedByFrame;
1116
+ }
1117
+ action.shape.containedShapes = [];
1118
+ if (action.shape.group && action.shape.group.parentNode) action.shape.group.parentNode.removeChild(action.shape.group);
1119
+ if (action.shape.clipGroup && action.shape.clipGroup.parentNode) action.shape.clipGroup.parentNode.removeChild(action.shape.clipGroup);
1120
+ if (action.shape.clipPath && action.shape.clipPath.parentNode) action.shape.clipPath.parentNode.removeChild(action.shape.clipPath);
1121
+ } else {
1122
+ if (action.shape.group && action.shape.group.parentNode) {
1123
+ action.shape.group.parentNode.removeChild(action.shape.group);
1124
+ }
1125
+ // Diagram frame OR no snapshot → fall back to destroy().
1126
+ if (action.shape.shapeName === 'frame' && typeof action.shape.destroy === 'function') {
1127
+ action.shape.destroy();
1128
+ }
1063
1129
  }
1064
1130
  }
1065
1131
  undoStack.push(action);
@@ -1329,6 +1395,7 @@ export function redo() {
1329
1395
  undoStack.push(action);
1330
1396
  return;
1331
1397
  }
1398
+ } finally { refreshSelectionAfterAction(); }
1332
1399
  }
1333
1400
 
1334
1401
  // Optional: Keyboard shortcuts
@@ -74,14 +74,24 @@ window.zoomFromCenter = function(direction) {
74
74
  };
75
75
 
76
76
  window.zoomReset = function() {
77
+ // Issue #24 bug #5: reset zoom in place around the current viewport
78
+ // centre — do NOT recenter to (0, 0). Recentering used to make whatever
79
+ // the user had on screen vanish until they panned back to it.
80
+ const cx = currentViewBox.x + currentViewBox.width / 2;
81
+ const cy = currentViewBox.y + currentViewBox.height / 2;
82
+
77
83
  currentZoom = 1;
78
- currentViewBox.x = 0;
79
- currentViewBox.y = 0;
80
- currentViewBox.width = window.innerWidth;
81
- currentViewBox.height = window.innerHeight;
84
+ const rect = freehandCanvas.getBoundingClientRect();
85
+ const vbW = rect.width || window.innerWidth;
86
+ const vbH = rect.height || window.innerHeight;
87
+
88
+ currentViewBox.x = cx - vbW / 2;
89
+ currentViewBox.y = cy - vbH / 2;
90
+ currentViewBox.width = vbW;
91
+ currentViewBox.height = vbH;
82
92
  freehandCanvas.setAttribute(
83
93
  "viewBox",
84
- `0 0 ${window.innerWidth} ${window.innerHeight}`
94
+ `${currentViewBox.x} ${currentViewBox.y} ${vbW} ${vbH}`
85
95
  );
86
96
  updateZoomDisplay();
87
97
  if (window.__sketchStoreApi && window.__sketchStoreApi.setZoom) {
package/src/index.js CHANGED
@@ -47,7 +47,20 @@ export function createSketchEngine(svgElement, options = {}) {
47
47
  // need to round-trip a scene through their own storage. saveScene reads from
48
48
  // window.shapes (populated by an active engine); loadScene rebuilds shapes onto
49
49
  // the active engine's SVG. Both must be called after engine.init() completes.
50
- export { saveScene, loadScene } from './core/SceneSerializer.js';
50
+ // Scene serializer is intentionally NOT re-exported here.
51
+ //
52
+ // SceneSerializer.js statically imports every shape module, and the shape
53
+ // modules reference bare `rough` / `svg` globals that only exist after
54
+ // engine.init() runs. Re-exporting `saveScene`/`loadScene` at this module
55
+ // scope makes any `import { ... } from '@elixpo/lixsketch'` evaluate the
56
+ // shape graph eagerly and crash with "rough is not defined".
57
+ //
58
+ // Hosts that need scene serialization should import the subpath directly,
59
+ // AFTER mounting the engine:
60
+ // const { saveScene, loadScene } = await import(
61
+ // '@elixpo/lixsketch/src/core/SceneSerializer.js'
62
+ // );
63
+ // or use window.__sceneSerializer which engine.init() exposes.
51
64
 
52
65
  // Adaptive image compressor — same one the engine uses internally for
53
66
  // uploads. Exported so embedded hosts can pre-compress images before