using Avalonia; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Skia; using Avalonia.Styling; using Serein.Library.Utils; using Serein.Workbench.Avalonia.Extension; using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; namespace Serein.Workbench.Avalonia.Custom.Views { public class ConnectionLineShape : Control { private readonly double strokeThickness; /// /// 确定起始坐标和目标坐标、外观样式的曲线 /// /// 起始坐标 /// 结束坐标 /// 颜色 /// 是否为虚线 public ConnectionLineShape(Point left, Point right, Brush brush, bool isDotted = false, bool isTop = false) { this.brush = brush; this.leftPoint = left; this.rightPoint = right; this.strokeThickness = 4; InitElementPoint(isDotted, isTop); InvalidateVisual(); // 触发重绘 } public void InitElementPoint(bool isDotted, bool isTop = false) { //hitVisiblePen = new Pen(Brushes.Transparent, 1.0); // 初始化碰撞检测线 //hitVisiblePen.Freeze(); // Freeze以提高性能 visualPen = new Pen(brush, 3.0); // 默认可视化Pen opacity = 1.0d; //var dashStyle = new DashStyle(); if (isDotted) { opacity = 0.42d; visualPen.DashStyle = new DashStyle(); // DashStyles.Dash; // 选择虚线样式 } //visualPen.Freeze(); // Freeze以提高性能 linkSize = 4; // 整线条粗细 int zIndex = -999999; this.ZIndex = zIndex; //Panel.SetZIndex(this, zIndex); // 置底 } /// /// 更新线条落点位置 /// /// /// public void UpdatePoint(Point left, Point right, Brush? brush = null) { if(brush is not null) { visualPen = new Pen(brush, 3.0); // 默认可视化Pen } this.leftPoint = left; this.rightPoint = right; InvalidateVisual(); // 触发重绘 } /// /// 更新线条落点位置 /// /// public void UpdateRightPoint(Point right, Brush? brush = null) { if (brush is not null) { visualPen = new Pen(brush, 3.0); // 默认可视化Pen } this.rightPoint = right; InvalidateVisual(); // 触发重绘 } /// /// 更新线条起点位置 /// /// public void UpdateLeftPoints(Point left, Brush? brush = null) { if (brush is not null) { visualPen = new Pen(brush, 3.0); // 默认可视化Pen } this.leftPoint = left; InvalidateVisual(); // 触发重绘 } /// /// 刷新颜色 /// /// public void UpdateColor(Brush brush ) { visualPen = new Pen(brush, 3.0); // 默认可视化Pen InvalidateVisual(); // 触发重绘 } /// /// 控件重绘事件 /// /// public override void Render(DrawingContext drawingContext) { // 刷新线条显示位置 DrawBezierCurve(drawingContext, leftPoint, rightPoint); } #region 重绘 private StreamGeometry streamGeometry = new StreamGeometry(); private Point rightCenterOfStartLocation; // 目标节点选择左侧边缘中心 private Point leftCenterOfEndLocation; // 起始节点选择右侧边缘中心 //private Pen hitVisiblePen; // 初始化碰撞检测线 private Pen visualPen; // 默认可视化Pen private Point leftPoint; // 连接线的起始节点 private Point rightPoint; // 连接线的终点 private Brush brush; // 线条颜色 private double opacity; // 透明度 double linkSize; // 根据缩放比例调整线条粗细 //public void UpdateLineColor() //{ // visualPen = new Pen(brush, 3.0); // 默认可视化Pen // InvalidateVisual(); // 触发重绘 //} private Point c0, c1; // 用于计算贝塞尔曲线控制点逻辑 private Vector axis = new Vector(1, 0); private Vector startToEnd; private int i = 0; private void DrawBezierCurve(DrawingContext drawingContext, Point left, Point right) { // 控制点的计算逻辑 double power = 140; // 控制贝塞尔曲线的“拉伸”强度 drawingContext.PushOpacity(opacity); // 计算轴向向量与起点到终点的向量 //var axis = new Vector(1, 0); startToEnd = (right.ToVector() - left.ToVector()).NormalizeTo(); //var dp = axis.DotProduct(startToEnd); //dp = dp < 50 ? 50 : dp; //var pow = Math.Max(0, dp) ; //var k = 1 - Math.Pow(pow, 10.0); // //Debug.WriteLine("pow : " + pow); //Debug.WriteLine("k : " + k); //Debug.WriteLine(""); // 计算拉伸程度k,拉伸与水平夹角正相关 var dp = axis.DotProduct(startToEnd) ; var pow = Math.Pow(dp, 10.0); pow = pow > 0 ? 0 : pow; var k = 1 - pow; // 如果起点x大于终点x,增加额外的偏移量,避免重叠 var bias = left.X > right.X ? Math.Abs(left.X - right.X) * 0.25 : 0; // 控制点的实际计算 c0 = new Point(+(power + bias) * k + left.X, left.Y); c1 = new Point(-(power + bias) * k + right.X, right.Y); // 准备StreamGeometry以用于绘制曲线 // why can't clearValue()? //streamGeometry.ClearValue(ThemeProperty); //var streamGeometry = new StreamGeometry(); //if( i++ > 100 && streamGeometry is AvaloniaObject avaloniaObject) //{ // var platformImplInfo = streamGeometry.GetType().GetProperty("PlatformImpl", BindingFlags.NonPublic | BindingFlags.Instance); // var platformImpl = platformImplInfo?.GetValue(streamGeometry); // var pathCache = platformImpl?.GetType().GetField("_bounds", BindingFlags.NonPublic | BindingFlags.Instance); // if(pathCache is IDisposable disposable) // { // disposable.Dispose(); // } // //pathCache?.Invoke(platformImpl, []); // Debug.WriteLine("invoke => InvalidateCaches"); // i = 0; // //public class AvaloniaObject : IAvaloniaObjectDebug, INotifyPropertyChanged // //private readonly ValueStore _values; //} // this is override "Render()" method // display a bezier-line on canvas and follow the mouse in real time // I don't want to re-instantiate StreamGeometry() // because I want to reduce the number of GC // but , it seems impossible to do so in avalonia streamGeometry = new StreamGeometry(); // in wpf , this method allows display content to be cleared // streamGeometry.Clear(); // this is wpf method // in avalonia, why does this method need value of "AvaloniaProperty" data type ? // but I don't know use what "AvaloniaProperty" to clear the displayed content // if I don't clear the cache or re-instantiate it // the canvas will display repeated lines , because exits cache inside streamGeometry // streamGeometry.ClearValue("AvaloniaProperty"); using (var context = streamGeometry.Open()) { context.BeginFigure(left, true); // start point of the bezier-line context.CubicBezierTo(c0, c1, right, true); // drawing bezier-line } drawingContext.DrawGeometry(null, visualPen, streamGeometry); } #endregion } }