| 1 | using Avalonia; |
| 2 | using Avalonia.Media; |
| 3 | using CascadeIDE.Cockpit.Graph.Layout; |
| 4 | |
| 5 | namespace CascadeIDE.Views.SkiaKit.Graph; |
| 6 | |
| 7 | /// <summary>Render graph-backed surface: edges → nodes → legend (ADR 0055, SkiaKit 0117).</summary> |
| 8 | public 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 | |