Forge
csharpdeeb25a2
1using Avalonia;
2using Avalonia.Media;
3using CascadeIDE.Cockpit.Graph.Layout;
4
5namespace CascadeIDE.Views.SkiaKit.Graph;
6
7/// <summary>Render graph-backed surface: edges → nodes → legend (ADR 0055, SkiaKit 0117).</summary>
8public static partial class SkiaGraphSceneDrawing
9{
10 private const string ConditionStepKind = "condition_step";
11 private const string ExitStepKind = "exit_step";
12
13 /// <summary>Стрелка «выход из зоны» (NE): не зависит от глифа ↗ в шрифте.</summary>
14 internal static void DrawNorthEastExitArrow(DrawingContext context, IBrush stroke, Point tip, double size, double thickness = 1.35)
15 {
16 if (size < 2)
17 return;
18
19 var dirX = 0.70710678118654757;
20 var dirY = -0.70710678118654757;
21 var basePt = new Point(tip.X - dirX * size, tip.Y - dirY * size);
22 var pen = new Pen(stroke, thickness) { LineCap = PenLineCap.Round };
23 context.DrawLine(pen, basePt, tip);
24 var wing = size * 0.38;
25 var px = -dirY;
26 var py = dirX;
27 var w1 = new Point(
28 tip.X - dirX * wing + px * wing * 0.35,
29 tip.Y - dirY * wing + py * wing * 0.35);
30 context.DrawLine(pen, w1, tip);
31 var w2 = new Point(
32 tip.X - dirX * wing - px * wing * 0.35,
33 tip.Y - dirY * wing - py * wing * 0.35);
34 context.DrawLine(pen, w2, tip);
35 }
36
37 /// <summary>
38 /// Та же NE-стрелка, но середина ствола (от <see cref="DrawNorthEastExitArrow"/>) совпадает с
39 /// <paramref name="shaftCenter"/> — визуально «по центру кружка», без смещения 0.12·R.
40 /// </summary>
41 internal static void DrawNorthEastExitArrowShaftCentered(
42 DrawingContext context, IBrush stroke, Point shaftCenter, double size, double thickness = 1.35)
43 {
44 if (size < 2)
45 return;
46 const double h = 0.70710678118654757;
47 var tip = new Point(shaftCenter.X + h * size * 0.5, shaftCenter.Y - h * size * 0.5);
48 DrawNorthEastExitArrow(context, stroke, tip, size, thickness);
49 }
50
51 public static void DrawScene(
52 DrawingContext context,
53 GraphLayoutScene scene,
54 SkiaGraphVisualTheme theme,
55 double width,
56 double height)
57 {
58 if (scene.IsEmpty || width <= 0 || height <= 0)
59 return;
60
61 DrawLoopGroupOutlines(context, scene, theme);
62 DrawEdges(context, scene, theme);
63 DrawNodes(context, scene, theme);
64 DrawLegend(context, scene, theme, width, height);
65 }
66
67 /// <summary>Hit-test узла для pointer routing (узел условия — ромб, манхэттенское расстояние до ромба).</summary>
68 public static bool HitTestNode(GraphLayoutNode n, Point p, double tolerance = 6)
69 {
70 if (n.Shape == GraphNodeShape.Condition)
71 return HitConditionBranchOutline(n.Center, n.Radius, p, tolerance);
72 if (n.Shape == GraphNodeShape.Rectangle)
73 return HitRectangleOutline(n.Center, n.Radius, p, tolerance);
74 var dx = p.X - n.Center.X;
75 var dy = p.Y - n.Center.Y;
76 return Math.Sqrt(dx * dx + dy * dy) <= n.Radius + tolerance;
77 }
78
79 private static bool HitConditionBranchOutline(Point center, double r, Point p, double tolerance)
80 {
81 var dx = Math.Abs(p.X - center.X);
82 var dy = Math.Abs(p.Y - center.Y);
83 return dx + dy <= r + tolerance;
84 }
85
86 private static bool HitRectangleOutline(Point center, double r, Point p, double tolerance)
87 {
88 // Keep constants in sync with rectangle sizing in SkiaGraphSceneDrawing.Nodes.
89 // Hit-test should be generous enough even when cards grow to fit text.
90 var w = Math.Clamp(r * 4.6, 56, 560) + tolerance * 2;
91 var h = Math.Clamp(r * 2.6, 24, 150) + tolerance * 2;
92 return Math.Abs(p.X - center.X) <= w / 2 && Math.Abs(p.Y - center.Y) <= h / 2;
93 }
94}
95
View only · write via MCP/CIDE