| Name | Message | Date |
|---|---|---|
| 📄 EdgeAvoidance.cs | 5 days ago | |
| 📄 GraphLayout.cs | 5 days ago | |
| 📄 GraphScene.cs | 5 days ago | |
| 📄 PhysicsLayout.cs | 5 days ago |
📄
ui/src/Graph/GraphScene.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
using System; using System.Collections.Generic; using Avalonia; using Avalonia.Media; using Microsoft.CodeAnalysis; namespace Cfa.Ui.Graph; public sealed record GraphScene(IReadOnlyList<GraphNode> Nodes, IReadOnlyList<GraphEdge> Edges) { /// <summary>Conservative viewport bounds including connection curves, strokes, and arrowheads.</summary> public Rect Bounds { get; } = CalculateBounds(Nodes, Edges); private static Rect CalculateBounds(IReadOnlyList<GraphNode> nodes, IReadOnlyList<GraphEdge> edges) { Rect? bounds = null; foreach (var node in nodes) bounds = bounds?.Union(node.Bounds) ?? node.Bounds; foreach (var edge in edges) { // A cubic curve lies within its control-point hull; 15 also covers the 13-by-6 arrowhead. var curve = Envelope(edge.Start, edge.End) .Union(Envelope(edge.Control1, edge.Control2)) .Inflate(15 + edge.Thickness / 2); bounds = bounds?.Union(curve) ?? curve; } return bounds ?? default; static Rect Envelope(Point first, Point second) => new( Math.Min(first.X, second.X), Math.Min(first.Y, second.Y), Math.Abs(first.X - second.X), Math.Abs(first.Y - second.Y) ); } } public sealed class GraphNode { internal GraphNode( int id, ISymbol symbol, string label, string? sourceLink, Size size, bool isOrigin, bool isCycleReference ) { Id = id; Symbol = symbol; Label = label; SourceLink = sourceLink; Width = size.Width; Height = size.Height; IsOrigin = isOrigin; IsCycleReference = isCycleReference; } public int Id { get; } public ISymbol Symbol { get; } public string Label { get; } public string? SourceLink { get; } public string ToolTip => Label + Environment.NewLine + SourceLink; public Point Location { get; internal set; } public double Width { get; } public double Height { get; } public double FontSize => 13; public bool IsOrigin { get; } public bool IsCycleReference { get; } public Rect Bounds => new(Location, new Size(Width, Height)); } public sealed record GraphEdge( GraphNode Source, GraphNode Target, Point Start, Point End, double Thickness, bool IsLoopback ) { /// <summary>Cubic control points shared by connection rendering and viewport fitting.</summary> public Point Control1 => IsLoopback ? new Point(Start.X + 50, Start.Y) : Start + (End - Start) / 3; public Point Control2 => IsLoopback ? new Point(End.X + 50, End.Y) : End - (End - Start) / 3; public StreamGeometry CreateCurveGeometry() { var geometry = new StreamGeometry(); using var drawing = geometry.Open(); drawing.BeginFigure(Start, false); drawing.CubicBezierTo(Control1, Control2, End); drawing.EndFigure(false); return geometry; } public StreamGeometry CreateArrowGeometry() { var geometry = new StreamGeometry(); var direction = new Vector(End.X - Control2.X, End.Y - Control2.Y); if (direction.Length == 0) return geometry; direction /= direction.Length; var normal = new Vector(-direction.Y, direction.X); using var drawing = geometry.Open(); drawing.BeginFigure(End, true); drawing.LineTo(End - direction * 13 + normal * 6); drawing.LineTo(End - direction * 13 - normal * 6); drawing.EndFigure(true); return geometry; } }