@elixpo/lixsketch 5.5.10 → 5.5.12
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.
- package/dist/react/{AIRenderer-46WIFF2B.js → AIRenderer-PWWLWI6U.js} +84 -16
- package/dist/react/AIRenderer-PWWLWI6U.js.map +7 -0
- package/dist/react/{MermaidFlowchartRenderer-A26B2SM3.js → MermaidFlowchartRenderer-4YEMAYQC.js} +18 -3
- package/dist/react/MermaidFlowchartRenderer-4YEMAYQC.js.map +7 -0
- package/dist/react/{MermaidSequenceRenderer-OF4JK77D.js → MermaidSequenceRenderer-KCUHO7UI.js} +106 -4
- package/dist/react/MermaidSequenceRenderer-KCUHO7UI.js.map +7 -0
- package/dist/react/MermaidStructuredRenderer-GGOOGON5.js +365 -0
- package/dist/react/MermaidStructuredRenderer-GGOOGON5.js.map +7 -0
- package/dist/react/{SketchEngine-BCRJ62CV.js → SketchEngine-NPF2XN6Z.js} +2 -2
- package/dist/react/index.js +61 -127
- package/dist/react/index.js.map +3 -3
- package/package.json +1 -1
- package/src/core/AIRenderer.js +95 -11
- package/src/core/MermaidFlowchartRenderer.js +18 -3
- package/src/core/MermaidSequenceRenderer.js +116 -7
- package/src/core/MermaidStructuredRenderer.js +363 -0
- package/src/react/LixSketchCanvas.jsx +1 -1
- package/src/react/components/canvas/FindBar.jsx +1 -1
- package/src/react/components/modals/CanvasPropertiesModal.jsx +1 -1
- package/src/react/components/modals/LixScriptModal.jsx +22 -96
- package/src/react/components/sidebars/ArrowSidebar.jsx +6 -6
- package/src/react/components/sidebars/CircleSidebar.jsx +3 -3
- package/src/react/components/sidebars/FrameSidebar.jsx +1 -1
- package/src/react/components/sidebars/LineSidebar.jsx +4 -4
- package/src/react/components/sidebars/PaintbrushSidebar.jsx +5 -5
- package/src/react/components/sidebars/RectangleSidebar.jsx +4 -4
- package/src/react/components/sidebars/TextSidebar.jsx +5 -5
- package/src/react/store/useUIStore.js +1 -1
- package/dist/react/AIRenderer-46WIFF2B.js.map +0 -7
- package/dist/react/MermaidFlowchartRenderer-A26B2SM3.js.map +0 -7
- package/dist/react/MermaidSequenceRenderer-OF4JK77D.js.map +0 -7
- /package/dist/react/{SketchEngine-BCRJ62CV.js.map → SketchEngine-NPF2XN6Z.js.map} +0 -0
package/package.json
CHANGED
package/src/core/AIRenderer.js
CHANGED
|
@@ -43,6 +43,30 @@ export function parseMermaid(src) {
|
|
|
43
43
|
const edges = [];
|
|
44
44
|
const classDefs = new Map(); // classDef name -> { fill, stroke, strokeWidth }
|
|
45
45
|
const classAssigns = []; // { nodeIds: [...], className }
|
|
46
|
+
const nodeStyles = new Map(); // Mermaid `style A fill:...,stroke:...`
|
|
47
|
+
const palette = [
|
|
48
|
+
{ fill: '#e8f3ec', stroke: '#5f836c' },
|
|
49
|
+
{ fill: '#f6eadf', stroke: '#a97852' },
|
|
50
|
+
{ fill: '#eee9f4', stroke: '#7e6b91' },
|
|
51
|
+
{ fill: '#f5efcf', stroke: '#9a8745' },
|
|
52
|
+
{ fill: '#e5f0f1', stroke: '#5d7f82' },
|
|
53
|
+
{ fill: '#f4e5e3', stroke: '#9a6863' },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
function parseStyleProps(value) {
|
|
57
|
+
const props = {};
|
|
58
|
+
for (const part of value.split(',')) {
|
|
59
|
+
const separator = part.indexOf(':');
|
|
60
|
+
if (separator < 0) continue;
|
|
61
|
+
const key = part.slice(0, separator).trim().toLowerCase();
|
|
62
|
+
const val = part.slice(separator + 1).trim();
|
|
63
|
+
if (key === 'fill') props.fill = val;
|
|
64
|
+
else if (key === 'stroke') props.stroke = val;
|
|
65
|
+
else if (key === 'stroke-width') props.strokeWidth = parseFloat(val);
|
|
66
|
+
else if (key === 'color') props.labelColor = val;
|
|
67
|
+
}
|
|
68
|
+
return props;
|
|
69
|
+
}
|
|
46
70
|
|
|
47
71
|
// Clean <br/> and <br> tags in labels → newline marker
|
|
48
72
|
function cleanLabel(label) {
|
|
@@ -91,18 +115,24 @@ export function parseMermaid(src) {
|
|
|
91
115
|
const classDefMatch = line.match(/^classDef\s+(\w+)\s+(.+)$/i);
|
|
92
116
|
if (classDefMatch) {
|
|
93
117
|
const name = classDefMatch[1];
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
118
|
+
classDefs.set(name, parseStyleProps(classDefMatch[2].replace(/;$/, '')));
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Direct node styling must be consumed before node/edge parsing.
|
|
123
|
+
// Previously these lines fell through to `parseNodeRef`, producing
|
|
124
|
+
// visible boxes containing the raw `style A fill:...` source.
|
|
125
|
+
const styleMatch = line.match(/^style\s+([\w,-]+)\s+(.+)$/i);
|
|
126
|
+
if (styleMatch) {
|
|
127
|
+
const props = parseStyleProps(styleMatch[2].replace(/;$/, ''));
|
|
128
|
+
styleMatch[1].split(',').map(id => id.trim()).filter(Boolean)
|
|
129
|
+
.forEach(id => nodeStyles.set(id, props));
|
|
103
130
|
continue;
|
|
104
131
|
}
|
|
105
132
|
|
|
133
|
+
// Presentation-only directives do not represent diagram nodes.
|
|
134
|
+
if (/^(linkStyle|click)\b/i.test(line)) continue;
|
|
135
|
+
|
|
106
136
|
// class assignment: class sq,e green
|
|
107
137
|
const classMatch = line.match(/^class\s+(.+?)\s+(\w+)$/i);
|
|
108
138
|
if (classMatch) {
|
|
@@ -214,6 +244,12 @@ export function parseMermaid(src) {
|
|
|
214
244
|
}
|
|
215
245
|
}
|
|
216
246
|
|
|
247
|
+
|
|
248
|
+
for (const [nid, style] of nodeStyles) {
|
|
249
|
+
const node = nodesMap.get(nid);
|
|
250
|
+
if (node) Object.assign(node, style);
|
|
251
|
+
}
|
|
252
|
+
|
|
217
253
|
// Topological BFS layering
|
|
218
254
|
const nodeIds = Array.from(nodesMap.keys());
|
|
219
255
|
const children = new Map();
|
|
@@ -247,6 +283,7 @@ export function parseMermaid(src) {
|
|
|
247
283
|
|
|
248
284
|
// Compute dynamic node sizes based on label length
|
|
249
285
|
const nodes = [];
|
|
286
|
+
let paletteIndex = 0;
|
|
250
287
|
Array.from(layerGroups.keys()).sort((a, b) => a - b).forEach((layerIdx, li) => {
|
|
251
288
|
const group = layerGroups.get(layerIdx);
|
|
252
289
|
const startOffset = -(group.length * H_SPACING) / 2 + H_SPACING / 2;
|
|
@@ -262,8 +299,12 @@ export function parseMermaid(src) {
|
|
|
262
299
|
nodes.push({
|
|
263
300
|
id: nd.id, type: nd.type, label: nd.label,
|
|
264
301
|
x, y, width: nw, height: nh,
|
|
265
|
-
fill: nd.fill
|
|
302
|
+
fill: nd.fill || palette[paletteIndex % palette.length].fill,
|
|
303
|
+
stroke: nd.stroke || palette[paletteIndex % palette.length].stroke,
|
|
304
|
+
strokeWidth: nd.strokeWidth,
|
|
305
|
+
labelColor: nd.labelColor,
|
|
266
306
|
});
|
|
307
|
+
paletteIndex += 1;
|
|
267
308
|
});
|
|
268
309
|
});
|
|
269
310
|
|
|
@@ -1327,6 +1368,9 @@ export function initAIRenderer() {
|
|
|
1327
1368
|
let _fcPreview = null;
|
|
1328
1369
|
let _fcCanvas = null;
|
|
1329
1370
|
|
|
1371
|
+
// Lazy-load ER / chart renderer
|
|
1372
|
+
let _structured = null;
|
|
1373
|
+
|
|
1330
1374
|
async function loadSequenceRenderer() {
|
|
1331
1375
|
if (_seqParser) return;
|
|
1332
1376
|
const mod = await import('./MermaidSequenceParser.js');
|
|
@@ -1343,9 +1387,25 @@ export function initAIRenderer() {
|
|
|
1343
1387
|
_fcCanvas = mod.renderFlowchartOnCanvas;
|
|
1344
1388
|
}
|
|
1345
1389
|
|
|
1390
|
+
async function loadStructuredRenderer() {
|
|
1391
|
+
if (_structured) return;
|
|
1392
|
+
_structured = await import('./MermaidStructuredRenderer.js');
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function mermaidHeader(src) {
|
|
1396
|
+
return src.split('\n').map(line => line.trim()).find(line => line && !line.startsWith('%%'))?.toLowerCase() || '';
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1346
1399
|
// Detect if source is a sequence diagram
|
|
1347
1400
|
function isSequenceDiagram(src) {
|
|
1348
|
-
return src
|
|
1401
|
+
return mermaidHeader(src) === 'sequencediagram';
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function structuredType(src) {
|
|
1405
|
+
const header = mermaidHeader(src);
|
|
1406
|
+
if (header === 'erdiagram') return 'er';
|
|
1407
|
+
if (/^pie(?:\s|$)/.test(header) || /^xychart(?:-beta)?(?:\s|$)/.test(header)) return 'chart';
|
|
1408
|
+
return null;
|
|
1349
1409
|
}
|
|
1350
1410
|
|
|
1351
1411
|
window.__aiRenderer = renderAIDiagram;
|
|
@@ -1363,6 +1423,15 @@ export function initAIRenderer() {
|
|
|
1363
1423
|
return null;
|
|
1364
1424
|
}
|
|
1365
1425
|
}
|
|
1426
|
+
if (structuredType(src)) {
|
|
1427
|
+
if (!_structured) {
|
|
1428
|
+
loadStructuredRenderer();
|
|
1429
|
+
return { _pendingStructured: true, src };
|
|
1430
|
+
}
|
|
1431
|
+
return structuredType(src) === 'er'
|
|
1432
|
+
? _structured.parseERDiagram(src)
|
|
1433
|
+
: _structured.parseChartDiagram(src);
|
|
1434
|
+
}
|
|
1366
1435
|
return parseMermaid(src);
|
|
1367
1436
|
};
|
|
1368
1437
|
|
|
@@ -1374,6 +1443,13 @@ export function initAIRenderer() {
|
|
|
1374
1443
|
if (!diagram) return '';
|
|
1375
1444
|
return _seqPreview(diagram);
|
|
1376
1445
|
}
|
|
1446
|
+
const type = structuredType(src);
|
|
1447
|
+
if (type) {
|
|
1448
|
+
await loadStructuredRenderer();
|
|
1449
|
+
const diagram = type === 'er' ? _structured.parseERDiagram(src) : _structured.parseChartDiagram(src);
|
|
1450
|
+
if (!diagram) return '';
|
|
1451
|
+
return type === 'er' ? _structured.renderERPreviewSVG(diagram) : _structured.renderChartPreviewSVG(diagram);
|
|
1452
|
+
}
|
|
1377
1453
|
// Flowchart: use unified flowchart renderer
|
|
1378
1454
|
await loadFlowchartRenderer();
|
|
1379
1455
|
const diagram = parseMermaid(src);
|
|
@@ -1389,6 +1465,13 @@ export function initAIRenderer() {
|
|
|
1389
1465
|
if (!diagram) { console.error('[AIRenderer] Sequence parse failed'); return false; }
|
|
1390
1466
|
return _seqCanvas(diagram);
|
|
1391
1467
|
}
|
|
1468
|
+
const type = structuredType(src);
|
|
1469
|
+
if (type) {
|
|
1470
|
+
await loadStructuredRenderer();
|
|
1471
|
+
const diagram = type === 'er' ? _structured.parseERDiagram(src) : _structured.parseChartDiagram(src);
|
|
1472
|
+
if (!diagram) { console.error('[AIRenderer] Structured Mermaid parse failed'); return false; }
|
|
1473
|
+
return type === 'er' ? _structured.renderEROnCanvas(diagram) : _structured.renderChartOnCanvas(diagram);
|
|
1474
|
+
}
|
|
1392
1475
|
// Flowchart: use unified flowchart renderer (same SVG as preview)
|
|
1393
1476
|
await loadFlowchartRenderer();
|
|
1394
1477
|
const diagram = parseMermaid(src);
|
|
@@ -1399,4 +1482,5 @@ export function initAIRenderer() {
|
|
|
1399
1482
|
// Pre-load renderers so they're ready
|
|
1400
1483
|
loadSequenceRenderer();
|
|
1401
1484
|
loadFlowchartRenderer();
|
|
1485
|
+
loadStructuredRenderer();
|
|
1402
1486
|
}
|
|
@@ -27,8 +27,7 @@ function isThemeDark() {
|
|
|
27
27
|
function nodeStrokeColor() { return isThemeDark() ? '#fff' : '#1a1a2e'; }
|
|
28
28
|
function edgeStrokeColor() { return isThemeDark() ? '#888' : '#444'; }
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
const THEME = {
|
|
30
|
+
const DARK_THEME = {
|
|
32
31
|
bg: '#1e1e28',
|
|
33
32
|
nodeBg: 'transparent',
|
|
34
33
|
nodeStroke: '#9090c0',
|
|
@@ -40,6 +39,20 @@ const THEME = {
|
|
|
40
39
|
subgraphLabel: '#888',
|
|
41
40
|
};
|
|
42
41
|
|
|
42
|
+
const LIGHT_THEME = {
|
|
43
|
+
bg: '#fbfaf6',
|
|
44
|
+
nodeBg: '#f5f2ea',
|
|
45
|
+
nodeStroke: '#6e746c',
|
|
46
|
+
nodeText: '#343832',
|
|
47
|
+
edgeStroke: '#6e746c',
|
|
48
|
+
edgeText: '#4f554e',
|
|
49
|
+
subgraphBg: 'rgba(117, 135, 119, 0.06)',
|
|
50
|
+
subgraphBorder: '#aaa99f',
|
|
51
|
+
subgraphLabel: '#666b64',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function previewTheme() { return isThemeDark() ? DARK_THEME : LIGHT_THEME; }
|
|
55
|
+
|
|
43
56
|
function escapeXml(str) {
|
|
44
57
|
return String(str)
|
|
45
58
|
.replace(/&/g, '&')
|
|
@@ -62,6 +75,8 @@ function measureText(text, fontSize) {
|
|
|
62
75
|
export function renderFlowchartSVG(diagram, opts = {}) {
|
|
63
76
|
if (!diagram || !diagram.nodes || diagram.nodes.length === 0) return '';
|
|
64
77
|
|
|
78
|
+
const THEME = previewTheme();
|
|
79
|
+
|
|
65
80
|
const nodes = diagram.nodes;
|
|
66
81
|
const edges = diagram.edges || [];
|
|
67
82
|
const subgraphs = diagram.subgraphs || [];
|
|
@@ -455,7 +470,7 @@ export function renderFlowchartOnCanvas(diagram) {
|
|
|
455
470
|
fillStyle: n.fill && n.fill !== 'transparent' ? 'solid' : 'none',
|
|
456
471
|
roughness: 1,
|
|
457
472
|
label: n.label || '',
|
|
458
|
-
labelColor: n.labelColor || nodeStrokeColor(),
|
|
473
|
+
labelColor: n.labelColor || (n.fill && n.fill !== 'transparent' ? getContrastColor(n.fill) : nodeStrokeColor()),
|
|
459
474
|
};
|
|
460
475
|
|
|
461
476
|
let shape = null;
|
|
@@ -35,7 +35,7 @@ function themeColors() {
|
|
|
35
35
|
const isDark = typeof document !== 'undefined'
|
|
36
36
|
&& document.body
|
|
37
37
|
&& document.body.classList.contains('theme-dark');
|
|
38
|
-
if (isDark) return
|
|
38
|
+
if (isDark) return DARK_THEME;
|
|
39
39
|
return {
|
|
40
40
|
bg: '#fbfaf6',
|
|
41
41
|
participantBg: '#ffffff',
|
|
@@ -56,7 +56,7 @@ function themeColors() {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// Theme colors (dark theme — preview/SVG-string path).
|
|
59
|
-
const
|
|
59
|
+
const DARK_THEME = {
|
|
60
60
|
bg: '#1e1e28',
|
|
61
61
|
participantBg: '#232329',
|
|
62
62
|
participantBorder: '#555',
|
|
@@ -125,6 +125,8 @@ function wrapText(text, fontSize, maxWidth) {
|
|
|
125
125
|
export function renderSequenceSVG(diagram, opts = {}) {
|
|
126
126
|
if (!diagram || diagram.type !== 'sequenceDiagram') return '';
|
|
127
127
|
|
|
128
|
+
const THEME = themeColors();
|
|
129
|
+
|
|
128
130
|
const participants = diagram.participants;
|
|
129
131
|
const messages = diagram.messages;
|
|
130
132
|
const notes = diagram.notes;
|
|
@@ -401,9 +403,9 @@ export function parseAndRenderSequence(src) {
|
|
|
401
403
|
* Issue #34 bug #3 (follow-up to #24 per-actor split): drops the shared
|
|
402
404
|
* `groupId` glue so clicking one shape selects only that shape.
|
|
403
405
|
*
|
|
404
|
-
* Notes and block
|
|
405
|
-
*
|
|
406
|
-
*
|
|
406
|
+
* Notes and block frames are ordinary labelled rectangles. Self messages
|
|
407
|
+
* use three line segments plus an arrow, keeping every visible component
|
|
408
|
+
* editable with the same primitives users draw manually.
|
|
407
409
|
*/
|
|
408
410
|
export function renderSequenceOnCanvas(diagram) {
|
|
409
411
|
if (!diagram || diagram.type !== 'sequenceDiagram') return false;
|
|
@@ -414,6 +416,8 @@ export function renderSequenceOnCanvas(diagram) {
|
|
|
414
416
|
|
|
415
417
|
const participants = diagram.participants || [];
|
|
416
418
|
const messages = diagram.messages || [];
|
|
419
|
+
const notes = diagram.notes || [];
|
|
420
|
+
const blocks = diagram.blocks || [];
|
|
417
421
|
if (participants.length === 0) return false;
|
|
418
422
|
|
|
419
423
|
// Mirror the layout math from renderSequenceSVG so the canvas layout
|
|
@@ -425,12 +429,23 @@ export function renderSequenceOnCanvas(diagram) {
|
|
|
425
429
|
const pCenters = participants.map((_, i) => startX + i * PARTICIPANT_GAP + PARTICIPANT_W / 2);
|
|
426
430
|
|
|
427
431
|
const topBoxBottom = TOP_MARGIN + PARTICIPANT_H;
|
|
432
|
+
const noteAtMsg = new Map();
|
|
433
|
+
notes.forEach((note) => {
|
|
434
|
+
const lines = wrapText(note.text, 11, NOTE_MAX_W - NOTE_PAD * 2);
|
|
435
|
+
const height = lines.length * 15 + NOTE_PAD * 2;
|
|
436
|
+
if (!noteAtMsg.has(note.atMessage)) noteAtMsg.set(note.atMessage, []);
|
|
437
|
+
noteAtMsg.get(note.atMessage).push({ ...note, lines, height });
|
|
438
|
+
});
|
|
428
439
|
const msgYPositions = [];
|
|
429
440
|
let currentY = topBoxBottom + 30;
|
|
430
441
|
for (let mi = 0; mi < messages.length; mi++) {
|
|
442
|
+
const before = noteAtMsg.get(mi);
|
|
443
|
+
if (before?.length) currentY += Math.max(...before.map(note => note.height)) + 10;
|
|
431
444
|
msgYPositions.push(currentY);
|
|
432
445
|
currentY += MSG_ROW_HEIGHT;
|
|
433
446
|
}
|
|
447
|
+
const trailingNotes = noteAtMsg.get(messages.length);
|
|
448
|
+
if (trailingNotes?.length) currentY += Math.max(...trailingNotes.map(note => note.height)) + 10;
|
|
434
449
|
const bottomBoxTop = currentY + 20;
|
|
435
450
|
const totalHeight = bottomBoxTop + PARTICIPANT_H + BOTTOM_MARGIN;
|
|
436
451
|
|
|
@@ -532,13 +547,43 @@ export function renderSequenceOnCanvas(diagram) {
|
|
|
532
547
|
}
|
|
533
548
|
}
|
|
534
549
|
|
|
550
|
+
// ── Blocks: editable containers behind message rows ───────────────
|
|
551
|
+
for (const block of blocks) {
|
|
552
|
+
const startY = block.startMsg < msgYPositions.length ? msgYPositions[block.startMsg] - 22 : topBoxBottom + 15;
|
|
553
|
+
const endY = block.endMsg < msgYPositions.length ? msgYPositions[block.endMsg] - 8 : currentY;
|
|
554
|
+
try {
|
|
555
|
+
const blockShape = new window.Rectangle(
|
|
556
|
+
startX - 18 + ox,
|
|
557
|
+
startY + oy,
|
|
558
|
+
contentWidth + 36,
|
|
559
|
+
Math.max(36, endY - startY),
|
|
560
|
+
{
|
|
561
|
+
stroke: TK.blockBorder,
|
|
562
|
+
strokeWidth: 1,
|
|
563
|
+
strokeDasharray: '5 3',
|
|
564
|
+
fill: 'transparent',
|
|
565
|
+
fillStyle: 'none',
|
|
566
|
+
roughness: .5,
|
|
567
|
+
label: [block.type, block.label].filter(Boolean).join(': '),
|
|
568
|
+
labelColor: TK.blockLabel,
|
|
569
|
+
labelFontSize: 10,
|
|
570
|
+
}
|
|
571
|
+
);
|
|
572
|
+
window.shapes.push(blockShape);
|
|
573
|
+
if (window.pushCreateAction) window.pushCreateAction(blockShape);
|
|
574
|
+
frame.addShapeToFrame(blockShape);
|
|
575
|
+
created.push(blockShape);
|
|
576
|
+
} catch (err) {
|
|
577
|
+
console.warn('[SequenceRenderer] Block creation failed:', block, err);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
535
581
|
// ── Messages: arrow (or line for --x style) per row ───────────────
|
|
536
582
|
for (let mi = 0; mi < messages.length; mi++) {
|
|
537
583
|
const m = messages[mi];
|
|
538
584
|
const fromI = pIndex.get(m.from);
|
|
539
585
|
const toI = pIndex.get(m.to);
|
|
540
586
|
if (fromI == null || toI == null) continue;
|
|
541
|
-
if (fromI === toI) continue; // skip self-messages (v1 limitation)
|
|
542
587
|
|
|
543
588
|
const fromCx = pCenters[fromI] + ox;
|
|
544
589
|
const toCx = pCenters[toI] + ox;
|
|
@@ -562,6 +607,26 @@ export function renderSequenceOnCanvas(diagram) {
|
|
|
562
607
|
try {
|
|
563
608
|
const sp = { x: fromCx, y };
|
|
564
609
|
const ep = { x: toCx, y };
|
|
610
|
+
if (fromI === toI) {
|
|
611
|
+
const loopWidth = 46;
|
|
612
|
+
const loopHeight = 28;
|
|
613
|
+
const a = { x: fromCx, y };
|
|
614
|
+
const b = { x: fromCx + loopWidth, y };
|
|
615
|
+
const c = { x: fromCx + loopWidth, y: y + loopHeight };
|
|
616
|
+
const d = { x: fromCx + 5, y: y + loopHeight };
|
|
617
|
+
const segments = [
|
|
618
|
+
new window.Line(a, b, opts),
|
|
619
|
+
new window.Line(b, c, opts),
|
|
620
|
+
new window.Arrow(c, d, opts),
|
|
621
|
+
];
|
|
622
|
+
segments.forEach((segment) => {
|
|
623
|
+
window.shapes.push(segment);
|
|
624
|
+
if (window.pushCreateAction) window.pushCreateAction(segment);
|
|
625
|
+
frame.addShapeToFrame(segment);
|
|
626
|
+
created.push(segment);
|
|
627
|
+
});
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
565
630
|
const connector = isCross
|
|
566
631
|
? new window.Line(sp, ep, opts)
|
|
567
632
|
: new window.Arrow(sp, ep, opts);
|
|
@@ -574,6 +639,51 @@ export function renderSequenceOnCanvas(diagram) {
|
|
|
574
639
|
}
|
|
575
640
|
}
|
|
576
641
|
|
|
642
|
+
|
|
643
|
+
// ── Notes: one independently editable rectangle per note ─────────
|
|
644
|
+
for (const [messageIndex, noteGroup] of noteAtMsg.entries()) {
|
|
645
|
+
const baseY = messageIndex < msgYPositions.length
|
|
646
|
+
? msgYPositions[messageIndex] - 15
|
|
647
|
+
: (messageIndex > 0 ? msgYPositions[messageIndex - 1] + MSG_ROW_HEIGHT - 15 : topBoxBottom + 30);
|
|
648
|
+
noteGroup.forEach((note, noteIndex) => {
|
|
649
|
+
const indexes = note.targets.map(target => pIndex.get(target)).filter(index => index !== undefined);
|
|
650
|
+
if (!indexes.length) return;
|
|
651
|
+
const noteWidth = NOTE_MAX_W;
|
|
652
|
+
let centerX = pCenters[indexes[0]];
|
|
653
|
+
if (note.position === 'over' && indexes.length > 1) {
|
|
654
|
+
centerX = (pCenters[Math.min(...indexes)] + pCenters[Math.max(...indexes)]) / 2;
|
|
655
|
+
} else if (note.position === 'left of') {
|
|
656
|
+
centerX -= PARTICIPANT_W / 2 + noteWidth / 2 + 8;
|
|
657
|
+
} else if (note.position === 'right of') {
|
|
658
|
+
centerX += PARTICIPANT_W / 2 + noteWidth / 2 + 8;
|
|
659
|
+
}
|
|
660
|
+
try {
|
|
661
|
+
const noteShape = new window.Rectangle(
|
|
662
|
+
centerX - noteWidth / 2 + ox,
|
|
663
|
+
baseY - note.height - noteIndex * (note.height + 6) + oy,
|
|
664
|
+
noteWidth,
|
|
665
|
+
note.height,
|
|
666
|
+
{
|
|
667
|
+
stroke: TK.noteBorder,
|
|
668
|
+
strokeWidth: 1,
|
|
669
|
+
fill: TK.noteBg,
|
|
670
|
+
fillStyle: 'solid',
|
|
671
|
+
roughness: .7,
|
|
672
|
+
label: note.text,
|
|
673
|
+
labelColor: TK.noteText,
|
|
674
|
+
labelFontSize: 11,
|
|
675
|
+
}
|
|
676
|
+
);
|
|
677
|
+
window.shapes.push(noteShape);
|
|
678
|
+
if (window.pushCreateAction) window.pushCreateAction(noteShape);
|
|
679
|
+
frame.addShapeToFrame(noteShape);
|
|
680
|
+
created.push(noteShape);
|
|
681
|
+
} catch (err) {
|
|
682
|
+
console.warn('[SequenceRenderer] Note creation failed:', note, err);
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
577
687
|
// Auto-select the first node so the user has feedback that the
|
|
578
688
|
// diagram landed. Selecting a child (not the frame) reinforces the
|
|
579
689
|
// "independent shapes inside a frame" model from issue #34 bug #3.
|
|
@@ -586,4 +696,3 @@ export function renderSequenceOnCanvas(diagram) {
|
|
|
586
696
|
console.log(`[SequenceRenderer] Done: ${pCount} participants, ${messages.length} messages`);
|
|
587
697
|
return true;
|
|
588
698
|
}
|
|
589
|
-
|