@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
@@ -359,117 +359,182 @@ export function renderFlowchartPreviewSVG(diagram) {
359
359
  }
360
360
 
361
361
  /**
362
- * Render a flowchart diagram onto the canvas inside a Frame.
362
+ * Render a flowchart diagram onto the canvas as a real engine `Frame`
363
+ * containing independent shapes — Rectangle nodes (with embedded labels)
364
+ * joined by Arrow edges. Each child is fully independent for click /
365
+ * drag / resize / colour-change, exactly like a user-drawn shape; the
366
+ * Frame's `_diagramType` marker means deleting it pulls every child with
367
+ * it (so the diagram behaves as one logical unit when you're done with
368
+ * it) — see Frame.destroy().
369
+ *
370
+ * Issue #34 bug #3 (follow-up to #22 phase 5): drops the shared `groupId`
371
+ * glue that made selecting one shape select the whole diagram; the Frame
372
+ * is the explicit container instead.
363
373
  */
364
374
  export function renderFlowchartOnCanvas(diagram) {
365
375
  if (!diagram || !diagram.nodes || diagram.nodes.length === 0) return false;
366
- if (!window.svg || !window.Frame) {
376
+ if (!window.svg || !window.Rectangle || !window.Frame) {
367
377
  console.error('[FlowchartRenderer] Engine not initialized');
368
378
  return false;
369
379
  }
370
380
 
371
- // Generate the SVG markup (no fixed size — use natural dimensions)
372
- const svgMarkup = renderFlowchartSVG(diagram);
373
- if (!svgMarkup) return false;
374
-
375
- // Parse SVG to get dimensions
376
- const parser = new DOMParser();
377
- const doc = parser.parseFromString(svgMarkup, 'image/svg+xml');
378
- const svgEl = doc.querySelector('svg');
379
- if (!svgEl) return false;
381
+ const nodes = diagram.nodes;
382
+ const edges = diagram.edges || [];
380
383
 
381
- const gWidth = parseFloat(svgEl.getAttribute('width'));
382
- const gHeight = parseFloat(svgEl.getAttribute('height'));
384
+ // Diagram bounds (in the renderer's natural coordinate space)
385
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
386
+ nodes.forEach((n) => {
387
+ const nw = n.width || 140;
388
+ const nh = n.height || 60;
389
+ minX = Math.min(minX, n.x);
390
+ minY = Math.min(minY, n.y);
391
+ maxX = Math.max(maxX, n.x + nw);
392
+ maxY = Math.max(maxY, n.y + nh);
393
+ });
394
+ const dw = (maxX - minX) || 1;
395
+ const dh = (maxY - minY) || 1;
383
396
 
384
- // Viewport center
397
+ // Center on the current viewport
385
398
  const vb = window.currentViewBox || { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight };
386
399
  const vcx = vb.x + vb.width / 2;
387
400
  const vcy = vb.y + vb.height / 2;
401
+ const ox = vcx - dw / 2 - minX;
402
+ const oy = vcy - dh / 2 - minY;
403
+
404
+ // ── Wrapper frame ──────────────────────────────────────────────────
405
+ // Created up-front so we can call addShapeToFrame as each child is
406
+ // built. _diagramType marks it so Frame.destroy() takes the children
407
+ // along on delete (issue #34 bug #3).
408
+ const PADDING = 40;
409
+ const frameTitle = diagram.title || 'Mermaid diagram';
410
+ const frame = new window.Frame(
411
+ vcx - dw / 2 - PADDING,
412
+ vcy - dh / 2 - PADDING,
413
+ dw + PADDING * 2,
414
+ dh + PADDING * 2,
415
+ {
416
+ stroke: '#888',
417
+ strokeWidth: 1,
418
+ fill: 'transparent',
419
+ opacity: 0.7,
420
+ frameName: frameTitle,
421
+ }
422
+ );
423
+ frame._diagramType = 'mermaid-flowchart';
424
+ window.shapes.push(frame);
425
+ if (window.pushCreateAction) window.pushCreateAction(frame);
426
+
427
+ const nodeMap = new Map(); // id → shape
428
+
429
+ // ── Nodes ──────────────────────────────────────────────────────────
430
+ for (const n of nodes) {
431
+ const nw = n.width || 140;
432
+ const nh = n.height || 60;
433
+ const nx = n.x + ox;
434
+ const ny = n.y + oy;
435
+ const cx = nx + nw / 2;
436
+ const cy = ny + nh / 2;
437
+
438
+ const opts = {
439
+ stroke: n.stroke || '#fff',
440
+ strokeWidth: n.strokeWidth ?? 1.5,
441
+ fill: n.fill || 'transparent',
442
+ fillStyle: n.fill && n.fill !== 'transparent' ? 'solid' : 'none',
443
+ roughness: 1,
444
+ label: n.label || '',
445
+ };
388
446
 
389
- const framePad = 30;
390
- const frameW = gWidth + framePad * 2;
391
- const frameH = gHeight + framePad * 2;
392
- const frameX = vcx - frameW / 2;
393
- const frameY = vcy - frameH / 2;
394
-
395
- const title = diagram.title || 'Flowchart';
447
+ let shape = null;
448
+ try {
449
+ if (n.type === 'circle' && window.Circle) {
450
+ shape = new window.Circle(cx, cy, nw / 2, nh / 2, opts);
451
+ } else if (n.type === 'diamond') {
452
+ const sz = Math.max(nw, nh) * 0.75;
453
+ shape = new window.Rectangle(cx - sz / 2, cy - sz / 2, sz, sz, opts);
454
+ shape.rotation = 45;
455
+ if (typeof shape.draw === 'function') shape.draw();
456
+ } else if (n.type === 'roundrect') {
457
+ shape = new window.Rectangle(nx, ny, nw, nh, { ...opts, cornerRadius: Math.min(nw, nh) * 0.2 });
458
+ } else {
459
+ shape = new window.Rectangle(nx, ny, nw, nh, opts);
460
+ }
461
+ } catch (err) {
462
+ console.warn('[FlowchartRenderer] Node creation failed:', n.id, err);
463
+ continue;
464
+ }
465
+ if (!shape) continue;
396
466
 
397
- // Create frame
398
- let frame;
399
- try {
400
- frame = new window.Frame(frameX, frameY, frameW, frameH, {
401
- stroke: '#888', strokeWidth: 2, fill: 'transparent', opacity: 1,
402
- frameName: title,
403
- });
404
- frame._diagramType = 'flowchart';
405
- frame._diagramData = diagram;
467
+ window.shapes.push(shape);
468
+ if (window.pushCreateAction) window.pushCreateAction(shape);
469
+ frame.addShapeToFrame(shape);
406
470
 
407
- window.shapes.push(frame);
408
- if (window.pushCreateAction) window.pushCreateAction(frame);
409
- } catch (err) {
410
- console.error('[FlowchartRenderer] Frame creation failed:', err);
411
- return false;
471
+ nodeMap.set(n.id, { shape, x: nx, y: ny, width: nw, height: nh, centerX: cx, centerY: cy });
412
472
  }
413
473
 
414
- // Insert SVG content into the canvas
415
- const NS = 'http://www.w3.org/2000/svg';
416
- try {
417
- const graphGroup = document.createElementNS(NS, 'g');
418
- graphGroup.setAttribute('data-type', 'flowchart-diagram');
419
- graphGroup.setAttribute('transform', `translate(${frameX + framePad}, ${frameY + framePad})`);
420
-
421
- // Copy defs
422
- const defs = svgEl.querySelector('defs');
423
- if (defs) {
424
- let mainDefs = window.svg.querySelector('defs');
425
- if (!mainDefs) {
426
- mainDefs = document.createElementNS(NS, 'defs');
427
- window.svg.insertBefore(mainDefs, window.svg.firstChild);
428
- }
429
- while (defs.firstChild) {
430
- mainDefs.appendChild(defs.firstChild);
431
- }
432
- }
474
+ // ── Edges ──────────────────────────────────────────────────────────
475
+ for (const e of edges) {
476
+ const fromNode = nodeMap.get(e.from);
477
+ const toNode = nodeMap.get(e.to);
478
+ if (!fromNode || !toNode) continue;
433
479
 
434
- // Copy all child elements (skip defs)
435
- while (svgEl.childNodes.length > 0) {
436
- const child = svgEl.childNodes[0];
437
- if (child.nodeName === 'defs') {
438
- svgEl.removeChild(child);
439
- continue;
440
- }
441
- graphGroup.appendChild(child);
442
- }
480
+ // Connect from the source center to the target center — autoAttach
481
+ // will snap each endpoint to the appropriate edge of the shape.
482
+ const sp = { x: fromNode.centerX, y: fromNode.centerY };
483
+ const ep = { x: toNode.centerX, y: toNode.centerY };
443
484
 
444
- window.svg.appendChild(graphGroup);
445
-
446
- // Wrap as a shape-like object for the frame
447
- const fcShape = {
448
- shapeName: 'flowchartContent',
449
- group: graphGroup,
450
- element: graphGroup,
451
- x: frameX + framePad,
452
- y: frameY + framePad,
453
- width: gWidth,
454
- height: gHeight,
455
- move(dx, dy) {
456
- this.x += dx;
457
- this.y += dy;
458
- this.group.setAttribute('transform', `translate(${this.x}, ${this.y})`);
459
- },
460
- updateAttachedArrows() {},
485
+ const directed = e.directed !== false;
486
+ const style = e.style || 'normal';
487
+ const isThick = style === 'thick';
488
+ const isDotted = style === 'dotted';
489
+
490
+ const opts = {
491
+ stroke: e.stroke || '#fff',
492
+ strokeWidth: isThick ? 3 : 1.5,
493
+ roughness: 1,
494
+ strokeDasharray: isDotted ? '5 3' : '',
495
+ label: e.label || '',
461
496
  };
462
497
 
463
- window.shapes.push(fcShape);
464
- if (frame.addShapeToFrame) frame.addShapeToFrame(fcShape);
465
- } catch (err) {
466
- console.error('[FlowchartRenderer] SVG insertion failed:', err);
498
+ let connector = null;
499
+ try {
500
+ if (directed && window.Arrow) {
501
+ connector = new window.Arrow(sp, ep, opts);
502
+ } else if (window.Line) {
503
+ connector = new window.Line(sp, ep, opts);
504
+ } else if (window.Arrow) {
505
+ connector = new window.Arrow(sp, ep, opts);
506
+ }
507
+ } catch (err) {
508
+ console.warn('[FlowchartRenderer] Edge creation failed:', e, err);
509
+ continue;
510
+ }
511
+ if (!connector) continue;
512
+
513
+ window.shapes.push(connector);
514
+ if (window.pushCreateAction) window.pushCreateAction(connector);
515
+ frame.addShapeToFrame(connector);
516
+
517
+ // Wire arrow endpoints into the source/target shapes so moving a
518
+ // node drags its connections along. window.__autoAttach is set up
519
+ // by AIRenderer.initAIRenderer() during engine init.
520
+ if (directed && connector.shapeName === 'arrow' && typeof window.__autoAttach === 'function') {
521
+ try {
522
+ window.__autoAttach(connector, fromNode.shape, true, sp);
523
+ window.__autoAttach(connector, toNode.shape, false, ep);
524
+ } catch (err) {
525
+ console.warn('[FlowchartRenderer] autoAttach failed:', err);
526
+ }
527
+ }
467
528
  }
468
529
 
469
- // Select the frame
470
- window.currentShape = frame;
471
- if (frame.selectFrame) frame.selectFrame();
472
- if (window.__sketchStoreApi) window.__sketchStoreApi.setSelectedShapeSidebar('frame');
530
+ // Select the first node so the user has feedback that something
531
+ // landed. Selecting a child (not the frame) reinforces the
532
+ // "independent shapes inside a frame" model.
533
+ const first = nodeMap.values().next().value;
534
+ if (first) {
535
+ window.currentShape = first.shape;
536
+ if (typeof first.shape.selectShape === 'function') first.shape.selectShape();
537
+ }
473
538
 
474
539
  return true;
475
540
  }
@@ -361,119 +361,194 @@ export function parseAndRenderSequence(src) {
361
361
  }
362
362
 
363
363
  /**
364
- * Render a sequence diagram onto the canvas inside a Frame.
365
- * Creates real SVG groups with data attributes so individual elements
366
- * can be selected and edited.
364
+ * Render a sequence diagram onto the canvas as a real engine `Frame`
365
+ * containing independent shapes top + bottom participant boxes,
366
+ * dashed lifelines, and a real Arrow (or Line for `--x`/`-x`) per
367
+ * message. Each child is fully independent for click / drag / resize;
368
+ * the Frame's `_diagramType` marker makes Frame.destroy() pull the
369
+ * children along on delete, so the diagram still behaves as one
370
+ * logical unit when discarded.
371
+ *
372
+ * Issue #34 bug #3 (follow-up to #24 per-actor split): drops the shared
373
+ * `groupId` glue so clicking one shape selects only that shape.
374
+ *
375
+ * Notes and block-frames (alt/opt/loop) are skipped for now — they'd
376
+ * either need their own shape types or a richer label model. Self-
377
+ * messages are also skipped (would need a curved arrow).
367
378
  */
368
379
  export function renderSequenceOnCanvas(diagram) {
369
380
  if (!diagram || diagram.type !== 'sequenceDiagram') return false;
370
- if (!window.svg || !window.Frame) {
371
- console.error('[SequenceRenderer] Engine not initialized');
381
+ if (!window.svg || !window.Rectangle || !window.Line || !window.Arrow) {
382
+ console.error('[SequenceRenderer] Engine not initialized (Rectangle / Line / Arrow missing)');
372
383
  return false;
373
384
  }
374
385
 
375
- // Generate the SVG markup
376
- const svgMarkup = renderSequenceSVG(diagram);
377
- if (!svgMarkup) return false;
386
+ const participants = diagram.participants || [];
387
+ const messages = diagram.messages || [];
388
+ if (participants.length === 0) return false;
378
389
 
379
- // Parse SVG to get dimensions
380
- const parser = new DOMParser();
381
- const doc = parser.parseFromString(svgMarkup, 'image/svg+xml');
382
- const svgEl = doc.querySelector('svg');
383
- if (!svgEl) return false;
390
+ // Mirror the layout math from renderSequenceSVG so the canvas layout
391
+ // matches the modal preview.
392
+ const pCount = participants.length;
393
+ const contentWidth = (pCount - 1) * PARTICIPANT_GAP + PARTICIPANT_W;
394
+ const totalWidth = Math.max(contentWidth + SIDE_MARGIN * 2, 400);
395
+ const startX = (totalWidth - contentWidth) / 2;
396
+ const pCenters = participants.map((_, i) => startX + i * PARTICIPANT_GAP + PARTICIPANT_W / 2);
384
397
 
385
- const gWidth = parseFloat(svgEl.getAttribute('width'));
386
- const gHeight = parseFloat(svgEl.getAttribute('height'));
398
+ const topBoxBottom = TOP_MARGIN + PARTICIPANT_H;
399
+ const msgYPositions = [];
400
+ let currentY = topBoxBottom + 30;
401
+ for (let mi = 0; mi < messages.length; mi++) {
402
+ msgYPositions.push(currentY);
403
+ currentY += MSG_ROW_HEIGHT;
404
+ }
405
+ const bottomBoxTop = currentY + 20;
406
+ const totalHeight = bottomBoxTop + PARTICIPANT_H + BOTTOM_MARGIN;
387
407
 
388
- // Viewport center
408
+ // Centre the diagram on the current viewport so the user sees it
409
+ // right where they invoked the renderer.
389
410
  const vb = window.currentViewBox || { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight };
390
- const vcx = vb.x + vb.width / 2;
391
- const vcy = vb.y + vb.height / 2;
392
-
393
- const framePad = 30;
394
- const frameW = gWidth + framePad * 2;
395
- const frameH = gHeight + framePad * 2;
396
- const frameX = vcx - frameW / 2;
397
- const frameY = vcy - frameH / 2;
398
-
399
- const title = diagram.title || 'Sequence Diagram';
400
-
401
- // Create frame
402
- let frame;
403
- try {
404
- frame = new window.Frame(frameX, frameY, frameW, frameH, {
405
- stroke: '#888', strokeWidth: 2, fill: 'transparent', opacity: 1,
406
- frameName: title,
407
- });
408
- frame._diagramType = 'sequence';
409
- frame._diagramData = diagram;
410
-
411
- window.shapes.push(frame);
412
- if (window.pushCreateAction) window.pushCreateAction(frame);
413
- } catch (err) {
414
- console.error('[SequenceRenderer] Frame creation failed:', err);
411
+ const ox = vb.x + vb.width / 2 - totalWidth / 2;
412
+ const oy = vb.y + vb.height / 2 - totalHeight / 2;
413
+
414
+ // ── Wrapper frame ──────────────────────────────────────────────────
415
+ // Issue #34 bug #3: drop the shared groupId glue. Each child is
416
+ // independent for click / drag / resize. The Frame's `_diagramType`
417
+ // marker makes Frame.destroy() pull the children along on delete,
418
+ // so the diagram still behaves as one logical unit when discarded.
419
+ if (!window.Frame) {
420
+ console.error('[SequenceRenderer] window.Frame missing cannot wrap diagram');
415
421
  return false;
416
422
  }
417
-
418
- // Insert SVG content into the canvas
419
- const NS = 'http://www.w3.org/2000/svg';
420
- try {
421
- const graphGroup = document.createElementNS(NS, 'g');
422
- graphGroup.setAttribute('data-type', 'sequence-diagram');
423
- graphGroup.setAttribute('transform', `translate(${frameX + framePad}, ${frameY + framePad})`);
424
-
425
- // Copy defs
426
- const defs = svgEl.querySelector('defs');
427
- if (defs) {
428
- let mainDefs = window.svg.querySelector('defs');
429
- if (!mainDefs) {
430
- mainDefs = document.createElementNS(NS, 'defs');
431
- window.svg.insertBefore(mainDefs, window.svg.firstChild);
432
- }
433
- while (defs.firstChild) {
434
- mainDefs.appendChild(defs.firstChild);
435
- }
423
+ const PADDING = 24;
424
+ const frameTitle = diagram.title || 'Sequence diagram';
425
+ const frame = new window.Frame(
426
+ ox - PADDING,
427
+ oy - PADDING,
428
+ totalWidth + PADDING * 2,
429
+ totalHeight + PADDING * 2,
430
+ {
431
+ stroke: '#888',
432
+ strokeWidth: 1,
433
+ fill: 'transparent',
434
+ opacity: 0.7,
435
+ frameName: frameTitle,
436
436
  }
437
+ );
438
+ frame._diagramType = 'mermaid-sequence';
439
+ window.shapes.push(frame);
440
+ if (window.pushCreateAction) window.pushCreateAction(frame);
437
441
 
438
- // Copy all child elements (skip defs)
439
- while (svgEl.childNodes.length > 0) {
440
- const child = svgEl.childNodes[0];
441
- if (child.nodeName === 'defs') {
442
- svgEl.removeChild(child);
443
- continue;
444
- }
445
- graphGroup.appendChild(child);
442
+ const created = [];
443
+
444
+ // ── Participants: top box + lifeline (and bottom box) ─────────────
445
+ const pIndex = new Map();
446
+ for (let pi = 0; pi < pCount; pi++) {
447
+ const p = participants[pi];
448
+ pIndex.set(p.name, pi);
449
+ const cx = pCenters[pi] + ox;
450
+ const bx = cx - PARTICIPANT_W / 2;
451
+
452
+ try {
453
+ // Top participant box
454
+ const topBox = new window.Rectangle(bx, TOP_MARGIN + oy, PARTICIPANT_W, PARTICIPANT_H, {
455
+ stroke: THEME.participantBorder,
456
+ strokeWidth: 1.5,
457
+ fill: THEME.participantBg,
458
+ fillStyle: 'solid',
459
+ roughness: 1,
460
+ label: p.name,
461
+ });
462
+ window.shapes.push(topBox);
463
+ if (window.pushCreateAction) window.pushCreateAction(topBox);
464
+ frame.addShapeToFrame(topBox);
465
+ created.push(topBox);
466
+
467
+ // Lifeline (dashed vertical line spanning the diagram height)
468
+ const lifeline = new window.Line(
469
+ { x: cx, y: topBoxBottom + oy },
470
+ { x: cx, y: bottomBoxTop + oy },
471
+ {
472
+ stroke: THEME.lifeline,
473
+ strokeWidth: 1,
474
+ strokeDasharray: '6 4',
475
+ roughness: 0,
476
+ }
477
+ );
478
+ window.shapes.push(lifeline);
479
+ if (window.pushCreateAction) window.pushCreateAction(lifeline);
480
+ frame.addShapeToFrame(lifeline);
481
+ created.push(lifeline);
482
+
483
+ // Bottom participant box (mirrors top — Mermaid convention)
484
+ const bottomBox = new window.Rectangle(bx, bottomBoxTop + oy, PARTICIPANT_W, PARTICIPANT_H, {
485
+ stroke: THEME.participantBorder,
486
+ strokeWidth: 1.5,
487
+ fill: THEME.participantBg,
488
+ fillStyle: 'solid',
489
+ roughness: 1,
490
+ label: p.name,
491
+ });
492
+ window.shapes.push(bottomBox);
493
+ if (window.pushCreateAction) window.pushCreateAction(bottomBox);
494
+ frame.addShapeToFrame(bottomBox);
495
+ created.push(bottomBox);
496
+ } catch (err) {
497
+ console.warn('[SequenceRenderer] Participant creation failed:', p.name, err);
446
498
  }
499
+ }
447
500
 
448
- window.svg.appendChild(graphGroup);
449
-
450
- // Wrap as a shape-like object for the frame
451
- const seqShape = {
452
- shapeName: 'sequenceContent',
453
- group: graphGroup,
454
- element: graphGroup,
455
- x: frameX + framePad,
456
- y: frameY + framePad,
457
- width: gWidth,
458
- height: gHeight,
459
- move(dx, dy) {
460
- this.x += dx;
461
- this.y += dy;
462
- this.group.setAttribute('transform', `translate(${this.x}, ${this.y})`);
463
- },
464
- updateAttachedArrows() {},
501
+ // ── Messages: arrow (or line for --x style) per row ───────────────
502
+ for (let mi = 0; mi < messages.length; mi++) {
503
+ const m = messages[mi];
504
+ const fromI = pIndex.get(m.from);
505
+ const toI = pIndex.get(m.to);
506
+ if (fromI == null || toI == null) continue;
507
+ if (fromI === toI) continue; // skip self-messages (v1 limitation)
508
+
509
+ const fromCx = pCenters[fromI] + ox;
510
+ const toCx = pCenters[toI] + ox;
511
+ const y = msgYPositions[mi] + oy;
512
+
513
+ const labelText = m.number ? `${m.number}. ${m.text}` : m.text;
514
+
515
+ // Solid vs dashed (sync vs async response). `cross` style (-x)
516
+ // would render as a line with an X at the head — fall back to a
517
+ // line for that, arrow otherwise.
518
+ const isCross = !!m.cross && m.arrowHead === 'cross';
519
+ const opts = {
520
+ stroke: THEME.messageLine,
521
+ strokeWidth: 1.5,
522
+ roughness: 0,
523
+ strokeDasharray: m.solid ? '' : '6 4',
524
+ label: labelText || '',
465
525
  };
466
526
 
467
- window.shapes.push(seqShape);
468
- if (frame.addShapeToFrame) frame.addShapeToFrame(seqShape);
469
- } catch (err) {
470
- console.error('[SequenceRenderer] SVG insertion failed:', err);
527
+ try {
528
+ const sp = { x: fromCx, y };
529
+ const ep = { x: toCx, y };
530
+ const connector = isCross
531
+ ? new window.Line(sp, ep, opts)
532
+ : new window.Arrow(sp, ep, opts);
533
+ window.shapes.push(connector);
534
+ if (window.pushCreateAction) window.pushCreateAction(connector);
535
+ frame.addShapeToFrame(connector);
536
+ created.push(connector);
537
+ } catch (err) {
538
+ console.warn('[SequenceRenderer] Message creation failed:', m, err);
539
+ }
471
540
  }
472
541
 
473
- // Select the frame
474
- window.currentShape = frame;
475
- if (frame.selectFrame) frame.selectFrame();
476
- if (window.__sketchStoreApi) window.__sketchStoreApi.setSelectedShapeSidebar('frame');
542
+ // Auto-select the first node so the user has feedback that the
543
+ // diagram landed. Selecting a child (not the frame) reinforces the
544
+ // "independent shapes inside a frame" model from issue #34 bug #3.
545
+ const first = created[0];
546
+ if (first) {
547
+ window.currentShape = first;
548
+ if (typeof first.selectShape === 'function') first.selectShape();
549
+ }
477
550
 
551
+ console.log(`[SequenceRenderer] Done: ${pCount} participants, ${messages.length} messages`);
478
552
  return true;
479
553
  }
554
+