Forge
csharp4405de34
1using System.Globalization;
2using Avalonia;
3using Avalonia.Controls.Documents;
4using Avalonia.Media;
5using CascadeIDE.Cockpit.Graph.Layout;
6
7namespace CascadeIDE.Views.SkiaKit.Graph;
8
9public static partial class SkiaGraphSceneDrawing
10{
11 private static void DrawEdges(DrawingContext context, GraphLayoutScene scene, SkiaGraphVisualTheme theme)
12 {
13 var previousWasLoop = false;
14 foreach (var edge in scene.Edges)
15 {
16 var isHighlighted = scene.HighlightedEdgeKeys.Contains(edge.Key);
17 var isLoop = IsLoopEdge(edge.Kind);
18 if (isLoop && !previousWasLoop)
19 {
20 var baseLoopPen = isHighlighted ? theme.HighlightedLoopEdgePen : theme.LoopEdgePen;
21 var loopPen = ScaleLoopPen(baseLoopPen, ResolveLoopWeightScale(scene, edge), isHighlighted);
22 DrawLoopEdge(context, scene, edge, loopPen);
23 previousWasLoop = true;
24 continue;
25 }
26
27 var edgeStyle = ResolveEdgePen(theme, edge.Kind, isHighlighted);
28 var fromR = GetNodeRadius(scene, edge.FromNodeId, fallback: 12);
29 DrawCubicEdge(context, edgeStyle, edge.From, fromR, edge.To, edge.ToRadius);
30 if (!string.IsNullOrEmpty(edge.BranchLabel))
31 DrawBranchLabel(context, theme, scene, edge, fromR, edge.BranchLabel);
32 previousWasLoop = isLoop;
33 }
34 }
35
36 private static Pen ResolveEdgePen(SkiaGraphVisualTheme theme, string? kind, bool highlighted)
37 {
38 if (highlighted)
39 return theme.HighlightedEdgePen;
40 if (IsMultiBranchEdge(kind))
41 return theme.MultiBranchEdgePen;
42 if (IsConditionalEdge(kind))
43 return theme.ConditionalEdgePen;
44 if (IsExceptionFlowEdge(kind))
45 return theme.ConditionalEdgePen;
46 return theme.BaseEdgePen;
47 }
48
49 private static bool IsLoopEdge(string? kind) =>
50 !string.IsNullOrWhiteSpace(kind)
51 && kind.Contains("loop", StringComparison.OrdinalIgnoreCase);
52
53 private static bool IsMultiBranchEdge(string? kind) =>
54 !string.IsNullOrWhiteSpace(kind)
55 && kind.Contains("multibranch", StringComparison.OrdinalIgnoreCase);
56
57 private static bool IsConditionalEdge(string? kind) =>
58 !string.IsNullOrWhiteSpace(kind)
59 && kind.Contains("conditional", StringComparison.OrdinalIgnoreCase);
60
61 private static bool IsExceptionFlowEdge(string? kind) =>
62 !string.IsNullOrWhiteSpace(kind)
63 && kind.Contains("exception", StringComparison.OrdinalIgnoreCase);
64
65 /// <summary>
66 /// ADR 0053: «петля на ребре» — один штрих пунктирной кубики с усиленным боковым выносом к цели, без второго декоративного эллипса по узлу.
67 /// </summary>
68 private static void DrawLoopEdge(
69 DrawingContext context,
70 GraphLayoutScene scene,
71 GraphLayoutEdge edge,
72 Pen loopPen)
73 {
74 var fromR = GetNodeRadius(scene, edge.FromNodeId, fallback: 12);
75 DrawCubicEdge(
76 context,
77 loopPen,
78 edge.From,
79 fromR,
80 edge.To,
81 edge.ToRadius,
82 lateralBendMultiplier: LoopEdgeOrbitBendMultiplier(scene, edge));
83 }
84
85 /// <summary>Чуть сильнее «орбита» при вертикальном главном потоке и при большей группе цикла (ADR 0053).</summary>
86 private static double LoopEdgeOrbitBendMultiplier(GraphLayoutScene scene, GraphLayoutEdge edge)
87 {
88 var baseMul = scene.ControlFlowMainAxis == GraphControlFlowMainAxis.Horizontal ? 2.05 : 2.2;
89 var dx = edge.To.X - edge.From.X;
90 var dy = edge.To.Y - edge.From.Y;
91 var len = Math.Sqrt(dx * dx + dy * dy);
92 if (len > 140)
93 baseMul *= 1.05;
94 var members = ResolveLoopGroupMemberCount(scene, edge);
95 if (members > 1)
96 baseMul *= 1.0 + Math.Min(0.18, (members - 1) * 0.04);
97 return baseMul;
98 }
99
100 /// <summary>Толщина/контраст петли по числу узлов в <see cref="GraphLayoutNode.LoopGroupId"/>.</summary>
101 private static double ResolveLoopWeightScale(GraphLayoutScene scene, GraphLayoutEdge edge)
102 {
103 var members = ResolveLoopGroupMemberCount(scene, edge);
104 return members switch
105 {
106 <= 1 => 1.0,
107 2 => 1.06,
108 3 => 1.12,
109 4 => 1.17,
110 _ => 1.22
111 };
112 }
113
114 private static int ResolveLoopGroupMemberCount(GraphLayoutScene scene, GraphLayoutEdge edge)
115 {
116 int? groupId = null;
117 foreach (var n in scene.Nodes)
118 {
119 if (n.LoopGroupId is not > 0)
120 continue;
121 if (string.Equals(n.Id, edge.ToNodeId, StringComparison.OrdinalIgnoreCase)
122 || string.Equals(n.Id, edge.FromNodeId, StringComparison.OrdinalIgnoreCase))
123 {
124 groupId = n.LoopGroupId;
125 break;
126 }
127 }
128
129 if (groupId is null)
130 return 1;
131
132 var count = 0;
133 foreach (var n in scene.Nodes)
134 {
135 if (n.LoopGroupId == groupId)
136 count++;
137 }
138
139 return Math.Max(1, count);
140 }
141
142 private static Pen ScaleLoopPen(Pen source, double scale, bool highlighted)
143 {
144 if (Math.Abs(scale - 1.0) < 0.01)
145 return source;
146
147 var thickness = source.Thickness * scale;
148 IBrush brush = source.Brush ?? Brushes.White;
149 if (!highlighted && brush is SolidColorBrush scb)
150 {
151 var c = scb.Color;
152 var alphaBoost = (byte)Math.Clamp(c.A + (int)((scale - 1.0) * 48), c.A, 255);
153 brush = new SolidColorBrush(Color.FromArgb(alphaBoost, c.R, c.G, c.B));
154 }
155
156 return new Pen(brush, thickness) { DashStyle = source.DashStyle, LineCap = source.LineCap };
157 }
158
159 private static void DrawBranchLabel(
160 DrawingContext context,
161 SkiaGraphVisualTheme theme,
162 GraphLayoutScene scene,
163 GraphLayoutEdge edge,
164 double fromRadius,
165 string label)
166 {
167 var dx = edge.To.X - edge.From.X;
168 var dy = edge.To.Y - edge.From.Y;
169 var len = Math.Sqrt(dx * dx + dy * dy);
170 if (len < 1e-6)
171 return;
172 var ux = dx / len;
173 var uy = dy / len;
174 var start = new Point(edge.From.X + ux * fromRadius, edge.From.Y + uy * fromRadius);
175 var end = edge.ToRadius is { } tr
176 ? new Point(edge.To.X - ux * tr, edge.To.Y - uy * tr)
177 : edge.To;
178 var t = 0.38;
179 var anchor = new Point(start.X + (end.X - start.X) * t, start.Y + (end.Y - start.Y) * t);
180 var px = -uy;
181 var py = ux;
182 var offset = 10;
183 anchor = new Point(anchor.X + px * offset, anchor.Y + py * offset);
184
185 var fontSize = scene.SideLabelFontSizePx ?? SkiaGraphRenderInvariants.MinSideLabelFontSize;
186 var ft = new FormattedText(
187 label,
188 CultureInfo.InvariantCulture,
189 FlowDirection.LeftToRight,
190 theme.SideLabelTypeface,
191 Math.Max(8.5, fontSize * 0.92),
192 theme.SideLabelBrush);
193 context.DrawText(ft, new Point(anchor.X - ft.Width / 2, anchor.Y - ft.Height / 2));
194 }
195
196 private static double GetNodeRadius(GraphLayoutScene scene, string nodeId, double fallback)
197 {
198 foreach (var n in scene.Nodes)
199 {
200 if (string.Equals(n.Id, nodeId, StringComparison.OrdinalIgnoreCase))
201 return n.Radius;
202 }
203
204 return fallback;
205 }
206
207 private static void DrawCubicEdge(
208 DrawingContext context,
209 Pen pen,
210 Point fromCenter,
211 double fromRadius,
212 Point to,
213 double? toRadius,
214 double lateralBendMultiplier = 1.0)
215 {
216 var dx = to.X - fromCenter.X;
217 var dy = to.Y - fromCenter.Y;
218 var len = Math.Sqrt(dx * dx + dy * dy);
219 if (len < 1e-6)
220 return;
221 var ux = dx / len;
222 var uy = dy / len;
223 var start = new Point(fromCenter.X + ux * fromRadius, fromCenter.Y + uy * fromRadius);
224 Point end = toRadius is { } tr
225 ? new Point(to.X - ux * tr, to.Y - uy * tr)
226 : to;
227
228 var ex = end.X - start.X;
229 var ey = end.Y - start.Y;
230 var elen = Math.Sqrt(ex * ex + ey * ey);
231 if (elen < 1e-6)
232 return;
233 if (elen < 6)
234 {
235 context.DrawLine(pen, start, end);
236 DrawArrowHeadAtTip(context, pen, end, ex / elen, ey / elen);
237 return;
238 }
239
240 // Для почти вертикальных / последовательных шагов — прямая, кроме рёбер с принудительной орбитой (loop).
241 var horizontalDrift = Math.Abs(ex);
242 var isNearStraightFlow = horizontalDrift <= Math.Max(8, elen * 0.08);
243 if (isNearStraightFlow && lateralBendMultiplier <= 1.02)
244 {
245 context.DrawLine(pen, start, end);
246 DrawArrowHeadAtTip(context, pen, end, ex / elen, ey / elen);
247 return;
248 }
249
250 var bendCap = Math.Clamp(elen * 0.22, 18, 56);
251 var bendByDistance = Math.Min(bendCap, elen * 0.2);
252 var bendByHorizontalRoom = Math.Max(0, horizontalDrift * 0.45);
253 var crossRoom = lateralBendMultiplier > 1.02
254 ? Math.Max(bendByHorizontalRoom, Math.Clamp(elen * 0.085, 12, 44))
255 : bendByHorizontalRoom;
256 var bend = Math.Min(bendByDistance, crossRoom) * lateralBendMultiplier;
257 var px = -ey / elen;
258 var py = ex / elen;
259 var c1 = new Point(start.X + ex / 3 + px * bend, start.Y + ey / 3 + py * bend);
260 var c2 = new Point(end.X - ex / 3 + px * bend, end.Y - ey / 3 + py * bend);
261
262 var geometry = new StreamGeometry();
263 using (var ig = geometry.Open())
264 {
265 ig.BeginFigure(start, false);
266 ig.CubicBezierTo(c1, c2, end);
267 }
268
269 context.DrawGeometry(null, pen, geometry);
270 var tdx = end.X - c2.X;
271 var tdy = end.Y - c2.Y;
272 var tlen = Math.Sqrt(tdx * tdx + tdy * tdy);
273 if (tlen < 1e-6)
274 {
275 tdx = end.X - start.X;
276 tdy = end.Y - start.Y;
277 tlen = Math.Sqrt(tdx * tdx + tdy * tdy);
278 }
279
280 if (tlen >= 1e-6)
281 DrawArrowHeadAtTip(context, pen, end, tdx / tlen, tdy / tlen);
282 }
283
284 private static void DrawArrowHeadAtTip(DrawingContext context, Pen pen, Point tip, double dirX, double dirY)
285 {
286 var brush = pen.Brush ?? Brushes.White;
287 var thickness = pen.Thickness;
288 if (thickness <= 0)
289 thickness = 1;
290 var arrowLen = 6 + Math.Min(5, thickness * 1.8);
291 var halfW = arrowLen * 0.45;
292 var bx = tip.X - dirX * arrowLen;
293 var by = tip.Y - dirY * arrowLen;
294 var px = -dirY;
295 var py = dirX;
296 var p0 = new Point(bx + px * halfW, by + py * halfW);
297 var p1 = new Point(bx - px * halfW, by - py * halfW);
298 var geo = new StreamGeometry();
299 using (var ig = geo.Open())
300 {
301 ig.BeginFigure(tip, true);
302 ig.LineTo(p0);
303 ig.LineTo(p1);
304 ig.EndFigure(true);
305 }
306
307 context.DrawGeometry(brush, null, geo);
308 }
309}
310
View only · write via MCP/CIDE