@elixpo/lixsketch 5.4.7 → 5.5.1
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-M4LMJ66J.js +1105 -0
- package/dist/react/AIRenderer-M4LMJ66J.js.map +7 -0
- package/dist/react/Arrow-BJDAXPMH.js +8 -0
- package/dist/react/Arrow-BJDAXPMH.js.map +7 -0
- package/dist/react/Circle-KPG7TKIX.js +8 -0
- package/dist/react/Circle-KPG7TKIX.js.map +7 -0
- package/dist/react/CodeShape-K4DIZAIV.js +8 -0
- package/dist/react/CodeShape-K4DIZAIV.js.map +7 -0
- package/dist/react/CopyPaste-4CKJRHJ3.js +569 -0
- package/dist/react/CopyPaste-4CKJRHJ3.js.map +7 -0
- package/dist/react/EraserTrail-PA6IQLQN.js +20 -0
- package/dist/react/EraserTrail-PA6IQLQN.js.map +7 -0
- package/dist/react/EventDispatcher-SRFH6LQZ.js +463 -0
- package/dist/react/EventDispatcher-SRFH6LQZ.js.map +7 -0
- package/dist/react/Frame-6UR5XPAX.js +10 -0
- package/dist/react/Frame-6UR5XPAX.js.map +7 -0
- package/dist/react/FreehandStroke-CK24NUT3.js +8 -0
- package/dist/react/FreehandStroke-CK24NUT3.js.map +7 -0
- package/dist/react/GraphEngine-QE5B2PMR.js +340 -0
- package/dist/react/GraphEngine-QE5B2PMR.js.map +7 -0
- package/dist/react/GraphMathParser-2WMTY4IE.js +10 -0
- package/dist/react/GraphMathParser-2WMTY4IE.js.map +7 -0
- package/dist/react/IconShape-KMIA6T2H.js +10 -0
- package/dist/react/IconShape-KMIA6T2H.js.map +7 -0
- package/dist/react/ImageShape-CHHGBXUQ.js +10 -0
- package/dist/react/ImageShape-CHHGBXUQ.js.map +7 -0
- package/dist/react/LayerOrder-XCHF4MSS.js +66 -0
- package/dist/react/LayerOrder-XCHF4MSS.js.map +7 -0
- package/dist/react/Line-2KOQEEGA.js +10 -0
- package/dist/react/Line-2KOQEEGA.js.map +7 -0
- package/dist/react/LixScriptParser-JYWJX375.js +1033 -0
- package/dist/react/LixScriptParser-JYWJX375.js.map +7 -0
- package/dist/react/MermaidFlowchartRenderer-4QBAZ5MB.js +412 -0
- package/dist/react/MermaidFlowchartRenderer-4QBAZ5MB.js.map +7 -0
- package/dist/react/MermaidSequenceParser-UKG2NSZY.js +8 -0
- package/dist/react/MermaidSequenceParser-UKG2NSZY.js.map +7 -0
- package/dist/react/MermaidSequenceRenderer-HPLHY5II.js +387 -0
- package/dist/react/MermaidSequenceRenderer-HPLHY5II.js.map +7 -0
- package/dist/react/Rectangle-6FE7RN6W.js +8 -0
- package/dist/react/Rectangle-6FE7RN6W.js.map +7 -0
- package/dist/react/ResizeCode-WAMDTKSW.js +2 -0
- package/dist/react/ResizeCode-WAMDTKSW.js.map +7 -0
- package/dist/react/ResizeShapes-LKEQTITP.js +250 -0
- package/dist/react/ResizeShapes-LKEQTITP.js.map +7 -0
- package/dist/react/SceneSerializer-4U4B24JM.js +739 -0
- package/dist/react/SceneSerializer-4U4B24JM.js.map +7 -0
- package/dist/react/Selection-VVUHY5YJ.js +34 -0
- package/dist/react/Selection-VVUHY5YJ.js.map +7 -0
- package/dist/react/SketchEngine-YA4RG3KX.js +413 -0
- package/dist/react/SketchEngine-YA4RG3KX.js.map +7 -0
- package/dist/react/TextShape-54ZJ66E3.js +8 -0
- package/dist/react/TextShape-54ZJ66E3.js.map +7 -0
- package/dist/react/UndoRedo-M2JZUBKN.js +30 -0
- package/dist/react/UndoRedo-M2JZUBKN.js.map +7 -0
- package/dist/react/ZoomPan-U3TXC5AJ.js +186 -0
- package/dist/react/ZoomPan-U3TXC5AJ.js.map +7 -0
- package/dist/react/arrowTool-CWEMIRXK.js +17 -0
- package/dist/react/arrowTool-CWEMIRXK.js.map +7 -0
- package/dist/react/boxicons-5JNJQPUP.woff +0 -0
- package/dist/react/boxicons-6NVWQSCQ.eot +0 -0
- package/dist/react/boxicons-AQ5N6WP7.woff2 +0 -0
- package/dist/react/boxicons-GDR4JU3X.ttf +0 -0
- package/dist/react/boxicons-NNFHLMOR.svg +1660 -0
- package/dist/react/chunk-2OQXI7QB.js +564 -0
- package/dist/react/chunk-2OQXI7QB.js.map +7 -0
- package/dist/react/chunk-5OUBTHHO.js +187 -0
- package/dist/react/chunk-5OUBTHHO.js.map +7 -0
- package/dist/react/chunk-6PFXZKL2.js +628 -0
- package/dist/react/chunk-6PFXZKL2.js.map +7 -0
- package/dist/react/chunk-6SFNWGGZ.js +453 -0
- package/dist/react/chunk-6SFNWGGZ.js.map +7 -0
- package/dist/react/chunk-7TZSIR6V.js +210 -0
- package/dist/react/chunk-7TZSIR6V.js.map +7 -0
- package/dist/react/chunk-AJAZC37E.js +662 -0
- package/dist/react/chunk-AJAZC37E.js.map +7 -0
- package/dist/react/chunk-BU4D2WYQ.js +1213 -0
- package/dist/react/chunk-BU4D2WYQ.js.map +7 -0
- package/dist/react/chunk-BZE4CEBG.js +747 -0
- package/dist/react/chunk-BZE4CEBG.js.map +7 -0
- package/dist/react/chunk-CB2RANCB.js +212 -0
- package/dist/react/chunk-CB2RANCB.js.map +7 -0
- package/dist/react/chunk-DNAG7ML6.js +128 -0
- package/dist/react/chunk-DNAG7ML6.js.map +7 -0
- package/dist/react/chunk-HZWLPXFN.js +198 -0
- package/dist/react/chunk-HZWLPXFN.js.map +7 -0
- package/dist/react/chunk-I6HCCIXB.js +572 -0
- package/dist/react/chunk-I6HCCIXB.js.map +7 -0
- package/dist/react/chunk-IZ5OGWGW.js +235 -0
- package/dist/react/chunk-IZ5OGWGW.js.map +7 -0
- package/dist/react/chunk-JJCJNLNN.js +1490 -0
- package/dist/react/chunk-JJCJNLNN.js.map +7 -0
- package/dist/react/chunk-JQ3IRFDP.js +222 -0
- package/dist/react/chunk-JQ3IRFDP.js.map +7 -0
- package/dist/react/chunk-L5OARZJ7.js +1134 -0
- package/dist/react/chunk-L5OARZJ7.js.map +7 -0
- package/dist/react/chunk-LHRTXNVE.js +1600 -0
- package/dist/react/chunk-LHRTXNVE.js.map +7 -0
- package/dist/react/chunk-Q2GA4MBZ.js +1653 -0
- package/dist/react/chunk-Q2GA4MBZ.js.map +7 -0
- package/dist/react/chunk-QNSE5BAY.js +519 -0
- package/dist/react/chunk-QNSE5BAY.js.map +7 -0
- package/dist/react/chunk-QXFBG7OM.js +191 -0
- package/dist/react/chunk-QXFBG7OM.js.map +7 -0
- package/dist/react/chunk-T6ZVV5ZO.js +714 -0
- package/dist/react/chunk-T6ZVV5ZO.js.map +7 -0
- package/dist/react/chunk-URB6MY46.js +974 -0
- package/dist/react/chunk-URB6MY46.js.map +7 -0
- package/dist/react/chunk-VN62KLOC.js +931 -0
- package/dist/react/chunk-VN62KLOC.js.map +7 -0
- package/dist/react/chunk-VQVEPJSA.js +158 -0
- package/dist/react/chunk-VQVEPJSA.js.map +7 -0
- package/dist/react/chunk-XISEAGBV.js +1632 -0
- package/dist/react/chunk-XISEAGBV.js.map +7 -0
- package/dist/react/chunk-YKJUBFHE.js +575 -0
- package/dist/react/chunk-YKJUBFHE.js.map +7 -0
- package/dist/react/circleTool-NUATSEI2.js +14 -0
- package/dist/react/circleTool-NUATSEI2.js.map +7 -0
- package/dist/react/codeTool-MHCB2FNB.js +38 -0
- package/dist/react/codeTool-MHCB2FNB.js.map +7 -0
- package/dist/react/eraserTool-2GS2LQCS.js +103 -0
- package/dist/react/eraserTool-2GS2LQCS.js.map +7 -0
- package/dist/react/frameTool-PWVKKXSP.js +14 -0
- package/dist/react/frameTool-PWVKKXSP.js.map +7 -0
- package/dist/react/freehandTool-SLYGASVP.js +14 -0
- package/dist/react/freehandTool-SLYGASVP.js.map +7 -0
- package/dist/react/iconTool-GPZ7O7NH.js +18 -0
- package/dist/react/iconTool-GPZ7O7NH.js.map +7 -0
- package/dist/react/imageTool-D5R4XGEU.js +15 -0
- package/dist/react/imageTool-D5R4XGEU.js.map +7 -0
- package/dist/react/index.js +5349 -0
- package/dist/react/index.js.map +7 -0
- package/dist/react/laserTool-XVMV63AV.js +239 -0
- package/dist/react/laserTool-XVMV63AV.js.map +7 -0
- package/dist/react/lineTool-WNI5EMZ5.js +14 -0
- package/dist/react/lineTool-WNI5EMZ5.js.map +7 -0
- package/dist/react/lixCode-OJMVUXXI.ttf +0 -0
- package/dist/react/lixDefault-JEAPAUJL.ttf +0 -0
- package/dist/react/lixFancy-BPXYM5CQ.ttf +0 -0
- package/dist/react/lixFont-CPKEUDVM.woff2 +0 -0
- package/dist/react/rectangleTool-U7DZ6ZTA.js +14 -0
- package/dist/react/rectangleTool-U7DZ6ZTA.js.map +7 -0
- package/dist/react/styles.css +6627 -0
- package/dist/react/textTool-M7S6F3VC.js +21 -0
- package/dist/react/textTool-M7S6F3VC.js.map +7 -0
- package/package.json +39 -5
- package/src/EngineShortcuts.js +83 -27
- package/src/core/AIRenderer.js +30 -18
- package/src/core/GraphEngine.js +64 -41
- package/src/core/MermaidFlowchartRenderer.js +153 -88
- package/src/core/MermaidSequenceRenderer.js +168 -93
- package/src/core/SceneSerializer.js +86 -2
- package/src/core/Selection.js +119 -19
- package/src/core/UndoRedo.js +82 -15
- package/src/core/ZoomPan.js +15 -5
- package/src/index.js +14 -1
- package/src/react/LixSketchCanvas.jsx +347 -0
- package/src/react/components/AppMenu.jsx +482 -0
- package/src/react/components/Footer.jsx +85 -0
- package/src/react/components/Toolbar.jsx +140 -0
- package/src/react/components/canvas/CanvasLoadingOverlay.jsx +76 -0
- package/src/react/components/canvas/ContextMenu.jsx +466 -0
- package/src/react/components/canvas/FindBar.jsx +177 -0
- package/src/react/components/canvas/ImageSourcePicker.jsx +133 -0
- package/src/react/components/canvas/MultiSelectActions.jsx +120 -0
- package/src/react/components/canvas/SVGCanvas.jsx +145 -0
- package/src/react/components/canvas/SaveStatusIndicator.jsx +29 -0
- package/src/react/components/modals/CanvasPropertiesModal.jsx +358 -0
- package/src/react/components/modals/CommandPalette.jsx +307 -0
- package/src/react/components/modals/ExportImageModal.jsx +287 -0
- package/src/react/components/modals/GraphModal.jsx +174 -0
- package/src/react/components/modals/HelpModal.jsx +208 -0
- package/src/react/components/modals/LixScriptModal.jsx +130 -0
- package/src/react/components/modals/ShortcutsModal.jsx +125 -0
- package/src/react/components/sidebars/ArrowSidebar.jsx +173 -0
- package/src/react/components/sidebars/CircleSidebar.jsx +134 -0
- package/src/react/components/sidebars/FrameSidebar.jsx +263 -0
- package/src/react/components/sidebars/IconSidebar.jsx +279 -0
- package/src/react/components/sidebars/ImageSidebar.jsx +69 -0
- package/src/react/components/sidebars/LineSidebar.jsx +109 -0
- package/src/react/components/sidebars/PaintbrushSidebar.jsx +119 -0
- package/src/react/components/sidebars/RectangleSidebar.jsx +165 -0
- package/src/react/components/sidebars/ShapeSidebar.jsx +130 -0
- package/src/react/components/sidebars/TextSidebar.jsx +174 -0
- package/src/react/hooks/inertStores.js +58 -0
- package/src/react/hooks/useSketchEngine.js +76 -0
- package/src/react/hooks/useTranslation.js +92 -0
- package/src/react/index.js +41 -0
- package/src/react/store/useSketchStore.js +234 -0
- package/src/react/store/useUIStore.js +196 -0
- package/src/react/styles/arrowSidebar.css +263 -0
- package/src/react/styles/canvasStroke.css +47 -0
- package/src/react/styles/canvasTools.css +769 -0
- package/src/react/styles/circleSidebar.css +206 -0
- package/src/react/styles/fonts/fonts.css +29 -0
- package/src/react/styles/fonts/lixCode.ttf +0 -0
- package/src/react/styles/fonts/lixDefault.ttf +0 -0
- package/src/react/styles/fonts/lixDocs.ttf +0 -0
- package/src/react/styles/fonts/lixFancy.ttf +0 -0
- package/src/react/styles/fonts/lixFont.woff2 +0 -0
- package/src/react/styles/lineSidebar.css +207 -0
- package/src/react/styles/paintbrushSidebar.css +247 -0
- package/src/react/styles/squareSidebar.css +206 -0
- package/src/react/styles/writeText.css +149 -0
- package/src/react/styles.css +175 -0
- package/src/react/toolsEnum.js +20 -0
- package/src/react/utils/toast.js +18 -0
- package/src/shapes/Frame.js +7 -1
- package/src/shapes/Line.js +11 -5
- package/src/tools/arrowTool.js +30 -4
- package/src/tools/circleTool.js +33 -9
- package/src/tools/codeTool.js +5 -3
- package/src/tools/frameTool.js +47 -7
- package/src/tools/freehandTool.js +12 -4
- package/src/tools/iconTool.js +7 -1
- package/src/tools/imageTool.js +7 -1
- package/src/tools/lineTool.js +27 -3
- package/src/tools/rectangleTool.js +45 -8
- package/src/tools/textTool.js +23 -6
|
@@ -0,0 +1,1105 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/core/AIRenderer.js
|
|
4
|
+
var NODE_W = 160;
|
|
5
|
+
var NODE_H = 60;
|
|
6
|
+
var H_SPACING = 260;
|
|
7
|
+
var V_SPACING = 180;
|
|
8
|
+
var NS = "http://www.w3.org/2000/svg";
|
|
9
|
+
var ALIGN_THRESHOLD = 30;
|
|
10
|
+
function parseMermaid(src) {
|
|
11
|
+
const lines = src.trim().split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("%%"));
|
|
12
|
+
if (lines.length === 0) return null;
|
|
13
|
+
const headerMatch = lines[0].match(/^(graph|flowchart)\s+(TD|TB|LR|RL|BT)/i);
|
|
14
|
+
const direction = headerMatch ? headerMatch[2].toUpperCase() : "TD";
|
|
15
|
+
const isHorizontal = direction === "LR" || direction === "RL";
|
|
16
|
+
const startIdx = headerMatch ? 1 : 0;
|
|
17
|
+
const nodesMap = /* @__PURE__ */ new Map();
|
|
18
|
+
const edges = [];
|
|
19
|
+
const classDefs = /* @__PURE__ */ new Map();
|
|
20
|
+
const classAssigns = [];
|
|
21
|
+
function cleanLabel(label) {
|
|
22
|
+
return label.replace(/<br\s*\/?>/gi, "\n").replace(/"/g, "");
|
|
23
|
+
}
|
|
24
|
+
function parseNodeRef(raw) {
|
|
25
|
+
raw = raw.trim();
|
|
26
|
+
if (!raw) return null;
|
|
27
|
+
let id, label, type;
|
|
28
|
+
let m = raw.match(/^(\w+)\(\((.+?)\)\)$/);
|
|
29
|
+
if (m) {
|
|
30
|
+
id = m[1];
|
|
31
|
+
label = m[2];
|
|
32
|
+
type = "circle";
|
|
33
|
+
}
|
|
34
|
+
if (!m) {
|
|
35
|
+
m = raw.match(/^(\w+)\{(.+?)\}$/);
|
|
36
|
+
if (m) {
|
|
37
|
+
id = m[1];
|
|
38
|
+
label = m[2];
|
|
39
|
+
type = "diamond";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (!m) {
|
|
43
|
+
m = raw.match(/^(\w+)>(.+?)\]$/);
|
|
44
|
+
if (m) {
|
|
45
|
+
id = m[1];
|
|
46
|
+
label = m[2];
|
|
47
|
+
type = "asymmetric";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (!m) {
|
|
51
|
+
m = raw.match(/^(\w+)\((.+?)\)$/);
|
|
52
|
+
if (m) {
|
|
53
|
+
id = m[1];
|
|
54
|
+
label = m[2];
|
|
55
|
+
type = "roundrect";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!m) {
|
|
59
|
+
m = raw.match(/^(\w+)\[(.+?)\]$/);
|
|
60
|
+
if (m) {
|
|
61
|
+
id = m[1];
|
|
62
|
+
label = m[2];
|
|
63
|
+
type = "rectangle";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!m) {
|
|
67
|
+
id = raw;
|
|
68
|
+
label = raw;
|
|
69
|
+
type = "rectangle";
|
|
70
|
+
}
|
|
71
|
+
label = cleanLabel(label);
|
|
72
|
+
if (!nodesMap.has(id)) {
|
|
73
|
+
nodesMap.set(id, { id, type, label });
|
|
74
|
+
} else if (label !== id) {
|
|
75
|
+
nodesMap.get(id).label = label;
|
|
76
|
+
nodesMap.get(id).type = type;
|
|
77
|
+
}
|
|
78
|
+
return id;
|
|
79
|
+
}
|
|
80
|
+
const subgraphs = [];
|
|
81
|
+
let currentSubgraph = null;
|
|
82
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
83
|
+
let addToSubgraph = function(fromId, toId) {
|
|
84
|
+
if (currentSubgraph) {
|
|
85
|
+
if (fromId && !currentSubgraph.nodeIds.includes(fromId)) currentSubgraph.nodeIds.push(fromId);
|
|
86
|
+
if (toId && !currentSubgraph.nodeIds.includes(toId)) currentSubgraph.nodeIds.push(toId);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const line = lines[i].replace(/;$/, "").trim();
|
|
90
|
+
if (!line) continue;
|
|
91
|
+
const classDefMatch = line.match(/^classDef\s+(\w+)\s+(.+)$/i);
|
|
92
|
+
if (classDefMatch) {
|
|
93
|
+
const name = classDefMatch[1];
|
|
94
|
+
const propsStr = classDefMatch[2].replace(/;$/, "");
|
|
95
|
+
const props = {};
|
|
96
|
+
for (const part of propsStr.split(",")) {
|
|
97
|
+
const [key, val] = part.split(":").map((s) => s.trim());
|
|
98
|
+
if (key === "fill") props.fill = val;
|
|
99
|
+
else if (key === "stroke") props.stroke = val;
|
|
100
|
+
else if (key === "stroke-width") props.strokeWidth = parseFloat(val);
|
|
101
|
+
}
|
|
102
|
+
classDefs.set(name, props);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const classMatch = line.match(/^class\s+(.+?)\s+(\w+)$/i);
|
|
106
|
+
if (classMatch) {
|
|
107
|
+
const nodeIds2 = classMatch[1].split(",").map((s) => s.trim());
|
|
108
|
+
classAssigns.push({ nodeIds: nodeIds2, className: classMatch[2] });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const sgMatch = line.match(/^subgraph\s+(\w+)(?:\s*\[?"?(.+?)"?\]?)?$/i);
|
|
112
|
+
if (sgMatch) {
|
|
113
|
+
currentSubgraph = {
|
|
114
|
+
id: sgMatch[1],
|
|
115
|
+
label: sgMatch[2] || sgMatch[1],
|
|
116
|
+
nodeIds: []
|
|
117
|
+
};
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (line.toLowerCase() === "end" && currentSubgraph) {
|
|
121
|
+
subgraphs.push(currentSubgraph);
|
|
122
|
+
currentSubgraph = null;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
let match = line.match(/^(.+?)\s*--\s*(.+?)\s*-->\s*(.+)$/);
|
|
126
|
+
if (match) {
|
|
127
|
+
const fromId = parseNodeRef(match[1].trim());
|
|
128
|
+
const toId = parseNodeRef(match[3].trim());
|
|
129
|
+
if (fromId && toId) edges.push({ from: fromId, to: toId, label: cleanLabel(match[2].trim()), directed: true, style: "normal" });
|
|
130
|
+
addToSubgraph(fromId, toId);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
match = line.match(/^(.+?)\s*-\.->?\s*(?:\|([^|]*)\|)?\s*(.+)$/);
|
|
134
|
+
if (match) {
|
|
135
|
+
const fromId = parseNodeRef(match[1].trim());
|
|
136
|
+
const toId = parseNodeRef(match[3].trim());
|
|
137
|
+
const edgeLabel = match[2] ? cleanLabel(match[2].trim()) : void 0;
|
|
138
|
+
if (fromId && toId) edges.push({ from: fromId, to: toId, label: edgeLabel, directed: true, style: "dotted" });
|
|
139
|
+
addToSubgraph(fromId, toId);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
match = line.match(/^(.+?)\s*==>\s*(?:\|([^|]*)\|)?\s*(.+)$/);
|
|
143
|
+
if (match) {
|
|
144
|
+
const fromId = parseNodeRef(match[1].trim());
|
|
145
|
+
const toId = parseNodeRef(match[3].trim());
|
|
146
|
+
const edgeLabel = match[2] ? cleanLabel(match[2].trim()) : void 0;
|
|
147
|
+
if (fromId && toId) edges.push({ from: fromId, to: toId, label: edgeLabel, directed: true, style: "thick" });
|
|
148
|
+
addToSubgraph(fromId, toId);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
match = line.match(/^(.+?)\s*(-{3,})\s*(?:\|([^|]*)\|)?\s*(.+)$/);
|
|
152
|
+
if (match && !match[2].includes(">")) {
|
|
153
|
+
const fromId = parseNodeRef(match[1].trim());
|
|
154
|
+
const toId = parseNodeRef(match[4].trim());
|
|
155
|
+
const edgeLabel = match[3] ? cleanLabel(match[3].trim()) : void 0;
|
|
156
|
+
if (fromId && toId) edges.push({ from: fromId, to: toId, label: edgeLabel, directed: false, style: "normal" });
|
|
157
|
+
addToSubgraph(fromId, toId);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
match = line.match(/^(.+?)\s*(-{1,2}>|-->)\s*(?:\|([^|]*)\|)?\s*(.+)$/);
|
|
161
|
+
if (match) {
|
|
162
|
+
const fromId = parseNodeRef(match[1].trim());
|
|
163
|
+
const toId = parseNodeRef(match[4].trim());
|
|
164
|
+
const edgeLabel = match[3] ? cleanLabel(match[3].trim()) : void 0;
|
|
165
|
+
if (fromId && toId) edges.push({ from: fromId, to: toId, label: edgeLabel, directed: true, style: "normal" });
|
|
166
|
+
addToSubgraph(fromId, toId);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const nodeId = parseNodeRef(line);
|
|
170
|
+
if (nodeId && currentSubgraph) {
|
|
171
|
+
if (!currentSubgraph.nodeIds.includes(nodeId)) currentSubgraph.nodeIds.push(nodeId);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (nodesMap.size === 0) return null;
|
|
175
|
+
for (const assign of classAssigns) {
|
|
176
|
+
const style = classDefs.get(assign.className);
|
|
177
|
+
if (!style) continue;
|
|
178
|
+
for (const nid of assign.nodeIds) {
|
|
179
|
+
const node = nodesMap.get(nid);
|
|
180
|
+
if (node) {
|
|
181
|
+
if (style.fill) node.fill = style.fill;
|
|
182
|
+
if (style.stroke) node.stroke = style.stroke;
|
|
183
|
+
if (style.strokeWidth) node.strokeWidth = style.strokeWidth;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const nodeIds = Array.from(nodesMap.keys());
|
|
188
|
+
const children = /* @__PURE__ */ new Map();
|
|
189
|
+
const parents = /* @__PURE__ */ new Map();
|
|
190
|
+
nodeIds.forEach((id) => {
|
|
191
|
+
children.set(id, []);
|
|
192
|
+
parents.set(id, []);
|
|
193
|
+
});
|
|
194
|
+
edges.forEach((e) => {
|
|
195
|
+
if (children.has(e.from)) children.get(e.from).push(e.to);
|
|
196
|
+
if (parents.has(e.to)) parents.get(e.to).push(e.from);
|
|
197
|
+
});
|
|
198
|
+
const layers = /* @__PURE__ */ new Map();
|
|
199
|
+
const roots = nodeIds.filter((id) => parents.get(id).length === 0);
|
|
200
|
+
if (roots.length === 0) roots.push(nodeIds[0]);
|
|
201
|
+
const queue = roots.map((id) => ({ id, layer: 0 }));
|
|
202
|
+
const visited = /* @__PURE__ */ new Set();
|
|
203
|
+
while (queue.length > 0) {
|
|
204
|
+
const { id, layer } = queue.shift();
|
|
205
|
+
if (visited.has(id)) {
|
|
206
|
+
if (layer > (layers.get(id) || 0)) layers.set(id, layer);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
visited.add(id);
|
|
210
|
+
layers.set(id, Math.max(layer, layers.get(id) || 0));
|
|
211
|
+
for (const child of children.get(id) || []) queue.push({ id: child, layer: layer + 1 });
|
|
212
|
+
}
|
|
213
|
+
nodeIds.forEach((id) => {
|
|
214
|
+
if (!visited.has(id)) layers.set(id, 0);
|
|
215
|
+
});
|
|
216
|
+
const layerGroups = /* @__PURE__ */ new Map();
|
|
217
|
+
layers.forEach((layer, id) => {
|
|
218
|
+
if (!layerGroups.has(layer)) layerGroups.set(layer, []);
|
|
219
|
+
layerGroups.get(layer).push(id);
|
|
220
|
+
});
|
|
221
|
+
const nodes = [];
|
|
222
|
+
Array.from(layerGroups.keys()).sort((a, b) => a - b).forEach((layerIdx, li) => {
|
|
223
|
+
const group = layerGroups.get(layerIdx);
|
|
224
|
+
const startOffset = -(group.length * H_SPACING) / 2 + H_SPACING / 2;
|
|
225
|
+
group.forEach((id, gi) => {
|
|
226
|
+
const nd = nodesMap.get(id);
|
|
227
|
+
const x = isHorizontal ? li * H_SPACING : startOffset + gi * H_SPACING;
|
|
228
|
+
const y = isHorizontal ? startOffset + gi * V_SPACING : li * V_SPACING;
|
|
229
|
+
const labelLines = (nd.label || "").split("\n");
|
|
230
|
+
const maxLineLen = Math.max(...labelLines.map((l) => l.length));
|
|
231
|
+
const nw = Math.max(NODE_W, maxLineLen * 10 + 40);
|
|
232
|
+
const nh = Math.max(NODE_H, labelLines.length * 20 + 20);
|
|
233
|
+
nodes.push({
|
|
234
|
+
id: nd.id,
|
|
235
|
+
type: nd.type,
|
|
236
|
+
label: nd.label,
|
|
237
|
+
x,
|
|
238
|
+
y,
|
|
239
|
+
width: nw,
|
|
240
|
+
height: nh,
|
|
241
|
+
fill: nd.fill,
|
|
242
|
+
stroke: nd.stroke,
|
|
243
|
+
strokeWidth: nd.strokeWidth
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
return {
|
|
248
|
+
title: "Mermaid Diagram",
|
|
249
|
+
direction,
|
|
250
|
+
nodes,
|
|
251
|
+
edges: edges.map((e) => ({
|
|
252
|
+
from: e.from,
|
|
253
|
+
to: e.to,
|
|
254
|
+
label: e.label,
|
|
255
|
+
directed: e.directed !== false,
|
|
256
|
+
style: e.style || "normal"
|
|
257
|
+
})),
|
|
258
|
+
subgraphs: subgraphs.length > 0 ? subgraphs.map((sg) => ({
|
|
259
|
+
id: sg.id,
|
|
260
|
+
label: sg.label,
|
|
261
|
+
nodes: sg.nodeIds
|
|
262
|
+
})) : void 0
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function renderAIDiagram(diagram) {
|
|
266
|
+
if (!diagram?.nodes?.length) {
|
|
267
|
+
console.error("[AIRenderer] Invalid diagram");
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
const nodes = diagram.nodes;
|
|
271
|
+
const edges = diagram.edges || [];
|
|
272
|
+
const title = diagram.title || "AI Diagram";
|
|
273
|
+
if (!window.svg || !window.Frame || !window.Rectangle) {
|
|
274
|
+
console.error("[AIRenderer] Engine not initialized");
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
const vb = window.currentViewBox || { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight };
|
|
278
|
+
const vcx = vb.x + vb.width / 2;
|
|
279
|
+
const vcy = vb.y + vb.height / 2;
|
|
280
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
281
|
+
nodes.forEach((n) => {
|
|
282
|
+
minX = Math.min(minX, n.x);
|
|
283
|
+
minY = Math.min(minY, n.y);
|
|
284
|
+
maxX = Math.max(maxX, n.x + (n.width || NODE_W));
|
|
285
|
+
maxY = Math.max(maxY, n.y + (n.height || NODE_H));
|
|
286
|
+
});
|
|
287
|
+
const dw = maxX - minX, dh = maxY - minY;
|
|
288
|
+
const ox = vcx - dw / 2 - minX;
|
|
289
|
+
const oy = vcy - dh / 2 - minY;
|
|
290
|
+
const groupId = `aidiag-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
|
|
291
|
+
const frame = {
|
|
292
|
+
addShapeToFrame(s) {
|
|
293
|
+
if (s) s.groupId = groupId;
|
|
294
|
+
},
|
|
295
|
+
selectFrame() {
|
|
296
|
+
},
|
|
297
|
+
removeSelection() {
|
|
298
|
+
},
|
|
299
|
+
shapeName: "frame"
|
|
300
|
+
};
|
|
301
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
302
|
+
for (const node of nodes) {
|
|
303
|
+
const nx = node.x + ox, ny = node.y + oy;
|
|
304
|
+
const nw = node.width || NODE_W, nh = node.height || NODE_H;
|
|
305
|
+
const cx = nx + nw / 2, cy = ny + nh / 2;
|
|
306
|
+
let shape = null;
|
|
307
|
+
const nodeOpts = {
|
|
308
|
+
stroke: node.stroke || "#e0e0e0",
|
|
309
|
+
strokeWidth: node.strokeWidth ?? 1.5,
|
|
310
|
+
fill: node.fill || "transparent",
|
|
311
|
+
fillStyle: node.fillStyle || "none",
|
|
312
|
+
roughness: node.roughness ?? 1,
|
|
313
|
+
strokeDasharray: node.strokeDasharray || "",
|
|
314
|
+
// Shading support
|
|
315
|
+
shadeColor: node.shadeColor || null,
|
|
316
|
+
shadeOpacity: node.shadeOpacity !== void 0 ? node.shadeOpacity : 0.15,
|
|
317
|
+
shadeDirection: node.shadeDirection || "bottom",
|
|
318
|
+
// Label styling
|
|
319
|
+
labelColor: node.labelColor || void 0,
|
|
320
|
+
labelFontSize: node.labelFontSize || void 0
|
|
321
|
+
};
|
|
322
|
+
try {
|
|
323
|
+
if (node.type === "icon" && node.iconKeyword) {
|
|
324
|
+
shape = new window.Rectangle(nx, ny, nw, nh, {
|
|
325
|
+
...nodeOpts,
|
|
326
|
+
stroke: nodeOpts.stroke,
|
|
327
|
+
strokeDasharray: "4 3"
|
|
328
|
+
});
|
|
329
|
+
fetchAndPlaceIcon(node.iconKeyword, nx, ny, nw, nh, shape, frame);
|
|
330
|
+
} else if (node.type === "circle" && window.Circle) {
|
|
331
|
+
shape = new window.Circle(cx, cy, nw / 2, nh / 2, nodeOpts);
|
|
332
|
+
} else if (node.type === "diamond" && window.Rectangle) {
|
|
333
|
+
const sz = Math.max(nw, nh) * 0.7;
|
|
334
|
+
shape = new window.Rectangle(cx - sz / 2, cy - sz / 2, sz, sz, nodeOpts);
|
|
335
|
+
shape.rotation = 45;
|
|
336
|
+
shape.draw();
|
|
337
|
+
} else if (node.type === "roundrect" && window.Rectangle) {
|
|
338
|
+
shape = new window.Rectangle(nx, ny, nw, nh, { ...nodeOpts, cornerRadius: Math.min(nw, nh) * 0.2 });
|
|
339
|
+
} else if (window.Rectangle) {
|
|
340
|
+
shape = new window.Rectangle(nx, ny, nw, nh, nodeOpts);
|
|
341
|
+
}
|
|
342
|
+
} catch (err) {
|
|
343
|
+
console.warn("[AIRenderer] Node creation failed:", node.id, err);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (!shape) continue;
|
|
347
|
+
if (node.rotation && shape.rotation !== void 0 && node.type !== "diamond") {
|
|
348
|
+
shape.rotation = node.rotation;
|
|
349
|
+
}
|
|
350
|
+
window.shapes.push(shape);
|
|
351
|
+
if (window.pushCreateAction) window.pushCreateAction(shape);
|
|
352
|
+
if (frame.addShapeToFrame) frame.addShapeToFrame(shape);
|
|
353
|
+
nodeMap.set(node.id, { shape, x: nx, y: ny, width: nw, height: nh, centerX: cx, centerY: cy });
|
|
354
|
+
if (node.label) {
|
|
355
|
+
let labelColor = node.labelColor || node.stroke || "#e0e0e0";
|
|
356
|
+
if (isColorTooDark(labelColor)) {
|
|
357
|
+
labelColor = "#e0e0e0";
|
|
358
|
+
}
|
|
359
|
+
const labelFontSize = node.labelFontSize || 14;
|
|
360
|
+
if (node.type === "icon") {
|
|
361
|
+
const labelY = cy + nh / 2 + 18;
|
|
362
|
+
createLabel(node.label, cx, labelY, labelFontSize, labelColor, frame);
|
|
363
|
+
} else if (shape && typeof shape.setLabel === "function") {
|
|
364
|
+
shape.setLabel(node.label, labelColor, labelFontSize);
|
|
365
|
+
} else {
|
|
366
|
+
createLabel(node.label, cx, cy, labelFontSize, labelColor, frame);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const fanOut = /* @__PURE__ */ new Map();
|
|
371
|
+
const fanIdx = /* @__PURE__ */ new Map();
|
|
372
|
+
edges.forEach((e) => {
|
|
373
|
+
fanOut.set(e.from, (fanOut.get(e.from) || 0) + 1);
|
|
374
|
+
fanIdx.set(e, fanOut.get(e.from) - 1);
|
|
375
|
+
});
|
|
376
|
+
const allNodeBounds = [];
|
|
377
|
+
nodeMap.forEach((n) => {
|
|
378
|
+
allNodeBounds.push({ x: n.x, y: n.y, width: n.width, height: n.height });
|
|
379
|
+
});
|
|
380
|
+
for (const edge of edges) {
|
|
381
|
+
const from = nodeMap.get(edge.from), to = nodeMap.get(edge.to);
|
|
382
|
+
if (!from || !to) continue;
|
|
383
|
+
const count = fanOut.get(edge.from) || 1;
|
|
384
|
+
const idx = fanIdx.get(edge);
|
|
385
|
+
const isDirected = edge.directed !== false;
|
|
386
|
+
const edgeStyle = chooseEdgeStyle(from, to, count, idx, allNodeBounds, nodeMap, edges);
|
|
387
|
+
const sp = getSpreadEdgePoint(from, to, count, idx);
|
|
388
|
+
const ep = getEdgePoint(to, from);
|
|
389
|
+
const adx = ep.x - sp.x, ady = ep.y - sp.y;
|
|
390
|
+
const alen = Math.sqrt(adx * adx + ady * ady) || 1;
|
|
391
|
+
const nudge = 6;
|
|
392
|
+
const spN = { x: sp.x + adx / alen * nudge, y: sp.y + ady / alen * nudge };
|
|
393
|
+
const epN = { x: ep.x - adx / alen * nudge, y: ep.y - ady / alen * nudge };
|
|
394
|
+
let connector = null;
|
|
395
|
+
const edgeStroke = edge.stroke || "#e0e0e0";
|
|
396
|
+
const edgeStrokeWidth = edge.strokeWidth ?? 1.5;
|
|
397
|
+
const edgeLineStyle = edge.lineStyle || "solid";
|
|
398
|
+
const dashMap = { solid: "", dashed: "5 3", dotted: "2 2" };
|
|
399
|
+
const dashValue = dashMap[edgeLineStyle] || "";
|
|
400
|
+
if (isDirected && window.Arrow) {
|
|
401
|
+
try {
|
|
402
|
+
const opts = {
|
|
403
|
+
stroke: edgeStroke,
|
|
404
|
+
strokeWidth: edgeStrokeWidth,
|
|
405
|
+
roughness: 1,
|
|
406
|
+
arrowOutlineStyle: edgeLineStyle,
|
|
407
|
+
arrowHeadStyle: edge.arrowHeadStyle || "default"
|
|
408
|
+
};
|
|
409
|
+
if (edgeStyle.type === "curved") {
|
|
410
|
+
opts.arrowCurved = "curved";
|
|
411
|
+
opts.arrowCurveAmount = edgeStyle.curveAmount;
|
|
412
|
+
} else if (edgeStyle.type === "elbow") {
|
|
413
|
+
opts.arrowCurved = "elbow";
|
|
414
|
+
}
|
|
415
|
+
connector = new window.Arrow(spN, epN, opts);
|
|
416
|
+
window.shapes.push(connector);
|
|
417
|
+
if (window.pushCreateAction) window.pushCreateAction(connector);
|
|
418
|
+
if (frame.addShapeToFrame) frame.addShapeToFrame(connector);
|
|
419
|
+
autoAttach(connector, from.shape, true, sp);
|
|
420
|
+
autoAttach(connector, to.shape, false, ep);
|
|
421
|
+
} catch (err) {
|
|
422
|
+
console.warn("[AIRenderer] Arrow creation failed:", edge, err);
|
|
423
|
+
}
|
|
424
|
+
} else if (!isDirected && window.Line) {
|
|
425
|
+
try {
|
|
426
|
+
const opts = {
|
|
427
|
+
stroke: edgeStroke,
|
|
428
|
+
strokeWidth: edgeStrokeWidth,
|
|
429
|
+
roughness: 1,
|
|
430
|
+
strokeDasharray: dashValue
|
|
431
|
+
};
|
|
432
|
+
connector = new window.Line(spN, epN, opts);
|
|
433
|
+
if (edgeStyle.type === "curved") {
|
|
434
|
+
connector.isCurved = true;
|
|
435
|
+
if (typeof connector.initializeCurveControlPoint === "function") {
|
|
436
|
+
connector.initializeCurveControlPoint();
|
|
437
|
+
}
|
|
438
|
+
if (typeof connector.draw === "function") connector.draw();
|
|
439
|
+
}
|
|
440
|
+
window.shapes.push(connector);
|
|
441
|
+
if (window.pushCreateAction) window.pushCreateAction(connector);
|
|
442
|
+
if (frame.addShapeToFrame) frame.addShapeToFrame(connector);
|
|
443
|
+
} catch (err) {
|
|
444
|
+
console.warn("[AIRenderer] Line creation failed:", edge, err);
|
|
445
|
+
}
|
|
446
|
+
} else if (window.Arrow) {
|
|
447
|
+
try {
|
|
448
|
+
connector = new window.Arrow(spN, epN, {
|
|
449
|
+
stroke: edgeStroke,
|
|
450
|
+
strokeWidth: edgeStrokeWidth,
|
|
451
|
+
roughness: 1
|
|
452
|
+
});
|
|
453
|
+
window.shapes.push(connector);
|
|
454
|
+
if (window.pushCreateAction) window.pushCreateAction(connector);
|
|
455
|
+
if (frame.addShapeToFrame) frame.addShapeToFrame(connector);
|
|
456
|
+
} catch (err) {
|
|
457
|
+
console.warn("[AIRenderer] Fallback arrow creation failed:", edge, err);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (edge.label && connector) {
|
|
461
|
+
const edgeLabelColor = edgeStroke === "#e0e0e0" ? "#a0a0b0" : edgeStroke;
|
|
462
|
+
if (typeof connector.setLabel === "function") {
|
|
463
|
+
connector.setLabel(edge.label, edgeLabelColor, 11);
|
|
464
|
+
} else {
|
|
465
|
+
const mx = (spN.x + epN.x) / 2;
|
|
466
|
+
const my = (spN.y + epN.y) / 2 - 18;
|
|
467
|
+
createLabel(edge.label, mx, my, 11, edgeLabelColor, frame);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
const subgraphs = diagram.subgraphs || [];
|
|
472
|
+
for (const sg of subgraphs) {
|
|
473
|
+
if (!sg.nodes || sg.nodes.length === 0) continue;
|
|
474
|
+
let sgMinX = Infinity, sgMinY = Infinity, sgMaxX = -Infinity, sgMaxY = -Infinity;
|
|
475
|
+
let hasNodes = false;
|
|
476
|
+
for (const nid of sg.nodes) {
|
|
477
|
+
const n = nodeMap.get(nid);
|
|
478
|
+
if (!n) continue;
|
|
479
|
+
hasNodes = true;
|
|
480
|
+
sgMinX = Math.min(sgMinX, n.x);
|
|
481
|
+
sgMinY = Math.min(sgMinY, n.y);
|
|
482
|
+
sgMaxX = Math.max(sgMaxX, n.x + n.width);
|
|
483
|
+
sgMaxY = Math.max(sgMaxY, n.y + n.height);
|
|
484
|
+
}
|
|
485
|
+
if (!hasNodes) continue;
|
|
486
|
+
const sgPad = 30;
|
|
487
|
+
try {
|
|
488
|
+
const subFrame = new window.Frame(
|
|
489
|
+
sgMinX - sgPad,
|
|
490
|
+
sgMinY - sgPad - 20,
|
|
491
|
+
sgMaxX - sgMinX + sgPad * 2,
|
|
492
|
+
sgMaxY - sgMinY + sgPad * 2 + 20,
|
|
493
|
+
{
|
|
494
|
+
stroke: sg.stroke || "#555",
|
|
495
|
+
strokeWidth: 1,
|
|
496
|
+
fill: "transparent",
|
|
497
|
+
opacity: 0.6,
|
|
498
|
+
frameName: sg.label || sg.id
|
|
499
|
+
}
|
|
500
|
+
);
|
|
501
|
+
window.shapes.push(subFrame);
|
|
502
|
+
if (window.pushCreateAction) window.pushCreateAction(subFrame);
|
|
503
|
+
if (frame.addShapeToFrame) frame.addShapeToFrame(subFrame);
|
|
504
|
+
for (const nid of sg.nodes) {
|
|
505
|
+
const n = nodeMap.get(nid);
|
|
506
|
+
if (n && n.shape && subFrame.addShapeToFrame) {
|
|
507
|
+
subFrame.addShapeToFrame(n.shape);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
} catch (err) {
|
|
511
|
+
console.warn("[AIRenderer] Subgraph frame failed:", sg.id, err);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const first = nodeMap.values().next().value;
|
|
515
|
+
if (first && first.shape) {
|
|
516
|
+
window.currentShape = first.shape;
|
|
517
|
+
if (typeof first.shape.selectShape === "function") first.shape.selectShape();
|
|
518
|
+
}
|
|
519
|
+
console.log(`[AIRenderer] Done: ${nodes.length} nodes, ${edges.length} edges, ${subgraphs.length} subgraphs \u2192 "${title}" (groupId=${groupId})`);
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
function chooseEdgeStyle(from, to, fanCount, fanIdx, allNodeBounds, nodeMap, edges) {
|
|
523
|
+
const dx = Math.abs(from.centerX - to.centerX);
|
|
524
|
+
const dy = Math.abs(from.centerY - to.centerY);
|
|
525
|
+
if (fanCount > 1) {
|
|
526
|
+
const curveAmount = 40 + (fanIdx - (fanCount - 1) / 2) * 35;
|
|
527
|
+
return { type: "curved", curveAmount };
|
|
528
|
+
}
|
|
529
|
+
const isHAligned = dx < ALIGN_THRESHOLD;
|
|
530
|
+
const isVAligned = dy < ALIGN_THRESHOLD;
|
|
531
|
+
if (isHAligned || isVAligned) {
|
|
532
|
+
const sp = getEdgePoint(from, to);
|
|
533
|
+
const ep = getEdgePoint(to, from);
|
|
534
|
+
const blocked = wouldCrossNode(sp, ep, from, to, allNodeBounds);
|
|
535
|
+
if (blocked) {
|
|
536
|
+
return { type: "curved", curveAmount: 40 };
|
|
537
|
+
}
|
|
538
|
+
return { type: "straight" };
|
|
539
|
+
}
|
|
540
|
+
const isGridLike = dx > ALIGN_THRESHOLD * 2 && dy > ALIGN_THRESHOLD * 2;
|
|
541
|
+
if (isGridLike) {
|
|
542
|
+
const elbowMidX = to.centerX;
|
|
543
|
+
const elbowMidY = from.centerY;
|
|
544
|
+
const elbowMid = { x: elbowMidX, y: elbowMidY };
|
|
545
|
+
const seg1Blocked = wouldCrossNode(
|
|
546
|
+
{ x: from.centerX, y: from.centerY },
|
|
547
|
+
elbowMid,
|
|
548
|
+
from,
|
|
549
|
+
to,
|
|
550
|
+
allNodeBounds
|
|
551
|
+
);
|
|
552
|
+
const seg2Blocked = wouldCrossNode(
|
|
553
|
+
elbowMid,
|
|
554
|
+
{ x: to.centerX, y: to.centerY },
|
|
555
|
+
from,
|
|
556
|
+
to,
|
|
557
|
+
allNodeBounds
|
|
558
|
+
);
|
|
559
|
+
if (!seg1Blocked && !seg2Blocked) {
|
|
560
|
+
return { type: "elbow" };
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return { type: "curved", curveAmount: 25 };
|
|
564
|
+
}
|
|
565
|
+
function wouldCrossNode(p1, p2, fromNode, toNode, allNodeBounds) {
|
|
566
|
+
const margin = 10;
|
|
567
|
+
for (const bounds of allNodeBounds) {
|
|
568
|
+
if (bounds.x === fromNode.x && bounds.y === fromNode.y) continue;
|
|
569
|
+
if (bounds.x === toNode.x && bounds.y === toNode.y) continue;
|
|
570
|
+
const bx = bounds.x - margin;
|
|
571
|
+
const by = bounds.y - margin;
|
|
572
|
+
const bw = bounds.width + margin * 2;
|
|
573
|
+
const bh = bounds.height + margin * 2;
|
|
574
|
+
if (lineIntersectsRect(p1.x, p1.y, p2.x, p2.y, bx, by, bw, bh)) {
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
function lineIntersectsRect(x1, y1, x2, y2, rx, ry, rw, rh) {
|
|
581
|
+
const left = rx, right = rx + rw, top = ry, bottom = ry + rh;
|
|
582
|
+
if (Math.max(x1, x2) < left || Math.min(x1, x2) > right) return false;
|
|
583
|
+
if (Math.max(y1, y2) < top || Math.min(y1, y2) > bottom) return false;
|
|
584
|
+
if (x1 >= left && x1 <= right && y1 >= top && y1 <= bottom) return true;
|
|
585
|
+
if (x2 >= left && x2 <= right && y2 >= top && y2 <= bottom) return true;
|
|
586
|
+
return segmentsIntersect(x1, y1, x2, y2, left, top, right, top) || segmentsIntersect(x1, y1, x2, y2, right, top, right, bottom) || segmentsIntersect(x1, y1, x2, y2, left, bottom, right, bottom) || segmentsIntersect(x1, y1, x2, y2, left, top, left, bottom);
|
|
587
|
+
}
|
|
588
|
+
function segmentsIntersect(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2) {
|
|
589
|
+
const d1 = cross(bx1, by1, bx2, by2, ax1, ay1);
|
|
590
|
+
const d2 = cross(bx1, by1, bx2, by2, ax2, ay2);
|
|
591
|
+
const d3 = cross(ax1, ay1, ax2, ay2, bx1, by1);
|
|
592
|
+
const d4 = cross(ax1, ay1, ax2, ay2, bx2, by2);
|
|
593
|
+
if ((d1 > 0 && d2 < 0 || d1 < 0 && d2 > 0) && (d3 > 0 && d4 < 0 || d3 < 0 && d4 > 0)) {
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
function cross(ax, ay, bx, by, cx, cy) {
|
|
599
|
+
return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
|
|
600
|
+
}
|
|
601
|
+
function autoAttach(arrow, shape, isStart, contactPoint) {
|
|
602
|
+
if (!arrow || !shape) return;
|
|
603
|
+
if (typeof arrow.attachToShape !== "function") return;
|
|
604
|
+
try {
|
|
605
|
+
let side, offset;
|
|
606
|
+
if (shape.shapeName === "rectangle" || shape.shapeName === "frame") {
|
|
607
|
+
const sx = shape.x, sy = shape.y, sw = shape.width, sh = shape.height;
|
|
608
|
+
const cx = sx + sw / 2, cy = sy + sh / 2;
|
|
609
|
+
const distTop = Math.abs(contactPoint.y - sy);
|
|
610
|
+
const distBottom = Math.abs(contactPoint.y - (sy + sh));
|
|
611
|
+
const distLeft = Math.abs(contactPoint.x - sx);
|
|
612
|
+
const distRight = Math.abs(contactPoint.x - (sx + sw));
|
|
613
|
+
const minDist = Math.min(distTop, distBottom, distLeft, distRight);
|
|
614
|
+
if (minDist === distTop) {
|
|
615
|
+
side = "top";
|
|
616
|
+
offset = { x: contactPoint.x - sx, y: 0, side: "top" };
|
|
617
|
+
} else if (minDist === distBottom) {
|
|
618
|
+
side = "bottom";
|
|
619
|
+
offset = { x: contactPoint.x - sx, y: sh, side: "bottom" };
|
|
620
|
+
} else if (minDist === distLeft) {
|
|
621
|
+
side = "left";
|
|
622
|
+
offset = { x: 0, y: contactPoint.y - sy, side: "left" };
|
|
623
|
+
} else {
|
|
624
|
+
side = "right";
|
|
625
|
+
offset = { x: sw, y: contactPoint.y - sy, side: "right" };
|
|
626
|
+
}
|
|
627
|
+
const attachment = { side, point: contactPoint, offset };
|
|
628
|
+
arrow.attachToShape(isStart, shape, attachment);
|
|
629
|
+
} else if (shape.shapeName === "circle") {
|
|
630
|
+
const angle = Math.atan2(
|
|
631
|
+
contactPoint.y - shape.y,
|
|
632
|
+
contactPoint.x - shape.x
|
|
633
|
+
);
|
|
634
|
+
const attachment = {
|
|
635
|
+
side: "perimeter",
|
|
636
|
+
point: contactPoint,
|
|
637
|
+
offset: { angle, side: "perimeter" }
|
|
638
|
+
};
|
|
639
|
+
arrow.attachToShape(isStart, shape, attachment);
|
|
640
|
+
}
|
|
641
|
+
} catch (err) {
|
|
642
|
+
console.warn("[AIRenderer] Auto-attach failed:", err);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
async function fetchAndPlaceIcon(keyword, x, y, w, h, placeholderShape, frame) {
|
|
646
|
+
try {
|
|
647
|
+
const res = await fetch(`/api/icons/search?q=${encodeURIComponent(keyword)}&inline=1`);
|
|
648
|
+
if (!res.ok) return;
|
|
649
|
+
const data = await res.json();
|
|
650
|
+
const results = data.results;
|
|
651
|
+
if (!results || results.length === 0 || !results[0].svg) return;
|
|
652
|
+
const svgContent = results[0].svg;
|
|
653
|
+
if (window.svg && window.IconShape) {
|
|
654
|
+
const svg = window.svg;
|
|
655
|
+
const parser = new DOMParser();
|
|
656
|
+
const doc = parser.parseFromString(svgContent, "image/svg+xml");
|
|
657
|
+
const svgEl = doc.querySelector("svg");
|
|
658
|
+
if (!svgEl) return;
|
|
659
|
+
const vbWidth = parseFloat(svgEl.getAttribute("width") || svgEl.viewBox?.baseVal?.width || 24);
|
|
660
|
+
const vbHeight = parseFloat(svgEl.getAttribute("height") || svgEl.viewBox?.baseVal?.height || 24);
|
|
661
|
+
const scale = Math.min(w / vbWidth, h / vbHeight);
|
|
662
|
+
const g = document.createElementNS(NS, "g");
|
|
663
|
+
g.setAttribute("type", "icon");
|
|
664
|
+
g.setAttribute("x", x);
|
|
665
|
+
g.setAttribute("y", y);
|
|
666
|
+
g.setAttribute("width", w);
|
|
667
|
+
g.setAttribute("height", h);
|
|
668
|
+
g.setAttribute("data-shape-x", x);
|
|
669
|
+
g.setAttribute("data-shape-y", y);
|
|
670
|
+
g.setAttribute("data-shape-width", w);
|
|
671
|
+
g.setAttribute("data-shape-height", h);
|
|
672
|
+
g.setAttribute("data-shape-rotation", "0");
|
|
673
|
+
g.setAttribute("data-viewbox-width", vbWidth);
|
|
674
|
+
g.setAttribute("data-viewbox-height", vbHeight);
|
|
675
|
+
g.setAttribute("transform", `translate(${x}, ${y}) scale(${scale})`);
|
|
676
|
+
const children = svgEl.querySelectorAll("path, circle, rect, polygon, polyline, line, ellipse");
|
|
677
|
+
children.forEach((child) => {
|
|
678
|
+
const clone = child.cloneNode(true);
|
|
679
|
+
if (!clone.getAttribute("fill") || clone.getAttribute("fill") === "none" || clone.getAttribute("fill") === "black" || clone.getAttribute("fill") === "#000" || clone.getAttribute("fill") === "#000000") {
|
|
680
|
+
clone.setAttribute("fill", "#ffffff");
|
|
681
|
+
}
|
|
682
|
+
if (clone.getAttribute("stroke") === "black" || clone.getAttribute("stroke") === "#000" || clone.getAttribute("stroke") === "#000000") {
|
|
683
|
+
clone.setAttribute("stroke", "#ffffff");
|
|
684
|
+
}
|
|
685
|
+
g.appendChild(clone);
|
|
686
|
+
});
|
|
687
|
+
const hitRect = document.createElementNS(NS, "rect");
|
|
688
|
+
hitRect.setAttribute("x", "0");
|
|
689
|
+
hitRect.setAttribute("y", "0");
|
|
690
|
+
hitRect.setAttribute("width", vbWidth);
|
|
691
|
+
hitRect.setAttribute("height", vbHeight);
|
|
692
|
+
hitRect.setAttribute("fill", "transparent");
|
|
693
|
+
hitRect.setAttribute("stroke", "none");
|
|
694
|
+
g.insertBefore(hitRect, g.firstChild);
|
|
695
|
+
svg.appendChild(g);
|
|
696
|
+
const iconShape = new window.IconShape(g);
|
|
697
|
+
window.shapes.push(iconShape);
|
|
698
|
+
if (window.pushCreateAction) window.pushCreateAction(iconShape);
|
|
699
|
+
if (frame?.addShapeToFrame) frame.addShapeToFrame(iconShape);
|
|
700
|
+
if (placeholderShape) {
|
|
701
|
+
const idx = window.shapes.indexOf(placeholderShape);
|
|
702
|
+
if (idx !== -1) window.shapes.splice(idx, 1);
|
|
703
|
+
if (placeholderShape.group && placeholderShape.group.parentNode) {
|
|
704
|
+
placeholderShape.group.parentNode.removeChild(placeholderShape.group);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
} catch (err) {
|
|
709
|
+
console.warn("[AIRenderer] Icon fetch failed for keyword:", keyword, err);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
function createLabel(text, x, y, fontSize, fill, frame) {
|
|
713
|
+
const svg = window.svg;
|
|
714
|
+
if (!svg || !window.TextShape) return null;
|
|
715
|
+
try {
|
|
716
|
+
const g = document.createElementNS(NS, "g");
|
|
717
|
+
g.setAttribute("data-type", "text-group");
|
|
718
|
+
g.setAttribute("transform", `translate(${x}, ${y})`);
|
|
719
|
+
g.setAttribute("data-x", x);
|
|
720
|
+
g.setAttribute("data-y", y);
|
|
721
|
+
const t = document.createElementNS(NS, "text");
|
|
722
|
+
t.setAttribute("x", 0);
|
|
723
|
+
t.setAttribute("y", 0);
|
|
724
|
+
t.setAttribute("text-anchor", "middle");
|
|
725
|
+
t.setAttribute("dominant-baseline", "central");
|
|
726
|
+
t.setAttribute("fill", fill);
|
|
727
|
+
t.setAttribute("font-size", fontSize);
|
|
728
|
+
t.setAttribute("font-family", "lixFont, sans-serif");
|
|
729
|
+
t.setAttribute("data-initial-font", "lixFont");
|
|
730
|
+
t.setAttribute("data-initial-color", fill);
|
|
731
|
+
t.setAttribute("data-initial-size", fontSize + "px");
|
|
732
|
+
t.textContent = text;
|
|
733
|
+
g.appendChild(t);
|
|
734
|
+
svg.appendChild(g);
|
|
735
|
+
const shape = new window.TextShape(g);
|
|
736
|
+
window.shapes.push(shape);
|
|
737
|
+
if (window.pushCreateAction) window.pushCreateAction(shape);
|
|
738
|
+
if (frame?.addShapeToFrame) frame.addShapeToFrame(shape);
|
|
739
|
+
return shape;
|
|
740
|
+
} catch (err) {
|
|
741
|
+
console.warn("[AIRenderer] Label creation failed:", err);
|
|
742
|
+
return null;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
function getSpreadEdgePoint(node, targetNode, count, idx) {
|
|
746
|
+
if (count <= 1) return getEdgePoint(node, targetNode);
|
|
747
|
+
const dx = targetNode.centerX - node.centerX;
|
|
748
|
+
const dy = targetNode.centerY - node.centerY;
|
|
749
|
+
const hw = node.width / 2;
|
|
750
|
+
const hh = node.height / 2;
|
|
751
|
+
const spread = 0.6;
|
|
752
|
+
const t = count === 1 ? 0.5 : idx / (count - 1);
|
|
753
|
+
const offset = (t - 0.5) * spread;
|
|
754
|
+
if (Math.abs(dx) < 1e-3 || Math.abs(dy) * hw > Math.abs(dx) * hh) {
|
|
755
|
+
const px2 = node.centerX + offset * node.width;
|
|
756
|
+
const py2 = dy > 0 ? node.y + node.height : node.y;
|
|
757
|
+
return { x: px2, y: py2 };
|
|
758
|
+
}
|
|
759
|
+
const px = dx > 0 ? node.x + node.width : node.x;
|
|
760
|
+
const py = node.centerY + offset * node.height;
|
|
761
|
+
return { x: px, y: py };
|
|
762
|
+
}
|
|
763
|
+
function getEdgePoint(node, targetNode) {
|
|
764
|
+
const dx = targetNode.centerX - node.centerX;
|
|
765
|
+
const dy = targetNode.centerY - node.centerY;
|
|
766
|
+
const hw = node.width / 2;
|
|
767
|
+
const hh = node.height / 2;
|
|
768
|
+
if (Math.abs(dx) < 1e-3 || Math.abs(dy) * hw > Math.abs(dx) * hh) {
|
|
769
|
+
if (dy > 0) return { x: node.centerX, y: node.y + node.height };
|
|
770
|
+
return { x: node.centerX, y: node.y };
|
|
771
|
+
}
|
|
772
|
+
if (dx > 0) return { x: node.x + node.width, y: node.centerY };
|
|
773
|
+
return { x: node.x, y: node.centerY };
|
|
774
|
+
}
|
|
775
|
+
function generatePreviewSVG(diagram, width = 500, height = 350) {
|
|
776
|
+
if (!diagram?.nodes?.length) return "";
|
|
777
|
+
const nodes = diagram.nodes;
|
|
778
|
+
const edges = diagram.edges || [];
|
|
779
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
780
|
+
nodes.forEach((n) => {
|
|
781
|
+
minX = Math.min(minX, n.x);
|
|
782
|
+
minY = Math.min(minY, n.y);
|
|
783
|
+
maxX = Math.max(maxX, n.x + (n.width || NODE_W));
|
|
784
|
+
maxY = Math.max(maxY, n.y + (n.height || NODE_H));
|
|
785
|
+
});
|
|
786
|
+
const dw = maxX - minX || 1;
|
|
787
|
+
const dh = maxY - minY || 1;
|
|
788
|
+
const pad = 40;
|
|
789
|
+
const scale = Math.min((width - pad * 2) / dw, (height - pad * 2) / dh, 1.5);
|
|
790
|
+
const offX = (width - dw * scale) / 2 - minX * scale;
|
|
791
|
+
const offY = (height - dh * scale) / 2 - minY * scale;
|
|
792
|
+
const nodeById = /* @__PURE__ */ new Map();
|
|
793
|
+
nodes.forEach((n) => {
|
|
794
|
+
const nx = n.x * scale + offX;
|
|
795
|
+
const ny = n.y * scale + offY;
|
|
796
|
+
const nw = (n.width || NODE_W) * scale;
|
|
797
|
+
const nh = (n.height || NODE_H) * scale;
|
|
798
|
+
nodeById.set(n.id, { x: nx, y: ny, w: nw, h: nh, cx: nx + nw / 2, cy: ny + nh / 2 });
|
|
799
|
+
});
|
|
800
|
+
let svgContent = "";
|
|
801
|
+
edges.forEach((e) => {
|
|
802
|
+
const f = nodeById.get(e.from);
|
|
803
|
+
const t = nodeById.get(e.to);
|
|
804
|
+
if (!f || !t) return;
|
|
805
|
+
const directed = e.directed !== false;
|
|
806
|
+
const eColor = e.stroke || "#666";
|
|
807
|
+
const markerId = directed ? `url(#preview-arrow-${eColor.replace("#", "")})` : "";
|
|
808
|
+
if (directed && !svgContent.includes(`id="preview-arrow-${eColor.replace("#", "")}"`)) {
|
|
809
|
+
svgContent = `<marker id="preview-arrow-${eColor.replace("#", "")}" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="none" stroke="${eColor}" stroke-width="1" /></marker>` + svgContent;
|
|
810
|
+
}
|
|
811
|
+
const eDash = e.lineStyle === "dashed" ? ' stroke-dasharray="5 3"' : e.lineStyle === "dotted" ? ' stroke-dasharray="2 2"' : "";
|
|
812
|
+
const dx = t.cx - f.cx;
|
|
813
|
+
const dy = t.cy - f.cy;
|
|
814
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
815
|
+
let mx = (f.cx + t.cx) / 2;
|
|
816
|
+
let my = (f.cy + t.cy) / 2;
|
|
817
|
+
if (dist > 0 && Math.abs(dy) > 10 && Math.abs(dx) > 10) {
|
|
818
|
+
const perpX = -dy / dist;
|
|
819
|
+
const perpY = dx / dist;
|
|
820
|
+
const curveAmt = dist * 0.15;
|
|
821
|
+
const cpx = mx + perpX * curveAmt;
|
|
822
|
+
const cpy = my + perpY * curveAmt;
|
|
823
|
+
mx = 0.25 * f.cx + 0.5 * cpx + 0.25 * t.cx;
|
|
824
|
+
my = 0.25 * f.cy + 0.5 * cpy + 0.25 * t.cy;
|
|
825
|
+
svgContent += `<path d="M ${f.cx} ${f.cy} Q ${cpx} ${cpy} ${t.cx} ${t.cy}" fill="none" stroke="${eColor}" stroke-width="1.5"${eDash} marker-end="${markerId}" />`;
|
|
826
|
+
} else {
|
|
827
|
+
svgContent += `<line x1="${f.cx}" y1="${f.cy}" x2="${t.cx}" y2="${t.cy}" stroke="${eColor}" stroke-width="1.5"${eDash} marker-end="${markerId}" />`;
|
|
828
|
+
}
|
|
829
|
+
if (e.label) {
|
|
830
|
+
svgContent += `<text x="${mx}" y="${my - 8}" text-anchor="middle" fill="${eColor === "#666" ? "#888" : eColor}" font-size="9" font-family="lixFont, sans-serif">${escapeXml(e.label)}</text>`;
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
nodes.forEach((n) => {
|
|
834
|
+
const d = nodeById.get(n.id);
|
|
835
|
+
if (!d) return;
|
|
836
|
+
const nStroke = n.stroke || "#9090c0";
|
|
837
|
+
const nFill = n.fill || "transparent";
|
|
838
|
+
const nDash = n.strokeDasharray ? ` stroke-dasharray="${n.strokeDasharray}"` : "";
|
|
839
|
+
if (n.type === "circle") {
|
|
840
|
+
svgContent += `<ellipse cx="${d.cx}" cy="${d.cy}" rx="${d.w / 2}" ry="${d.h / 2}" fill="${nFill}" stroke="${nStroke}" stroke-width="1.5"${nDash} />`;
|
|
841
|
+
} else if (n.type === "diamond") {
|
|
842
|
+
const sz = Math.min(d.w, d.h) * 0.7;
|
|
843
|
+
svgContent += `<rect x="${d.cx - sz / 2}" y="${d.cy - sz / 2}" width="${sz}" height="${sz}" fill="${nFill}" stroke="${nStroke}" stroke-width="1.5"${nDash} transform="rotate(45, ${d.cx}, ${d.cy})" />`;
|
|
844
|
+
} else if (n.type === "roundrect") {
|
|
845
|
+
svgContent += `<rect x="${d.x}" y="${d.y}" width="${d.w}" height="${d.h}" rx="${Math.min(d.w, d.h) * 0.2}" fill="${nFill}" stroke="${nStroke}" stroke-width="1.5"${nDash} />`;
|
|
846
|
+
} else {
|
|
847
|
+
svgContent += `<rect x="${d.x}" y="${d.y}" width="${d.w}" height="${d.h}" rx="4" fill="${nFill}" stroke="${nStroke}" stroke-width="1.5"${nDash} />`;
|
|
848
|
+
}
|
|
849
|
+
if (n.label) {
|
|
850
|
+
const fontSize = Math.max(8, Math.min(12, 11 * scale));
|
|
851
|
+
let labelFill = nStroke === "#9090c0" ? "#d0d0d0" : nStroke;
|
|
852
|
+
if (isColorTooDark(labelFill)) labelFill = "#d0d0d0";
|
|
853
|
+
const labelY = n.type === "icon" ? d.cy + d.h / 2 + 12 * scale : d.cy;
|
|
854
|
+
svgContent += `<text x="${d.cx}" y="${labelY}" text-anchor="middle" dominant-baseline="central" fill="${labelFill}" font-size="${fontSize}" font-family="lixFont, sans-serif">${escapeXml(n.label)}</text>`;
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
|
858
|
+
<defs>${svgContent.match(/<marker[^]*?<\/marker>/g)?.join("") || ""}</defs>
|
|
859
|
+
${svgContent.replace(/<marker[^]*?<\/marker>/g, "")}
|
|
860
|
+
</svg>`;
|
|
861
|
+
}
|
|
862
|
+
function escapeXml(str) {
|
|
863
|
+
return String(str).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
864
|
+
}
|
|
865
|
+
function isColorTooDark(hex) {
|
|
866
|
+
if (!hex || hex === "transparent" || hex === "none") return false;
|
|
867
|
+
const c = hex.replace("#", "");
|
|
868
|
+
if (c.length < 6) return false;
|
|
869
|
+
const r = parseInt(c.substring(0, 2), 16);
|
|
870
|
+
const g = parseInt(c.substring(2, 4), 16);
|
|
871
|
+
const b = parseInt(c.substring(4, 6), 16);
|
|
872
|
+
const lum = 0.299 * r + 0.587 * g + 0.114 * b;
|
|
873
|
+
return lum < 80;
|
|
874
|
+
}
|
|
875
|
+
function generateFramePreviewSVG(frame, width = 500, height = 350) {
|
|
876
|
+
if (!frame || !frame.clipGroup) return "";
|
|
877
|
+
const fx = frame.x, fy = frame.y, fw = frame.width, fh = frame.height;
|
|
878
|
+
if (fw <= 0 || fh <= 0) return "";
|
|
879
|
+
const pad = 30;
|
|
880
|
+
const scale = Math.min((width - pad * 2) / fw, (height - pad * 2) / fh, 1.5);
|
|
881
|
+
const offX = (width - fw * scale) / 2 - fx * scale;
|
|
882
|
+
const offY = (height - fh * scale) / 2 - fy * scale;
|
|
883
|
+
let svgContent = "";
|
|
884
|
+
svgContent += `<rect x="${fx * scale + offX}" y="${fy * scale + offY}" width="${fw * scale}" height="${fh * scale}" rx="6" fill="transparent" stroke="#555" stroke-width="1.5" stroke-dasharray="6 3" />`;
|
|
885
|
+
if (frame.frameName) {
|
|
886
|
+
svgContent += `<text x="${fx * scale + offX + 10}" y="${fy * scale + offY + 16}" fill="#888" font-size="11" font-family="lixFont, sans-serif">${escapeXml(frame.frameName)}</text>`;
|
|
887
|
+
}
|
|
888
|
+
if (frame.containedShapes) {
|
|
889
|
+
frame.containedShapes.forEach((shape) => {
|
|
890
|
+
if (!shape) return;
|
|
891
|
+
if (shape.shapeName === "rectangle") {
|
|
892
|
+
const sx = shape.x * scale + offX;
|
|
893
|
+
const sy = shape.y * scale + offY;
|
|
894
|
+
const sw = shape.width * scale;
|
|
895
|
+
const sh = shape.height * scale;
|
|
896
|
+
const stroke = shape.options?.stroke || "#e0e0e0";
|
|
897
|
+
const fill = shape.options?.fill || "transparent";
|
|
898
|
+
if (shape.rotation === 45) {
|
|
899
|
+
const sz = Math.min(sw, sh) * 0.7;
|
|
900
|
+
const rcx = sx + sw / 2, rcy = sy + sh / 2;
|
|
901
|
+
svgContent += `<rect x="${rcx - sz / 2}" y="${rcy - sz / 2}" width="${sz}" height="${sz}" fill="${fill}" stroke="${stroke}" stroke-width="1.5" transform="rotate(45, ${rcx}, ${rcy})" />`;
|
|
902
|
+
if (shape.label) {
|
|
903
|
+
let lf = shape.labelColor || stroke;
|
|
904
|
+
if (isColorTooDark(lf)) lf = "#d0d0d0";
|
|
905
|
+
const fs = Math.max(8, (shape.labelFontSize || 14) * scale);
|
|
906
|
+
svgContent += `<text x="${rcx}" y="${rcy}" text-anchor="middle" dominant-baseline="central" fill="${lf}" font-size="${fs}" font-family="lixFont, sans-serif">${escapeXml(shape.label)}</text>`;
|
|
907
|
+
}
|
|
908
|
+
} else {
|
|
909
|
+
svgContent += `<rect x="${sx}" y="${sy}" width="${sw}" height="${sh}" rx="4" fill="${fill}" stroke="${stroke}" stroke-width="1.5" />`;
|
|
910
|
+
if (shape.label) {
|
|
911
|
+
let lf = shape.labelColor || stroke;
|
|
912
|
+
if (isColorTooDark(lf)) lf = "#d0d0d0";
|
|
913
|
+
const fs = Math.max(8, (shape.labelFontSize || 14) * scale);
|
|
914
|
+
svgContent += `<text x="${sx + sw / 2}" y="${sy + sh / 2}" text-anchor="middle" dominant-baseline="central" fill="${lf}" font-size="${fs}" font-family="lixFont, sans-serif">${escapeXml(shape.label)}</text>`;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
} else if (shape.shapeName === "circle") {
|
|
918
|
+
const ccx = shape.x * scale + offX;
|
|
919
|
+
const ccy = shape.y * scale + offY;
|
|
920
|
+
const crx = (shape.rx || 30) * scale;
|
|
921
|
+
const cry = (shape.ry || 30) * scale;
|
|
922
|
+
const stroke = shape.options?.stroke || "#e0e0e0";
|
|
923
|
+
const fill = shape.options?.fill || "transparent";
|
|
924
|
+
svgContent += `<ellipse cx="${ccx}" cy="${ccy}" rx="${crx}" ry="${cry}" fill="${fill}" stroke="${stroke}" stroke-width="1.5" />`;
|
|
925
|
+
if (shape.label) {
|
|
926
|
+
let lf = shape.labelColor || stroke;
|
|
927
|
+
if (isColorTooDark(lf)) lf = "#d0d0d0";
|
|
928
|
+
const fs = Math.max(8, (shape.labelFontSize || 14) * scale);
|
|
929
|
+
svgContent += `<text x="${ccx}" y="${ccy}" text-anchor="middle" dominant-baseline="central" fill="${lf}" font-size="${fs}" font-family="lixFont, sans-serif">${escapeXml(shape.label)}</text>`;
|
|
930
|
+
}
|
|
931
|
+
} else if (shape.shapeName === "text") {
|
|
932
|
+
const textEl = shape.group?.querySelector("text");
|
|
933
|
+
if (textEl) {
|
|
934
|
+
const tx = parseFloat(shape.group.getAttribute("data-x") || "0");
|
|
935
|
+
const ty = parseFloat(shape.group.getAttribute("data-y") || "0");
|
|
936
|
+
const fill = textEl.getAttribute("fill") || "#e0e0e0";
|
|
937
|
+
const fontSize = Math.max(8, (parseFloat(textEl.getAttribute("font-size")) || 14) * scale);
|
|
938
|
+
const text = textEl.textContent || "";
|
|
939
|
+
let labelFill = fill;
|
|
940
|
+
if (isColorTooDark(labelFill)) labelFill = "#d0d0d0";
|
|
941
|
+
svgContent += `<text x="${tx * scale + offX}" y="${ty * scale + offY}" text-anchor="middle" dominant-baseline="central" fill="${labelFill}" font-size="${fontSize}" font-family="lixFont, sans-serif">${escapeXml(text)}</text>`;
|
|
942
|
+
}
|
|
943
|
+
} else if (shape.shapeName === "arrow") {
|
|
944
|
+
const sp = shape.startPoint, ep = shape.endPoint;
|
|
945
|
+
if (sp && ep) {
|
|
946
|
+
const stroke = shape.options?.stroke || "#e0e0e0";
|
|
947
|
+
const sx1 = sp.x * scale + offX, sy1 = sp.y * scale + offY;
|
|
948
|
+
const sx2 = ep.x * scale + offX, sy2 = ep.y * scale + offY;
|
|
949
|
+
if (shape.arrowCurved === "curved" && shape.controlPoint1 && shape.controlPoint2) {
|
|
950
|
+
const cp1x = shape.controlPoint1.x * scale + offX;
|
|
951
|
+
const cp1y = shape.controlPoint1.y * scale + offY;
|
|
952
|
+
const cp2x = shape.controlPoint2.x * scale + offX;
|
|
953
|
+
const cp2y = shape.controlPoint2.y * scale + offY;
|
|
954
|
+
svgContent += `<path d="M ${sx1} ${sy1} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${sx2} ${sy2}" fill="none" stroke="${stroke}" stroke-width="1.5" marker-end="url(#frame-preview-arrow)" />`;
|
|
955
|
+
} else {
|
|
956
|
+
svgContent += `<line x1="${sx1}" y1="${sy1}" x2="${sx2}" y2="${sy2}" stroke="${stroke}" stroke-width="1.5" marker-end="url(#frame-preview-arrow)" />`;
|
|
957
|
+
}
|
|
958
|
+
if (shape.label) {
|
|
959
|
+
const amx = (sx1 + sx2) / 2, amy = (sy1 + sy2) / 2 - 8;
|
|
960
|
+
const afs = Math.max(7, (shape.labelFontSize || 11) * scale);
|
|
961
|
+
svgContent += `<text x="${amx}" y="${amy}" text-anchor="middle" fill="${shape.labelColor || stroke}" font-size="${afs}" font-family="lixFont, sans-serif">${escapeXml(shape.label)}</text>`;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
} else if (shape.shapeName === "line") {
|
|
965
|
+
const sp = shape.startPoint, ep = shape.endPoint;
|
|
966
|
+
if (sp && ep) {
|
|
967
|
+
const stroke = shape.options?.stroke || "#e0e0e0";
|
|
968
|
+
const sx1 = sp.x * scale + offX, sy1 = sp.y * scale + offY;
|
|
969
|
+
const sx2 = ep.x * scale + offX, sy2 = ep.y * scale + offY;
|
|
970
|
+
if (shape.isCurved && shape.controlPoint) {
|
|
971
|
+
const cpx = shape.controlPoint.x * scale + offX;
|
|
972
|
+
const cpy = shape.controlPoint.y * scale + offY;
|
|
973
|
+
svgContent += `<path d="M ${sx1} ${sy1} Q ${cpx} ${cpy} ${sx2} ${sy2}" fill="none" stroke="${stroke}" stroke-width="1.5" />`;
|
|
974
|
+
} else {
|
|
975
|
+
svgContent += `<line x1="${sx1}" y1="${sy1}" x2="${sx2}" y2="${sy2}" stroke="${stroke}" stroke-width="1.5" />`;
|
|
976
|
+
}
|
|
977
|
+
if (shape.label) {
|
|
978
|
+
const lmx = (sx1 + sx2) / 2, lmy = (sy1 + sy2) / 2 - 8;
|
|
979
|
+
const lfs = Math.max(7, (shape.labelFontSize || 11) * scale);
|
|
980
|
+
svgContent += `<text x="${lmx}" y="${lmy}" text-anchor="middle" fill="${shape.labelColor || stroke}" font-size="${lfs}" font-family="lixFont, sans-serif">${escapeXml(shape.label)}</text>`;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
} else if (shape.shapeName === "icon") {
|
|
984
|
+
const ix = parseFloat(shape.group?.getAttribute("data-shape-x") || "0");
|
|
985
|
+
const iy = parseFloat(shape.group?.getAttribute("data-shape-y") || "0");
|
|
986
|
+
const iw = parseFloat(shape.group?.getAttribute("data-shape-width") || "40");
|
|
987
|
+
const ih = parseFloat(shape.group?.getAttribute("data-shape-height") || "40");
|
|
988
|
+
const pix = ix * scale + offX;
|
|
989
|
+
const piy = iy * scale + offY;
|
|
990
|
+
const piw = iw * scale;
|
|
991
|
+
const pih = ih * scale;
|
|
992
|
+
const iconPaths = shape.group?.querySelectorAll("path, circle, rect, polygon, polyline, line, ellipse");
|
|
993
|
+
if (iconPaths && iconPaths.length > 0) {
|
|
994
|
+
const vbW = parseFloat(shape.group?.getAttribute("data-viewbox-width") || iw);
|
|
995
|
+
const vbH = parseFloat(shape.group?.getAttribute("data-viewbox-height") || ih);
|
|
996
|
+
const iconScale = Math.min(piw / vbW, pih / vbH);
|
|
997
|
+
svgContent += `<g transform="translate(${pix}, ${piy}) scale(${iconScale})">`;
|
|
998
|
+
iconPaths.forEach((p) => {
|
|
999
|
+
if (p.tagName === "rect" && p.getAttribute("fill") === "transparent" && p.getAttribute("stroke") === "none") return;
|
|
1000
|
+
const clone = p.cloneNode(true);
|
|
1001
|
+
svgContent += clone.outerHTML;
|
|
1002
|
+
});
|
|
1003
|
+
svgContent += `</g>`;
|
|
1004
|
+
} else {
|
|
1005
|
+
svgContent += `<rect x="${pix}" y="${piy}" width="${piw}" height="${pih}" rx="4" fill="transparent" stroke="#9090c0" stroke-width="1" stroke-dasharray="3 2" />`;
|
|
1006
|
+
svgContent += `<text x="${pix + piw / 2}" y="${piy + pih / 2}" text-anchor="middle" dominant-baseline="central" fill="#9090c0" font-size="9" font-family="lixFont">icon</text>`;
|
|
1007
|
+
}
|
|
1008
|
+
} else if (shape.shapeName === "frame") {
|
|
1009
|
+
const sx = shape.x * scale + offX;
|
|
1010
|
+
const sy = shape.y * scale + offY;
|
|
1011
|
+
const sw = shape.width * scale;
|
|
1012
|
+
const sh = shape.height * scale;
|
|
1013
|
+
svgContent += `<rect x="${sx}" y="${sy}" width="${sw}" height="${sh}" rx="4" fill="transparent" stroke="${shape.options?.stroke || "#555"}" stroke-width="1" stroke-dasharray="4 2" opacity="0.6" />`;
|
|
1014
|
+
if (shape.frameName) {
|
|
1015
|
+
svgContent += `<text x="${sx + 6}" y="${sy + 12}" fill="#888" font-size="9" font-family="lixFont">${escapeXml(shape.frameName)}</text>`;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
|
1021
|
+
<defs><marker id="frame-preview-arrow" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="none" stroke="#e0e0e0" stroke-width="1" /></marker></defs>
|
|
1022
|
+
${svgContent}
|
|
1023
|
+
</svg>`;
|
|
1024
|
+
}
|
|
1025
|
+
function initAIRenderer() {
|
|
1026
|
+
window.__autoAttach = autoAttach;
|
|
1027
|
+
let _seqParser = null;
|
|
1028
|
+
let _seqPreview = null;
|
|
1029
|
+
let _seqCanvas = null;
|
|
1030
|
+
let _fcPreview = null;
|
|
1031
|
+
let _fcCanvas = null;
|
|
1032
|
+
async function loadSequenceRenderer() {
|
|
1033
|
+
if (_seqParser) return;
|
|
1034
|
+
const mod = await import("./MermaidSequenceParser-UKG2NSZY.js");
|
|
1035
|
+
const rend = await import("./MermaidSequenceRenderer-HPLHY5II.js");
|
|
1036
|
+
_seqParser = mod.parseSequenceDiagram;
|
|
1037
|
+
_seqPreview = rend.renderSequencePreviewSVG;
|
|
1038
|
+
_seqCanvas = rend.renderSequenceOnCanvas;
|
|
1039
|
+
}
|
|
1040
|
+
async function loadFlowchartRenderer() {
|
|
1041
|
+
if (_fcPreview) return;
|
|
1042
|
+
const mod = await import("./MermaidFlowchartRenderer-4QBAZ5MB.js");
|
|
1043
|
+
_fcPreview = mod.renderFlowchartPreviewSVG;
|
|
1044
|
+
_fcCanvas = mod.renderFlowchartOnCanvas;
|
|
1045
|
+
}
|
|
1046
|
+
function isSequenceDiagram(src) {
|
|
1047
|
+
return src.trim().split("\n")[0].trim().toLowerCase() === "sequencediagram";
|
|
1048
|
+
}
|
|
1049
|
+
window.__aiRenderer = renderAIDiagram;
|
|
1050
|
+
window.__aiPreview = generatePreviewSVG;
|
|
1051
|
+
window.__aiFramePreview = generateFramePreviewSVG;
|
|
1052
|
+
window.__mermaidParser = (src) => {
|
|
1053
|
+
if (isSequenceDiagram(src)) {
|
|
1054
|
+
try {
|
|
1055
|
+
if (_seqParser) return _seqParser(src);
|
|
1056
|
+
loadSequenceRenderer();
|
|
1057
|
+
return { _pendingSequence: true, src };
|
|
1058
|
+
} catch {
|
|
1059
|
+
return null;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return parseMermaid(src);
|
|
1063
|
+
};
|
|
1064
|
+
window.__mermaidPreview = async (src) => {
|
|
1065
|
+
if (isSequenceDiagram(src)) {
|
|
1066
|
+
await loadSequenceRenderer();
|
|
1067
|
+
const diagram2 = _seqParser(src);
|
|
1068
|
+
if (!diagram2) return "";
|
|
1069
|
+
return _seqPreview(diagram2);
|
|
1070
|
+
}
|
|
1071
|
+
await loadFlowchartRenderer();
|
|
1072
|
+
const diagram = parseMermaid(src);
|
|
1073
|
+
if (!diagram) return "";
|
|
1074
|
+
return _fcPreview(diagram);
|
|
1075
|
+
};
|
|
1076
|
+
window.__mermaidRenderer = async (src) => {
|
|
1077
|
+
if (isSequenceDiagram(src)) {
|
|
1078
|
+
await loadSequenceRenderer();
|
|
1079
|
+
const diagram2 = _seqParser(src);
|
|
1080
|
+
if (!diagram2) {
|
|
1081
|
+
console.error("[AIRenderer] Sequence parse failed");
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
1084
|
+
return _seqCanvas(diagram2);
|
|
1085
|
+
}
|
|
1086
|
+
await loadFlowchartRenderer();
|
|
1087
|
+
const diagram = parseMermaid(src);
|
|
1088
|
+
if (!diagram) {
|
|
1089
|
+
console.error("[AIRenderer] Mermaid parse failed");
|
|
1090
|
+
return false;
|
|
1091
|
+
}
|
|
1092
|
+
return _fcCanvas(diagram);
|
|
1093
|
+
};
|
|
1094
|
+
loadSequenceRenderer();
|
|
1095
|
+
loadFlowchartRenderer();
|
|
1096
|
+
}
|
|
1097
|
+
export {
|
|
1098
|
+
autoAttach,
|
|
1099
|
+
generateFramePreviewSVG,
|
|
1100
|
+
generatePreviewSVG,
|
|
1101
|
+
initAIRenderer,
|
|
1102
|
+
parseMermaid,
|
|
1103
|
+
renderAIDiagram
|
|
1104
|
+
};
|
|
1105
|
+
//# sourceMappingURL=AIRenderer-M4LMJ66J.js.map
|