Forge
csharpdeeb25a2
1#nullable enable
2using Avalonia;
3using CascadeIDE.Models;
4
5namespace CascadeIDE.Cockpit.Graph.Layout;
6
7/// <summary>Звезда: якорь в центре, спутники по окружности (<c>radial</c>).</summary>
8public sealed class StarGraphLayoutEngine : IGraphLayoutEngine
9{
10 public GraphLayoutScene Layout(
11 GraphDocument doc,
12 double width,
13 double height,
14 CodeNavigationMapDetailLevel detailLevel = CodeNavigationMapDetailLevel.Normal,
15 GraphControlFlowMainAxis? controlFlowMainAxisOverride = null,
16 GraphLayoutEngineOptions layoutOptions = default)
17 {
18 if (width <= 0 || height <= 0)
19 return EmptyScene(width);
20
21 var anchor = doc.Nodes.FirstOrDefault(n => string.Equals(n.Kind, "anchor", StringComparison.OrdinalIgnoreCase))
22 ?? doc.Nodes.FirstOrDefault(n => n.Id.Equals("n0", StringComparison.OrdinalIgnoreCase));
23 var satellites = doc.Nodes
24 .Where(n => anchor is null || !string.Equals(n.Id, anchor.Id, StringComparison.OrdinalIgnoreCase))
25 .ToList();
26
27 var margin = Math.Min(
28 GraphFileLayoutMetrics.SideLabelMargin,
29 Math.Max(28, Math.Min(width, height) * 0.22));
30 var innerW = Math.Max(80, width - 2 * margin);
31 var innerH = Math.Max(80, height - 2 * margin);
32 var cx = width / 2;
33 var cy = height / 2;
34 var minDim = Math.Min(innerW, innerH);
35 var scale = Math.Clamp(minDim / 220.0, 0.58, ResolveRelatedAutoScaleCeiling(innerW, innerH, satellites.Count));
36 var anchorR = 16 * scale;
37 var satR = 13 * scale;
38 const int singleRingMaxSatellites = 8;
39
40 var layouts = new List<GraphLayoutNode>();
41 Point? anchorCenter = null;
42 var idToCenter = new Dictionary<string, Point>(StringComparer.OrdinalIgnoreCase);
43 var idToRadius = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
44
45 if (anchor is not null)
46 {
47 var ac = new Point(cx, cy);
48 anchorCenter = ac;
49 idToCenter[anchor.Id] = ac;
50 idToRadius[anchor.Id] = anchorR;
51 layouts.Add(MakeNode(anchor, ac, anchorR, isAnchor: true));
52 }
53
54 var nSat = satellites.Count;
55 var useTwoRings = nSat > singleRingMaxSatellites;
56 var innerCount = useTwoRings ? (nSat + 1) / 2 : nSat;
57 var (orbitInner, orbitOuter) = GraphFileLayoutMetrics.ResolveRadialOrbits(nSat, innerW, innerH, satR);
58
59 for (var i = 0; i < nSat; i++)
60 {
61 var sat = satellites[i];
62 double orbit;
63 double angle;
64 if (!useTwoRings)
65 {
66 orbit = orbitInner;
67 angle = nSat == 0 ? 0 : (2 * Math.PI * i / nSat) - Math.PI / 2;
68 }
69 else if (i < innerCount)
70 {
71 orbit = orbitInner;
72 angle = innerCount == 0 ? 0 : (2 * Math.PI * i / innerCount) - Math.PI / 2;
73 }
74 else
75 {
76 var outerN = nSat - innerCount;
77 var j = i - innerCount;
78 orbit = orbitOuter;
79 var stagger = innerCount > 0 ? Math.PI / innerCount : 0;
80 angle = outerN == 0 ? 0 : (2 * Math.PI * j / outerN) - Math.PI / 2 + stagger;
81 }
82
83 var px = cx + orbit * Math.Cos(angle);
84 var py = cy + orbit * Math.Sin(angle);
85 var p = new Point(px, py);
86 idToCenter[sat.Id] = p;
87 idToRadius[sat.Id] = satR;
88 layouts.Add(MakeNode(sat, p, satR, isAnchor: false));
89 }
90
91 var edgeLayouts = new List<GraphLayoutEdge>();
92 foreach (var e in doc.Edges)
93 {
94 if (!idToCenter.TryGetValue(e.FromId, out var a))
95 continue;
96 if (!idToCenter.TryGetValue(e.ToId, out var b))
97 continue;
98 edgeLayouts.Add(new GraphLayoutEdge
99 {
100 FromNodeId = e.FromId,
101 ToNodeId = e.ToId,
102 From = a,
103 To = b,
104 ToRadius = idToRadius.TryGetValue(e.ToId, out var toR) ? toR : satR,
105 Kind = e.Kind,
106 RelationKind = e.RelationKind
107 });
108 }
109
110 if (edgeLayouts.Count == 0 && anchorCenter is { } ac0)
111 {
112 foreach (var s in layouts.Where(x => !x.IsAnchor))
113 edgeLayouts.Add(new GraphLayoutEdge
114 {
115 FromNodeId = anchor?.Id ?? "n0",
116 ToNodeId = s.Id,
117 From = ac0,
118 To = s.Center,
119 ToRadius = s.Radius,
120 Kind = null,
121 RelationKind = null
122 });
123 }
124
125 return new GraphLayoutScene
126 {
127 Nodes = layouts,
128 Edges = edgeLayouts,
129 Legend = [],
130 UseLegendColumn = false,
131 LegendColumnLeft = width,
132 RelatedFilesLayout = CodeNavigationMapRelatedGraphLayoutKind.Radial
133 };
134 }
135
136 private static GraphLayoutNode MakeNode(GraphNode n, Point center, double radius, bool isAnchor) =>
137 new()
138 {
139 Id = n.Id,
140 Kind = n.Kind,
141 FullPath = n.Path,
142 Label = TruncateLabel(n.Label),
143 Center = center,
144 Radius = radius,
145 IsAnchor = isAnchor,
146 Shape = GraphNodeShape.Rectangle,
147 LineStart = n.LineStart,
148 LineEnd = n.LineEnd
149 };
150
151 private static GraphLayoutScene EmptyScene(double width) =>
152 new()
153 {
154 Nodes = [],
155 Edges = [],
156 Legend = [],
157 LegendColumnLeft = width
158 };
159
160 private static string TruncateLabel(string label)
161 {
162 // For related-files we want the renderer to handle wrapping inside cards.
163 // Control-flow uses strict budgets; related-files doesn't.
164 var s = label?.Trim() ?? "";
165 if (s.Length <= 80)
166 return s;
167 return s[..77] + "…";
168 }
169
170 private static double ResolveRelatedAutoScaleCeiling(double innerW, double innerH, int satelliteCount)
171 {
172 // For related-files, bigger cards are more readable when there's a lot of empty space.
173 // Keep a conservative ceiling for dense graphs to avoid overlap.
174 var minDim = Math.Min(innerW, innerH);
175 if (satelliteCount <= 4 && minDim >= 260)
176 return 1.75;
177 if (satelliteCount <= 8 && minDim >= 240)
178 return 1.55;
179 if (satelliteCount <= 12 && minDim >= 220)
180 return 1.40;
181 return 1.22;
182 }
183}
184
View only · write via MCP/CIDE