FlowCanvas.cs 80 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Collections.Specialized;
  4. using System.Linq;
  5. using System.Windows;
  6. using System.Windows.Controls;
  7. using System.Windows.Input;
  8. using System.Windows.Media;
  9. using System.Windows.Shapes;
  10. using System.Xml.Linq;
  11. using TeamAAS.Dialogs;
  12. using TeamAAS.FlowEditor.Models;
  13. using TeamAAS.FlowEditor.Plugins;
  14. using TeamAAS.FlowEngine;
  15. namespace TeamAAS.FlowEditor.Controls
  16. {
  17. /// <summary>
  18. /// 流程编辑器画布 - 支持缩放、平移、拖拽节点、绘制连线、框选
  19. /// </summary>
  20. public class FlowCanvas : Canvas
  21. {
  22. #region 拖放坐标(高DPI安全)
  23. [System.Runtime.InteropServices.DllImport("user32.dll")]
  24. private static extern bool GetCursorPos(out POINT lpPoint);
  25. [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
  26. private struct POINT { public int X; public int Y; }
  27. /// <summary>
  28. /// 用系统光标位置反推画布本地坐标。
  29. /// 不能用拖放事件的 e.GetPosition:PerMonitorV2 + 高 DPI 下 WPF 的拖放事件坐标
  30. /// 换算因子错误(已知问题),节点会落在光标旁边。
  31. /// 注意:PointFromScreen 的结果就是正确的 WPF 坐标,不要再乘 TransformToDevice——
  32. /// 进程按系统 DPI 虚拟化运行时,乘上去会造成预览超速偏移。
  33. /// </summary>
  34. public Point GetCursorCanvasPosition()
  35. {
  36. GetCursorPos(out POINT pt);
  37. var parent = (Visual)VisualTreeHelper.GetParent(this);
  38. var parentPt = parent.PointFromScreen(new Point(pt.X, pt.Y));
  39. // 逆画布 RenderTransform(缩放 + 平移)→ 画布本地坐标
  40. var t = RenderTransform;
  41. return t != null ? t.Inverse.Transform(parentPt) : parentPt;
  42. }
  43. #endregion
  44. #region 常量
  45. private const double MinZoom = 0.2;
  46. private const double MaxZoom = 3.0;
  47. private const double ZoomFactor = 1.15;
  48. #endregion
  49. #region 字段
  50. private readonly ScaleTransform _scale;
  51. private readonly TranslateTransform _translate;
  52. private FlowGraph _graph;
  53. private readonly Dictionary<string, NodeControl> _nodeControls = new Dictionary<string, NodeControl>();
  54. private readonly Dictionary<string, Path> _connectionPaths = new Dictionary<string, Path>();
  55. // 自动拉线预览(虚线)
  56. private Path _previewLine;
  57. // 平移状态
  58. private bool _isPanning;
  59. private Point _panStart;
  60. private Point _panOrigin;
  61. // 连线绘制状态
  62. private bool _isConnecting;
  63. private FlowNode _connectSourceNode;
  64. private PortSide _connectSourceSide;
  65. private Path _tempPath;
  66. // 框选状态
  67. private bool _isBoxSelecting;
  68. private Point _boxSelectStart;
  69. private Rectangle _selectionBox;
  70. // 选中项
  71. private FlowNode _selectedNode;
  72. private FlowConnection _selectedConnection;
  73. private readonly List<FlowNode> _selectedNodes = new List<FlowNode>();
  74. private const string ClipboardPrefix = "TeamAASFlow:";
  75. #endregion
  76. #region 事件
  77. public event Action<FlowNode> NodeDoubleClicked;
  78. public event Action<FlowNode> NodeSelected;
  79. public event Action<FlowConnection> ConnectionSelected;
  80. public event Action SelectionCleared;
  81. public event Action<double, double, NodePluginInfo> NodeDropRequested;
  82. public event Action<double> ZoomChanged;
  83. public event Action<FlowNode> NodeRunRequested;
  84. public event Action<FlowNode> NodeRunFromHereRequested;
  85. public event Action<FlowNode> NodePropertiesRequested;
  86. #endregion
  87. #region 属性
  88. public double Zoom => _scale.ScaleX;
  89. public double TranslateX => _translate.X;
  90. public double TranslateY => _translate.Y;
  91. public void SetTranslateX(double x) { _translate.X = x; }
  92. public void SetTranslateY(double y) { _translate.Y = y; }
  93. public Size ViewportSize => GetViewportSize();
  94. public FlowGraph Graph
  95. {
  96. get => _graph;
  97. set
  98. {
  99. if (_graph != null) UnsubscribeGraph();
  100. ClearCanvas();
  101. _graph = value;
  102. if (_graph != null) SubscribeGraph();
  103. }
  104. }
  105. /// <summary>流程运行中:锁定节点移动和双击编辑</summary>
  106. public static readonly DependencyProperty IsRunningProperty =
  107. DependencyProperty.Register(nameof(IsRunning), typeof(bool), typeof(FlowCanvas), new PropertyMetadata(false));
  108. public bool IsRunning
  109. {
  110. get => (bool)GetValue(IsRunningProperty);
  111. set => SetValue(IsRunningProperty, value);
  112. }
  113. /// <summary>只读模式(运行产品监控中):允许选中节点查看结果,禁止一切修改操作</summary>
  114. public static readonly DependencyProperty IsReadOnlyProperty =
  115. DependencyProperty.Register(nameof(IsReadOnly), typeof(bool), typeof(FlowCanvas), new PropertyMetadata(false));
  116. public bool IsReadOnly
  117. {
  118. get => (bool)GetValue(IsReadOnlyProperty);
  119. set => SetValue(IsReadOnlyProperty, value);
  120. }
  121. #endregion
  122. #region 构造函数
  123. public FlowCanvas()
  124. {
  125. _scale = new ScaleTransform(1, 1);
  126. _translate = new TranslateTransform(0, 0);
  127. var group = new TransformGroup();
  128. group.Children.Add(_scale);
  129. group.Children.Add(_translate);
  130. RenderTransform = group;
  131. ClipToBounds = true;
  132. Focusable = true;
  133. Background = new SolidColorBrush(Colors.White);
  134. AllowDrop = true;
  135. // 初始画布尺寸
  136. Width = 5000;
  137. Height = 3500;
  138. // 初始偏移,显示画布的一部分
  139. _translate.X = 0;
  140. _translate.Y = 0;
  141. }
  142. #endregion
  143. #region Graph 订阅
  144. private void SubscribeGraph()
  145. {
  146. _graph.Nodes.CollectionChanged += OnNodesChanged;
  147. _graph.Connections.CollectionChanged += OnConnectionsChanged;
  148. foreach (var node in _graph.Nodes)
  149. AddNodeVisual(node);
  150. foreach (var conn in _graph.Connections)
  151. AddConnectionVisual(conn);
  152. }
  153. private void UnsubscribeGraph()
  154. {
  155. _graph.Nodes.CollectionChanged -= OnNodesChanged;
  156. _graph.Connections.CollectionChanged -= OnConnectionsChanged;
  157. }
  158. private void ClearCanvas()
  159. {
  160. foreach (var node in _nodeControls.Values)
  161. {
  162. if (node.DataContext is FlowNode fn)
  163. fn.PropertyChanged -= OnNodePropertyChanged;
  164. }
  165. _nodeControls.Clear();
  166. _connectionPaths.Clear();
  167. Children.Clear();
  168. }
  169. private void OnNodesChanged(object sender, NotifyCollectionChangedEventArgs e)
  170. {
  171. // Clear()/整体替换会触发 Reset(NewItems/OldItems 均为 null),
  172. // 不处理会导致导入流程后旧节点可视化残留成“影子”。
  173. if (e.Action == NotifyCollectionChangedAction.Reset)
  174. {
  175. RebuildVisuals();
  176. return;
  177. }
  178. if (e.NewItems != null)
  179. foreach (FlowNode node in e.NewItems)
  180. AddNodeVisual(node);
  181. if (e.OldItems != null)
  182. foreach (FlowNode node in e.OldItems)
  183. RemoveNodeVisual(node);
  184. }
  185. private void OnConnectionsChanged(object sender, NotifyCollectionChangedEventArgs e)
  186. {
  187. if (e.Action == NotifyCollectionChangedAction.Reset)
  188. {
  189. RebuildVisuals();
  190. return;
  191. }
  192. if (e.NewItems != null)
  193. foreach (FlowConnection conn in e.NewItems)
  194. AddConnectionVisual(conn);
  195. if (e.OldItems != null)
  196. foreach (FlowConnection conn in e.OldItems)
  197. RemoveConnectionVisual(conn);
  198. }
  199. /// <summary>
  200. /// 集合被整体 Clear(Reset)时重建全部节点/连线可视化,避免旧图形残留(如导入流程后)。
  201. /// </summary>
  202. private void RebuildVisuals()
  203. {
  204. foreach (var kv in _nodeControls.ToList())
  205. {
  206. if (kv.Value.DataContext is FlowNode fn)
  207. fn.PropertyChanged -= OnNodePropertyChanged;
  208. Children.Remove(kv.Value);
  209. }
  210. _nodeControls.Clear();
  211. foreach (var kv in _connectionPaths.ToList())
  212. Children.Remove(kv.Value);
  213. _connectionPaths.Clear();
  214. if (_graph == null) return;
  215. foreach (var node in _graph.Nodes)
  216. AddNodeVisual(node);
  217. foreach (var conn in _graph.Connections)
  218. AddConnectionVisual(conn);
  219. }
  220. #endregion
  221. #region 节点可视化
  222. private void AddNodeVisual(FlowNode node)
  223. {
  224. var ctrl = new NodeControl { DataContext = node };
  225. SetLeft(ctrl, node.X);
  226. SetTop(ctrl, node.Y);
  227. Children.Add(ctrl);
  228. _nodeControls[node.NodeId] = ctrl;
  229. // 节点渲染后更新实际尺寸,用于端口位置和碰撞检测
  230. ctrl.SizeChanged += (s, e) =>
  231. {
  232. node.NodeWidth = ctrl.ActualWidth;
  233. node.NodeHeight = ctrl.ActualHeight;
  234. UpdateConnectionsForNode(node.NodeId);
  235. UpdateCanvasSize();
  236. };
  237. node.PropertyChanged += OnNodePropertyChanged;
  238. }
  239. private void RemoveNodeVisual(FlowNode node)
  240. {
  241. if (_nodeControls.TryGetValue(node.NodeId, out var ctrl))
  242. {
  243. Children.Remove(ctrl);
  244. _nodeControls.Remove(node.NodeId);
  245. node.PropertyChanged -= OnNodePropertyChanged;
  246. }
  247. // 移除相关连线可视化
  248. var toRemove = _connectionPaths
  249. .Where(kvp => kvp.Value.Tag is FlowConnection fc &&
  250. (fc.SourceNodeId == node.NodeId || fc.TargetNodeId == node.NodeId))
  251. .ToList();
  252. foreach (var kvp in toRemove)
  253. {
  254. Children.Remove(kvp.Value);
  255. _connectionPaths.Remove(kvp.Key);
  256. }
  257. if (_selectedNode == node)
  258. _selectedNode = null;
  259. _selectedNodes.Remove(node);
  260. }
  261. private void OnNodePropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
  262. {
  263. var node = (FlowNode)sender;
  264. if (e.PropertyName == nameof(FlowNode.X) || e.PropertyName == nameof(FlowNode.Y))
  265. {
  266. if (_nodeControls.TryGetValue(node.NodeId, out var ctrl))
  267. {
  268. SetLeft(ctrl, node.X);
  269. SetTop(ctrl, node.Y);
  270. }
  271. UpdateConnectionsForNode(node.NodeId);
  272. UpdateCanvasSize();
  273. }
  274. else if (e.PropertyName == nameof(FlowNode.IsEnabled))
  275. {
  276. if (_nodeControls.TryGetValue(node.NodeId, out var ctrl))
  277. ctrl.UpdateDisabledVisual();
  278. }
  279. else if (e.PropertyName == nameof(FlowNode.Status))
  280. {
  281. node.NotifyStatusChanged();
  282. }
  283. }
  284. #endregion
  285. #region 连线可视化
  286. private void AddConnectionVisual(FlowConnection conn)
  287. {
  288. var path = new Path
  289. {
  290. Stroke = new SolidColorBrush(Color.FromRgb(232, 145, 73)),
  291. StrokeThickness = 2,
  292. Fill = new SolidColorBrush(Color.FromRgb(232, 145, 73)),
  293. Tag = conn,
  294. Cursor = Cursors.Hand
  295. };
  296. UpdateConnectionPath(path, conn);
  297. Children.Insert(0, path);
  298. _connectionPaths[conn.ConnectionId] = path;
  299. path.MouseLeftButtonDown += (s, e) =>
  300. {
  301. SelectConnection(conn);
  302. e.Handled = true;
  303. };
  304. }
  305. private void RemoveConnectionVisual(FlowConnection conn)
  306. {
  307. if (_connectionPaths.TryGetValue(conn.ConnectionId, out var path))
  308. {
  309. Children.Remove(path);
  310. _connectionPaths.Remove(conn.ConnectionId);
  311. }
  312. if (_selectedConnection == conn)
  313. _selectedConnection = null;
  314. }
  315. private void UpdateConnectionPath(Path path, FlowConnection conn)
  316. {
  317. var sourceNode = _graph?.GetNode(conn.SourceNodeId);
  318. var targetNode = _graph?.GetNode(conn.TargetNodeId);
  319. if (sourceNode == null || targetNode == null) return;
  320. Point start = GetPortPosition(sourceNode, conn.SourceSide);
  321. Point end = GetPortPosition(targetNode, conn.TargetSide);
  322. path.Data = CreateConnectionGeometry(start, end, conn.SourceSide, conn.TargetSide);
  323. if (conn.IsSelected)
  324. {
  325. path.Stroke = Brushes.White;
  326. path.Fill = Brushes.White;
  327. path.StrokeThickness = 3;
  328. }
  329. else
  330. {
  331. // 连接到异常分支节点的线用红色,其余橙色
  332. bool isException = targetNode.Category == NodeCategory.ExceptionBranch;
  333. var color = isException
  334. ? Color.FromRgb(0xB7, 0x1C, 0x1C)
  335. : Color.FromRgb(232, 145, 73);
  336. path.Stroke = new SolidColorBrush(color);
  337. path.Fill = new SolidColorBrush(color);
  338. path.StrokeThickness = 2;
  339. }
  340. }
  341. private void UpdateConnectionsForNode(string nodeId)
  342. {
  343. foreach (var kvp in _connectionPaths)
  344. {
  345. if (kvp.Value.Tag is FlowConnection conn)
  346. {
  347. if (conn.SourceNodeId == nodeId || conn.TargetNodeId == nodeId)
  348. UpdateConnectionPath(kvp.Value, conn);
  349. }
  350. }
  351. }
  352. #endregion
  353. #region 端口位置 & 连线几何
  354. public static Point GetPortPosition(FlowNode node, PortDirection direction, int portIndex)
  355. {
  356. double w = node.NodeWidth;
  357. double h = node.NodeHeight;
  358. if (direction == PortDirection.Input)
  359. {
  360. // portIndex 0 = Left, 1 = Top
  361. if (portIndex == 0) return new Point(node.X, node.Y + h / 2);
  362. return new Point(node.X + w / 2, node.Y);
  363. }
  364. else // Output
  365. {
  366. // portIndex 0 = Right, 1 = Bottom
  367. if (portIndex == 0) return new Point(node.X + w, node.Y + h / 2);
  368. return new Point(node.X + w / 2, node.Y + h);
  369. }
  370. }
  371. /// <summary>
  372. /// 根据边获取端口位置(通用,不区分输入输出)
  373. /// </summary>
  374. public static Point GetPortPosition(FlowNode node, PortSide side)
  375. {
  376. double w = node.NodeWidth;
  377. double h = node.NodeHeight;
  378. switch (side)
  379. {
  380. case PortSide.Left: return new Point(node.X, node.Y + h / 2);
  381. case PortSide.Top: return new Point(node.X + w / 2, node.Y);
  382. case PortSide.Right: return new Point(node.X + w, node.Y + h / 2);
  383. case PortSide.Bottom: return new Point(node.X + w / 2, node.Y + h);
  384. default: return new Point(node.X, node.Y + h / 2);
  385. }
  386. }
  387. /// <summary>
  388. /// 根据端口方向和索引推断所在边
  389. /// </summary>
  390. public static PortSide GetPortSide(PortDirection direction, int portIndex)
  391. {
  392. if (direction == PortDirection.Input)
  393. return portIndex == 0 ? PortSide.Left : PortSide.Top;
  394. return portIndex == 0 ? PortSide.Right : PortSide.Bottom;
  395. }
  396. public static Geometry CreateConnectionGeometry(Point start, Point end, PortSide startSide = PortSide.Right, PortSide endSide = PortSide.Left)
  397. {
  398. // 偏移起止点,留出间距让箭头可见
  399. Point adjStart = OffsetPoint(start, startSide, 8);
  400. Point adjEnd = OffsetPoint(end, endSide, 12);
  401. var geo = new StreamGeometry();
  402. using (var ctx = geo.Open())
  403. {
  404. // 控制点方向(端口朝外方向)
  405. Vector startDir = GetSideDirection(startSide);
  406. Vector endDir = GetSideDirection(endSide);
  407. // 控制点偏移量:基于距离动态调整,保证曲度自然
  408. double dist = (adjEnd - adjStart).Length;
  409. double offset = Math.Max(50, dist * 0.4);
  410. Point cp1 = adjStart + startDir * offset;
  411. Point cp2 = adjEnd + endDir * offset;
  412. // 绘制平滑贝塞尔曲线
  413. ctx.BeginFigure(adjStart, false, false);
  414. ctx.BezierTo(cp1, cp2, adjEnd, true, false);
  415. // 箭头方向 = 贝塞尔终点切线 (end - cp2)
  416. double angle = Math.Atan2(adjEnd.Y - cp2.Y, adjEnd.X - cp2.X);
  417. double arrowSize = 7;
  418. var p1 = new Point(
  419. adjEnd.X - arrowSize * Math.Cos(angle - Math.PI / 6),
  420. adjEnd.Y - arrowSize * Math.Sin(angle - Math.PI / 6));
  421. var p2 = new Point(
  422. adjEnd.X - arrowSize * Math.Cos(angle + Math.PI / 6),
  423. adjEnd.Y - arrowSize * Math.Sin(angle + Math.PI / 6));
  424. ctx.BeginFigure(adjEnd, true, true);
  425. ctx.LineTo(p1, true, true);
  426. ctx.LineTo(p2, true, true);
  427. }
  428. geo.Freeze();
  429. return geo;
  430. }
  431. /// <summary>
  432. /// 端口朝外方向单位向量
  433. /// </summary>
  434. private static Vector GetSideDirection(PortSide side)
  435. {
  436. switch (side)
  437. {
  438. case PortSide.Left: return new Vector(-1, 0);
  439. case PortSide.Right: return new Vector(1, 0);
  440. case PortSide.Top: return new Vector(0, -1);
  441. case PortSide.Bottom: return new Vector(0, 1);
  442. default: return new Vector(1, 0);
  443. }
  444. }
  445. /// <summary>
  446. /// 根据鼠标位置推断目标端口边
  447. /// </summary>
  448. private static PortSide DetermineEndSide(Point start, Point end, PortSide startSide)
  449. {
  450. double dx = end.X - start.X;
  451. double dy = end.Y - start.Y;
  452. switch (startSide)
  453. {
  454. case PortSide.Right:
  455. return dx >= 0 ? PortSide.Left : PortSide.Right;
  456. case PortSide.Bottom:
  457. return dy >= 0 ? PortSide.Top : PortSide.Bottom;
  458. case PortSide.Left:
  459. return dx <= 0 ? PortSide.Right : PortSide.Left;
  460. case PortSide.Top:
  461. return dy <= 0 ? PortSide.Bottom : PortSide.Top;
  462. default:
  463. return PortSide.Left;
  464. }
  465. }
  466. /// <summary>
  467. /// 沿端口方向偏移点,留出间距
  468. /// </summary>
  469. private static Point OffsetPoint(Point p, PortSide side, double offset)
  470. {
  471. switch (side)
  472. {
  473. case PortSide.Left: return new Point(p.X - offset, p.Y);
  474. case PortSide.Right: return new Point(p.X + offset, p.Y);
  475. case PortSide.Top: return new Point(p.X, p.Y - offset);
  476. case PortSide.Bottom: return new Point(p.X, p.Y + offset);
  477. default: return p;
  478. }
  479. }
  480. #endregion
  481. #region 坐标转换
  482. public Point ToCanvasPoint(Point screenPoint)
  483. {
  484. return new Point(
  485. (screenPoint.X - _translate.X) / _scale.ScaleX,
  486. (screenPoint.Y - _translate.Y) / _scale.ScaleY);
  487. }
  488. #endregion
  489. #region 缩放 & 平移
  490. public void ZoomAt(Point center, double newScale)
  491. {
  492. newScale = Math.Max(MinZoom, Math.Min(MaxZoom, newScale));
  493. if (Math.Abs(newScale - _scale.ScaleX) < 0.001) return;
  494. // center 是 GetPosition(this) 返回的画布本地坐标
  495. // 保持 center 对应的屏幕点不变:screen = center * oldScale + oldTranslate = center * newScale + newTranslate
  496. _translate.X = _translate.X + center.X * (_scale.ScaleX - newScale);
  497. _translate.Y = _translate.Y + center.Y * (_scale.ScaleY - newScale);
  498. _scale.ScaleX = newScale;
  499. _scale.ScaleY = newScale;
  500. ClampTranslate();
  501. ZoomChanged?.Invoke(newScale);
  502. }
  503. protected override void OnMouseWheel(MouseWheelEventArgs e)
  504. {
  505. if ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
  506. {
  507. // Ctrl + 滚轮 = 缩放
  508. var mousePos = e.GetPosition(this);
  509. double factor = e.Delta > 0 ? ZoomFactor : 1 / ZoomFactor;
  510. ZoomAt(mousePos, _scale.ScaleX * factor);
  511. e.Handled = true;
  512. }
  513. else if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift)
  514. {
  515. // Shift + 滚轮 = 水平平移
  516. _translate.X += e.Delta * 0.5;
  517. ClampTranslate();
  518. e.Handled = true;
  519. }
  520. else
  521. {
  522. // 滚轮 = 上下平移
  523. _translate.Y += e.Delta * 0.5;
  524. ClampTranslate();
  525. e.Handled = true;
  526. }
  527. }
  528. protected override void OnMouseDown(MouseButtonEventArgs e)
  529. {
  530. base.OnMouseDown(e);
  531. if (e.ChangedButton == MouseButton.Middle)
  532. {
  533. _isPanning = true;
  534. _panStart = e.GetPosition(Parent as IInputElement);
  535. _panOrigin = new Point(_translate.X, _translate.Y);
  536. CaptureMouse();
  537. e.Handled = true;
  538. }
  539. }
  540. protected override void OnMouseMove(MouseEventArgs e)
  541. {
  542. base.OnMouseMove(e);
  543. var pos = e.GetPosition(this);
  544. if (_isPanning)
  545. {
  546. var panPos = e.GetPosition(Parent as IInputElement);
  547. _translate.X = _panOrigin.X + (panPos.X - _panStart.X);
  548. _translate.Y = _panOrigin.Y + (panPos.Y - _panStart.Y);
  549. ClampTranslate();
  550. }
  551. else if (_isConnecting && _tempPath != null)
  552. {
  553. Point start = GetPortPosition(_connectSourceNode, _connectSourceSide);
  554. PortSide endSide = DetermineEndSide(start, pos, _connectSourceSide);
  555. _tempPath.Data = CreateConnectionGeometry(start, pos, _connectSourceSide, endSide);
  556. }
  557. else if (_isBoxSelecting)
  558. {
  559. UpdateSelectionBox(pos);
  560. }
  561. }
  562. protected override void OnMouseUp(MouseButtonEventArgs e)
  563. {
  564. base.OnMouseUp(e);
  565. if (e.ChangedButton == MouseButton.Middle)
  566. {
  567. _isPanning = false;
  568. ReleaseMouseCapture();
  569. e.Handled = true;
  570. }
  571. }
  572. #endregion
  573. #region 左键交互
  574. // 双击检测 + 单击选中都用 Preview(隧道事件),在 MoveThumb 捕获鼠标之前触发
  575. protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
  576. {
  577. if (e.ClickCount == 2)
  578. {
  579. var dblNode = FindAncestor<NodeControl>(e.OriginalSource as DependencyObject);
  580. if (dblNode != null)
  581. {
  582. // 流程运行中 / 只读监控模式禁止双击打开编辑器
  583. if (IsRunning || IsReadOnly) { e.Handled = true; return; }
  584. NodeDoubleClicked?.Invoke(dblNode.GetNode());
  585. e.Handled = true;
  586. return;
  587. }
  588. }
  589. else if (e.ClickCount == 1)
  590. {
  591. // 单击节点选中(在 MoveThumb 捕获鼠标之前处理)
  592. var nodeCtrl = FindAncestor<NodeControl>(e.OriginalSource as DependencyObject);
  593. if (nodeCtrl != null)
  594. {
  595. Focus();
  596. bool isCtrl = (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control;
  597. SelectNode(nodeCtrl.GetNode(), isCtrl);
  598. // 不设 Handled,让 MoveThumb 继续处理拖拽
  599. }
  600. }
  601. base.OnPreviewMouseLeftButtonDown(e);
  602. }
  603. protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
  604. {
  605. base.OnMouseLeftButtonDown(e);
  606. Focus();
  607. var pos = e.GetPosition(this);
  608. var src = e.OriginalSource as DependencyObject;
  609. // 1. 检查连接点
  610. var connector = FindAncestor<ConnectorControl>(src);
  611. if (connector != null)
  612. {
  613. StartConnection(connector);
  614. e.Handled = true;
  615. return;
  616. }
  617. // 2. 检查连线(Path with FlowConnection Tag)
  618. if (src is Path path && path.Tag is FlowConnection conn)
  619. {
  620. SelectConnection(conn);
  621. e.Handled = true;
  622. return;
  623. }
  624. // 3. 检查节点
  625. var nodeCtrl = FindAncestor<NodeControl>(src);
  626. if (nodeCtrl != null)
  627. {
  628. bool isCtrl = (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control;
  629. SelectNode(nodeCtrl.GetNode(), isCtrl);
  630. return; // 不设 Handled,让 MoveThumb 处理拖拽
  631. }
  632. // 4. 空白区域 - 框选
  633. ClearSelection();
  634. StartBoxSelect(pos);
  635. e.Handled = true;
  636. }
  637. protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
  638. {
  639. base.OnMouseLeftButtonUp(e);
  640. if (_isConnecting)
  641. EndConnection(e.GetPosition(this));
  642. if (_isBoxSelecting)
  643. EndBoxSelect();
  644. }
  645. #endregion
  646. #region 右键菜单
  647. protected override void OnMouseRightButtonDown(MouseButtonEventArgs e)
  648. {
  649. base.OnMouseRightButtonDown(e);
  650. Focus();
  651. var pos = e.GetPosition(this);
  652. var src = e.OriginalSource as DependencyObject;
  653. var nodeCtrl = FindAncestor<NodeControl>(src);
  654. if (nodeCtrl != null)
  655. {
  656. var node = nodeCtrl.GetNode();
  657. // 如果右键的节点不在已选列表中,单选它
  658. if (!_selectedNodes.Contains(node) && _selectedNode != node)
  659. SelectNode(node, false);
  660. else if (_selectedNode == null && _selectedNodes.Count == 0)
  661. SelectNode(node, false);
  662. ShowNodeContextMenu(node);
  663. e.Handled = true;
  664. }
  665. else
  666. {
  667. ShowCanvasContextMenu(pos);
  668. e.Handled = true;
  669. }
  670. }
  671. private void ShowNodeContextMenu(FlowNode node)
  672. {
  673. var menu = new ContextMenu();
  674. bool readOnly = IsReadOnly || IsRunning;
  675. // 复制
  676. var miCopy = new MenuItem { Header = "复制", IsEnabled = !readOnly };
  677. miCopy.Click += (s, e) => CopySelectedNodes();
  678. menu.Items.Add(miCopy);
  679. // 禁用/启用节点
  680. var miToggleEnabled = new MenuItem { Header = node.IsEnabled ? "禁用节点" : "启用节点", IsEnabled = !readOnly };
  681. miToggleEnabled.Click += (s, e) => ToggleNodeEnabled(node);
  682. menu.Items.Add(miToggleEnabled);
  683. menu.Items.Add(new Separator());
  684. // 删除节点
  685. var miDelNode = new MenuItem { Header = "删除节点", IsEnabled = !readOnly };
  686. miDelNode.Click += (s, e) => DeleteSelectedNodes();
  687. menu.Items.Add(miDelNode);
  688. // 删除连接线
  689. var miDelConn = new MenuItem { Header = "删除连接线", IsEnabled = !readOnly };
  690. miDelConn.Click += (s, e) => DeleteSelectedConnections();
  691. menu.Items.Add(miDelConn);
  692. // 运行和从此处运行:仅单选时显示(只读模式下不允许从编辑器发起运行)
  693. bool isSingle = _selectedNodes.Count <= 1 && _selectedNode != null;
  694. if (isSingle)
  695. {
  696. menu.Items.Add(new Separator());
  697. var miRun = new MenuItem { Header = "运行", IsEnabled = !readOnly };
  698. miRun.Click += (s, e) => NodeRunRequested?.Invoke(node);
  699. menu.Items.Add(miRun);
  700. var miRunFrom = new MenuItem { Header = "此处开始运行", IsEnabled = !readOnly };
  701. miRunFrom.Click += (s, e) => NodeRunFromHereRequested?.Invoke(node);
  702. menu.Items.Add(miRunFrom);
  703. menu.Items.Add(new Separator());
  704. var miProp = new MenuItem { Header = "属性", IsEnabled = !readOnly };
  705. miProp.Click += (s, e) => NodePropertiesRequested?.Invoke(node);
  706. menu.Items.Add(miProp);
  707. }
  708. menu.IsOpen = true;
  709. }
  710. /// <summary>
  711. /// 切换节点启用状态(禁用后不执行、背景变灰)
  712. /// </summary>
  713. private void ToggleNodeEnabled(FlowNode node)
  714. {
  715. if (node == null) return;
  716. node.IsEnabled = !node.IsEnabled;
  717. }
  718. private void ShowCanvasContextMenu(Point pos)
  719. {
  720. var menu = new ContextMenu();
  721. var miLayout = new MenuItem { Header = "自动排版" };
  722. miLayout.Click += (s, e) => AutoLayout();
  723. menu.Items.Add(miLayout);
  724. menu.Items.Add(new Separator());
  725. var miPaste = new MenuItem { Header = "粘贴" };
  726. miPaste.Click += (s, e) => PasteNodesAt(pos);
  727. miPaste.IsEnabled = !IsReadOnly && !IsRunning && HasClipboardData();
  728. menu.Items.Add(miPaste);
  729. menu.IsOpen = true;
  730. }
  731. #endregion
  732. #region 连线绘制
  733. private void StartConnection(ConnectorControl connector)
  734. {
  735. if (IsRunning || IsReadOnly) return; // 流程运行中 / 只读模式禁止新建连线
  736. var node = connector.GetNode();
  737. if (node == null) return;
  738. _isConnecting = true;
  739. _connectSourceNode = node;
  740. _connectSourceSide = connector.Side;
  741. _tempPath = new Path
  742. {
  743. Stroke = new SolidColorBrush(Color.FromRgb(232, 145, 73)),
  744. StrokeThickness = 2,
  745. StrokeDashArray = new DoubleCollection { 4, 2 },
  746. IsHitTestVisible = false
  747. };
  748. Children.Add(_tempPath);
  749. CaptureMouse();
  750. }
  751. private void EndConnection(Point mousePos)
  752. {
  753. _isConnecting = false;
  754. ReleaseMouseCapture();
  755. if (_tempPath != null)
  756. {
  757. Children.Remove(_tempPath);
  758. _tempPath = null;
  759. }
  760. if (_connectSourceNode == null || _graph == null) return;
  761. // 手动命中检测:遍历所有节点的4个端口,找到最近的
  762. const double hitRadius = 22.0;
  763. double minDist = double.MaxValue;
  764. FlowNode targetNode = null;
  765. PortSide targetSide = PortSide.Left;
  766. var sides = new[] { PortSide.Left, PortSide.Top, PortSide.Right, PortSide.Bottom };
  767. foreach (var node in _graph.Nodes)
  768. {
  769. if (node.NodeId == _connectSourceNode.NodeId) continue;
  770. foreach (var side in sides)
  771. {
  772. var pos = GetPortPosition(node, side);
  773. double d = PointDist(pos, mousePos);
  774. if (d < hitRadius && d < minDist)
  775. {
  776. minDist = d;
  777. targetNode = node;
  778. targetSide = side;
  779. }
  780. }
  781. }
  782. if (targetNode == null) { _connectSourceNode = null; return; }
  783. // 源 = 拖出端,目标 = 放入端。不检查方向类型。
  784. var conn = new FlowConnection
  785. {
  786. SourceNodeId = _connectSourceNode.NodeId,
  787. SourceSide = _connectSourceSide,
  788. TargetNodeId = targetNode.NodeId,
  789. TargetSide = targetSide
  790. };
  791. _graph?.TryAddConnection(conn);
  792. _connectSourceNode = null;
  793. }
  794. private static double PointDist(Point a, Point b)
  795. {
  796. double dx = a.X - b.X;
  797. double dy = a.Y - b.Y;
  798. return Math.Sqrt(dx * dx + dy * dy);
  799. }
  800. #endregion
  801. #region 框选
  802. private void StartBoxSelect(Point pos)
  803. {
  804. _isBoxSelecting = true;
  805. _boxSelectStart = pos;
  806. _selectionBox = new Rectangle
  807. {
  808. Stroke = new SolidColorBrush(Color.FromArgb(100, 0, 122, 204)),
  809. Fill = new SolidColorBrush(Color.FromArgb(30, 0, 122, 204)),
  810. StrokeThickness = 1,
  811. IsHitTestVisible = false
  812. };
  813. Children.Add(_selectionBox);
  814. CaptureMouse();
  815. }
  816. private void UpdateSelectionBox(Point pos)
  817. {
  818. if (_selectionBox == null) return;
  819. double x = Math.Min(_boxSelectStart.X, pos.X);
  820. double y = Math.Min(_boxSelectStart.Y, pos.Y);
  821. double w = Math.Abs(pos.X - _boxSelectStart.X);
  822. double h = Math.Abs(pos.Y - _boxSelectStart.Y);
  823. SetLeft(_selectionBox, x);
  824. SetTop(_selectionBox, y);
  825. _selectionBox.Width = w;
  826. _selectionBox.Height = h;
  827. }
  828. private void EndBoxSelect()
  829. {
  830. _isBoxSelecting = false;
  831. ReleaseMouseCapture();
  832. if (_selectionBox != null)
  833. {
  834. double x = GetLeft(_selectionBox);
  835. double y = GetTop(_selectionBox);
  836. double w = _selectionBox.Width;
  837. double h = _selectionBox.Height;
  838. var rect = new Rect(x, y, w, h);
  839. // 清除旧选择
  840. foreach (var n in _selectedNodes)
  841. n.IsSelected = false;
  842. _selectedNodes.Clear();
  843. // 多选模式:选中框选范围内的所有节点
  844. foreach (var node in _graph?.Nodes ?? Enumerable.Empty<FlowNode>())
  845. {
  846. var nodeRect = new Rect(node.X, node.Y, node.NodeWidth, node.NodeHeight);
  847. if (rect.IntersectsWith(nodeRect))
  848. {
  849. node.IsSelected = true;
  850. _selectedNodes.Add(node);
  851. }
  852. }
  853. if (_selectedNodes.Count > 0)
  854. {
  855. _selectedNode = _selectedNodes[_selectedNodes.Count - 1];
  856. NodeSelected?.Invoke(_selectedNode);
  857. }
  858. Children.Remove(_selectionBox);
  859. _selectionBox = null;
  860. }
  861. }
  862. #endregion
  863. #region 选择
  864. public void SelectNode(FlowNode node, bool isMultiSelect = false)
  865. {
  866. if (isMultiSelect && node != null)
  867. {
  868. // Ctrl+Click: 切换该节点的选中状态
  869. if (_selectedNodes.Contains(node))
  870. {
  871. _selectedNodes.Remove(node);
  872. node.IsSelected = false;
  873. }
  874. else
  875. {
  876. _selectedNodes.Add(node);
  877. node.IsSelected = true;
  878. }
  879. _selectedNode = node;
  880. if (_selectedConnection != null)
  881. {
  882. _selectedConnection.IsSelected = false;
  883. if (_connectionPaths.TryGetValue(_selectedConnection.ConnectionId, out var p))
  884. UpdateConnectionPath(p, _selectedConnection);
  885. _selectedConnection = null;
  886. }
  887. NodeSelected?.Invoke(node);
  888. return;
  889. }
  890. // 普通点击:如果点击的节点已在多选列表中,保持多选不变(用于拖拽)
  891. if (node != null && _selectedNodes.Contains(node))
  892. {
  893. _selectedNode = node;
  894. if (_selectedConnection != null)
  895. {
  896. _selectedConnection.IsSelected = false;
  897. if (_connectionPaths.TryGetValue(_selectedConnection.ConnectionId, out var p))
  898. UpdateConnectionPath(p, _selectedConnection);
  899. _selectedConnection = null;
  900. }
  901. NodeSelected?.Invoke(node);
  902. return;
  903. }
  904. // 否则清除多选,单选
  905. foreach (var n in _selectedNodes)
  906. n.IsSelected = false;
  907. _selectedNodes.Clear();
  908. if (_selectedNode == node && _selectedConnection == null) return;
  909. if (_selectedNode != null)
  910. _selectedNode.IsSelected = false;
  911. if (_selectedConnection != null)
  912. {
  913. _selectedConnection.IsSelected = false;
  914. if (_connectionPaths.TryGetValue(_selectedConnection.ConnectionId, out var p))
  915. UpdateConnectionPath(p, _selectedConnection);
  916. }
  917. _selectedNode = node;
  918. _selectedConnection = null;
  919. if (node != null)
  920. node.IsSelected = true;
  921. NodeSelected?.Invoke(node);
  922. }
  923. private void SelectConnection(FlowConnection conn)
  924. {
  925. foreach (var n in _selectedNodes)
  926. n.IsSelected = false;
  927. _selectedNodes.Clear();
  928. if (_selectedNode != null)
  929. _selectedNode.IsSelected = false;
  930. if (_selectedConnection != null)
  931. {
  932. _selectedConnection.IsSelected = false;
  933. if (_connectionPaths.TryGetValue(_selectedConnection.ConnectionId, out var p))
  934. UpdateConnectionPath(p, _selectedConnection);
  935. }
  936. _selectedNode = null;
  937. _selectedConnection = conn;
  938. if (conn != null)
  939. {
  940. conn.IsSelected = true;
  941. if (_connectionPaths.TryGetValue(conn.ConnectionId, out var p))
  942. UpdateConnectionPath(p, conn);
  943. }
  944. ConnectionSelected?.Invoke(conn);
  945. }
  946. public void ClearSelection()
  947. {
  948. foreach (var n in _selectedNodes)
  949. n.IsSelected = false;
  950. _selectedNodes.Clear();
  951. if (_selectedNode != null)
  952. _selectedNode.IsSelected = false;
  953. if (_selectedConnection != null)
  954. {
  955. _selectedConnection.IsSelected = false;
  956. if (_connectionPaths.TryGetValue(_selectedConnection.ConnectionId, out var p))
  957. UpdateConnectionPath(p, _selectedConnection);
  958. }
  959. _selectedNode = null;
  960. _selectedConnection = null;
  961. SelectionCleared?.Invoke();
  962. }
  963. #endregion
  964. #region 拖放
  965. protected override void OnDrop(DragEventArgs e)
  966. {
  967. base.OnDrop(e);
  968. NodePluginInfo pluginInfo = null;
  969. // 尝试从 DataObject 获取插件信息
  970. if (e.Data.GetDataPresent(typeof(NodePluginInfo)))
  971. {
  972. pluginInfo = e.Data.GetData(typeof(NodePluginInfo)) as NodePluginInfo;
  973. }
  974. else if (e.Data.GetDataPresent(DataFormats.StringFormat))
  975. {
  976. var str = e.Data.GetData(DataFormats.StringFormat) as string;
  977. if (!string.IsNullOrEmpty(str))
  978. pluginInfo = PluginLoader.Instance.GetPluginInfo(str);
  979. }
  980. if (pluginInfo != null)
  981. {
  982. if (IsRunning || IsReadOnly)
  983. {
  984. e.Handled = true; // 流程运行中 / 只读模式禁止添加插件
  985. return;
  986. }
  987. // 预览与放置共用 e.GetPosition(实测正确);不用 GetCursorPos/PointFromScreen
  988. //(其内部窗口原点与 PointToScreen 不一致,PerMonitorV2 下会算出错误位置)
  989. var pos = e.GetPosition(this);
  990. double dropX = pos.X - NodeControl.DefaultWidth / 2;
  991. double dropY = pos.Y - NodeControl.DefaultHeight / 2;
  992. // 避免与现有节点重叠
  993. var freePos = FindFreePosition(dropX, dropY);
  994. NodeDropRequested?.Invoke(freePos.X, freePos.Y, pluginInfo);
  995. UpdateCanvasSize();
  996. e.Handled = true;
  997. }
  998. }
  999. #endregion
  1000. #region 键盘
  1001. protected override void OnKeyDown(KeyEventArgs e)
  1002. {
  1003. base.OnKeyDown(e);
  1004. if (e.Key == Key.Delete || e.Key == Key.Back)
  1005. {
  1006. if (IsRunning || IsReadOnly)
  1007. {
  1008. e.Handled = true;
  1009. return;
  1010. }
  1011. // 批量删除多选节点
  1012. if (_selectedNodes.Count > 0)
  1013. {
  1014. foreach (var node in _selectedNodes.ToList())
  1015. _graph?.RemoveNode(node.NodeId);
  1016. _selectedNodes.Clear();
  1017. _selectedNode = null;
  1018. e.Handled = true;
  1019. }
  1020. else if (_selectedNode != null)
  1021. {
  1022. _graph?.RemoveNode(_selectedNode.NodeId);
  1023. _selectedNode = null;
  1024. e.Handled = true;
  1025. }
  1026. else if (_selectedConnection != null)
  1027. {
  1028. _graph?.RemoveConnection(_selectedConnection.ConnectionId);
  1029. _selectedConnection = null;
  1030. e.Handled = true;
  1031. }
  1032. }
  1033. else if (e.Key == Key.C && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
  1034. {
  1035. if (IsRunning || IsReadOnly)
  1036. {
  1037. e.Handled = true;
  1038. return;
  1039. }
  1040. CopySelectedNodes();
  1041. e.Handled = true;
  1042. }
  1043. else if (e.Key == Key.V && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
  1044. {
  1045. if (IsRunning || IsReadOnly)
  1046. {
  1047. e.Handled = true;
  1048. return;
  1049. }
  1050. PasteNodes();
  1051. e.Handled = true;
  1052. }
  1053. else if (e.Key == Key.A && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
  1054. {
  1055. SelectAllNodes();
  1056. e.Handled = true;
  1057. }
  1058. }
  1059. #endregion
  1060. #region 复制粘贴
  1061. /// <summary>
  1062. /// 获取当前选中的节点列表(供 MoveThumb 多选拖拽使用)
  1063. /// </summary>
  1064. public List<FlowNode> GetSelectedNodes() => _selectedNodes;
  1065. /// <summary>
  1066. /// 全选所有节点
  1067. /// </summary>
  1068. private void SelectAllNodes()
  1069. {
  1070. foreach (var n in _selectedNodes)
  1071. n.IsSelected = false;
  1072. _selectedNodes.Clear();
  1073. foreach (var node in _graph?.Nodes ?? Enumerable.Empty<FlowNode>())
  1074. {
  1075. node.IsSelected = true;
  1076. _selectedNodes.Add(node);
  1077. }
  1078. if (_selectedNodes.Count > 0)
  1079. {
  1080. _selectedNode = _selectedNodes[_selectedNodes.Count - 1];
  1081. NodeSelected?.Invoke(_selectedNode);
  1082. }
  1083. }
  1084. private bool HasClipboardData()
  1085. {
  1086. try
  1087. {
  1088. var text = System.Windows.Clipboard.GetText();
  1089. return !string.IsNullOrEmpty(text) && text.StartsWith(ClipboardPrefix);
  1090. }
  1091. catch { return false; }
  1092. }
  1093. private void CopySelectedNodes()
  1094. {
  1095. // 如果多选列表为空但单选不为空,将单选加入多选列表
  1096. if (_selectedNodes.Count == 0 && _selectedNode != null)
  1097. {
  1098. _selectedNode.IsSelected = true;
  1099. _selectedNodes.Add(_selectedNode);
  1100. }
  1101. if (_selectedNodes.Count == 0) return;
  1102. var nodes = new List<FlowNode>();
  1103. var idMap = new Dictionary<string, string>();
  1104. foreach (var node in _selectedNodes)
  1105. {
  1106. var clone = CloneNode(node);
  1107. clone.NodeId = System.Guid.NewGuid().ToString("N");
  1108. idMap[node.NodeId] = clone.NodeId;
  1109. nodes.Add(clone);
  1110. }
  1111. // 复制选中节点之间的连接线(映射到剪贴板节点的ID)
  1112. var nodeIds = new HashSet<string>(_selectedNodes.Select(n => n.NodeId));
  1113. var connections = _graph?.Connections
  1114. .Where(c => nodeIds.Contains(c.SourceNodeId) && nodeIds.Contains(c.TargetNodeId))
  1115. .Select(c => new FlowConnection
  1116. {
  1117. SourceNodeId = idMap[c.SourceNodeId],
  1118. SourceSide = c.SourceSide,
  1119. TargetNodeId = idMap[c.TargetNodeId],
  1120. TargetSide = c.TargetSide,
  1121. BranchCondition = c.BranchCondition
  1122. })
  1123. .ToList();
  1124. // 序列化到系统剪贴板
  1125. try
  1126. {
  1127. var data = new ClipboardData { Nodes = nodes, Connections = connections };
  1128. var formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
  1129. using (var ms = new System.IO.MemoryStream())
  1130. {
  1131. formatter.Serialize(ms, data);
  1132. var base64 = Convert.ToBase64String(ms.ToArray());
  1133. System.Windows.Clipboard.SetText(ClipboardPrefix + base64);
  1134. }
  1135. }
  1136. catch { }
  1137. }
  1138. private void PasteNodes()
  1139. {
  1140. PasteNodesAt(Mouse.GetPosition(this));
  1141. }
  1142. /// <summary>
  1143. /// 在指定位置粘贴节点
  1144. /// </summary>
  1145. public void PasteNodesAt(Point pos)
  1146. {
  1147. if (_graph == null || IsRunning || IsReadOnly) return;
  1148. // 从系统剪贴板读取
  1149. List<FlowNode> clipNodes = null;
  1150. List<FlowConnection> clipConns = null;
  1151. try
  1152. {
  1153. var text = System.Windows.Clipboard.GetText();
  1154. if (!string.IsNullOrEmpty(text) && text.StartsWith(ClipboardPrefix))
  1155. {
  1156. var bytes = Convert.FromBase64String(text.Substring(ClipboardPrefix.Length));
  1157. var formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
  1158. using (var ms = new System.IO.MemoryStream(bytes))
  1159. {
  1160. var data = (ClipboardData)formatter.Deserialize(ms);
  1161. clipNodes = data.Nodes;
  1162. clipConns = data.Connections;
  1163. }
  1164. }
  1165. }
  1166. catch { return; }
  1167. if (clipNodes == null || clipNodes.Count == 0) return;
  1168. ClearSelection();
  1169. // 计算偏移:让剪贴板内容的左上角对齐到鼠标位置
  1170. double minX = double.MaxValue, minY = double.MaxValue;
  1171. foreach (var data in clipNodes)
  1172. {
  1173. if (data.X < minX) minX = data.X;
  1174. if (data.Y < minY) minY = data.Y;
  1175. }
  1176. double offsetX = pos.X - minX;
  1177. double offsetY = pos.Y - minY;
  1178. var newNodes = new List<FlowNode>();
  1179. foreach (var data in clipNodes)
  1180. {
  1181. var newNode = CloneNode(data);
  1182. newNode.X = data.X + offsetX;
  1183. newNode.Y = data.Y + offsetY;
  1184. newNode.IsSelected = true;
  1185. int suffix = 1;
  1186. while (Graph.Nodes.Any(n => n.NodeName == newNode.NodeName))
  1187. newNode.NodeName = $"{newNode.PluginModel.GetModel.ToolName}{++suffix}";
  1188. // 重新初始化所有ID(FlowId/FlowName/NodeId/PluginModel/Group子节点)
  1189. newNode.InitializeNode(_graph.GraphId, _graph.GraphName, _graph.Registry);
  1190. _graph.AddNode(newNode);
  1191. newNodes.Add(newNode);
  1192. }
  1193. _selectedNodes.AddRange(newNodes);
  1194. if (newNodes.Count > 0)
  1195. _selectedNode = newNodes[0];
  1196. // 复制连接线
  1197. if (clipConns != null && clipConns.Count > 0)
  1198. {
  1199. var idMap = new Dictionary<string, string>();
  1200. for (int i = 0; i < clipNodes.Count && i < newNodes.Count; i++)
  1201. idMap[clipNodes[i].NodeId] = newNodes[i].NodeId;
  1202. foreach (var conn in clipConns)
  1203. {
  1204. if (idMap.TryGetValue(conn.SourceNodeId, out var newSrcId) &&
  1205. idMap.TryGetValue(conn.TargetNodeId, out var newTgtId))
  1206. {
  1207. _graph.TryAddConnection(new FlowConnection
  1208. {
  1209. SourceNodeId = newSrcId,
  1210. SourceSide = conn.SourceSide,
  1211. TargetNodeId = newTgtId,
  1212. TargetSide = conn.TargetSide,
  1213. BranchCondition = conn.BranchCondition
  1214. });
  1215. }
  1216. }
  1217. }
  1218. UpdateCanvasSize();
  1219. }
  1220. /// <summary>
  1221. /// 删除所有选中节点
  1222. /// </summary>
  1223. public void DeleteSelectedNodes()
  1224. {
  1225. if (_graph == null || IsRunning || IsReadOnly) return;
  1226. if (_selectedNodes.Count > 0)
  1227. {
  1228. foreach (var node in _selectedNodes.ToList())
  1229. _graph.RemoveNode(node.NodeId);
  1230. _selectedNodes.Clear();
  1231. _selectedNode = null;
  1232. }
  1233. else if (_selectedNode != null)
  1234. {
  1235. _graph.RemoveNode(_selectedNode.NodeId);
  1236. _selectedNode = null;
  1237. }
  1238. }
  1239. /// <summary>
  1240. /// 删除选中节点的所有连接线
  1241. /// </summary>
  1242. public void DeleteSelectedConnections()
  1243. {
  1244. if (_graph == null || IsRunning || IsReadOnly) return;
  1245. var nodes = _selectedNodes.Count > 0 ? _selectedNodes.ToList() :
  1246. (_selectedNode != null ? new List<FlowNode> { _selectedNode } : new List<FlowNode>());
  1247. if (nodes.Count == 0) return;
  1248. var nodeIds = new HashSet<string>(nodes.Select(n => n.NodeId));
  1249. var connectionsToRemove = _graph.Connections
  1250. .Where(c => nodeIds.Contains(c.SourceNodeId) || nodeIds.Contains(c.TargetNodeId))
  1251. .ToList();
  1252. foreach (var conn in connectionsToRemove)
  1253. _graph.RemoveConnection(conn.ConnectionId);
  1254. }
  1255. /// <summary>
  1256. /// 深拷贝节点(使用 BinaryFormatter,完整复制 PluginModel)
  1257. /// </summary>
  1258. private static FlowNode CloneNode(FlowNode source)
  1259. {
  1260. try
  1261. {
  1262. #pragma warning disable SYSLIB0011
  1263. var formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
  1264. using (var stream = new System.IO.MemoryStream())
  1265. {
  1266. formatter.Serialize(stream, source);
  1267. stream.Position = 0;
  1268. return (FlowNode)formatter.Deserialize(stream);
  1269. }
  1270. #pragma warning restore SYSLIB0011
  1271. }
  1272. catch(Exception ex)
  1273. {
  1274. System.Diagnostics.Debug.WriteLine($"[CloneNode] 序列化失败: {ex}");
  1275. DialogHelper.Error($"节点复制失败: {ex.Message}");
  1276. return default;
  1277. }
  1278. }
  1279. #endregion
  1280. [System.Serializable]
  1281. private class ClipboardData
  1282. {
  1283. public List<FlowNode> Nodes;
  1284. public List<FlowConnection> Connections;
  1285. }
  1286. #region 辅助
  1287. private static T FindAncestor<T>(DependencyObject current) where T : DependencyObject
  1288. {
  1289. while (current != null && !(current is T))
  1290. current = VisualTreeHelper.GetParent(current);
  1291. return current as T;
  1292. }
  1293. #endregion
  1294. #region 节点碰撞检测
  1295. /// <summary>
  1296. /// 检查指定位置是否会与其他节点碰撞
  1297. /// </summary>
  1298. public bool CheckNodeCollision(FlowNode draggingNode, double newX, double newY)
  1299. {
  1300. return CheckNodeCollision(draggingNode, newX, newY, null);
  1301. }
  1302. /// <summary>
  1303. /// 检查指定位置是否会与其他节点碰撞。
  1304. /// ignoreNodes 中的节点视为"与拖拽节点同步移动"(多选拖拽的同组成员):
  1305. /// 它们相对位置不变、不会与拖拽成员相撞,因此不作为障碍参与检测。
  1306. /// </summary>
  1307. public bool CheckNodeCollision(FlowNode draggingNode, double newX, double newY, System.Collections.Generic.ICollection<FlowNode> ignoreNodes)
  1308. {
  1309. if (_graph?.Nodes == null) return false;
  1310. var newRect = new Rect(newX, newY, draggingNode.NodeWidth, draggingNode.NodeHeight);
  1311. foreach (var node in _graph.Nodes)
  1312. {
  1313. if (node.NodeId == draggingNode.NodeId) continue;
  1314. if (ignoreNodes != null && ignoreNodes.Contains(node)) continue;
  1315. var nodeRect = new Rect(node.X, node.Y, node.NodeWidth, node.NodeHeight);
  1316. if (newRect.IntersectsWith(nodeRect))
  1317. return true;
  1318. }
  1319. return false;
  1320. }
  1321. /// <summary>
  1322. /// 从指定位置开始查找不与任何节点重叠的空闲位置
  1323. /// </summary>
  1324. public Point FindFreePosition(double startX, double startY)
  1325. {
  1326. double x = startX, y = startY;
  1327. if (_graph?.Nodes == null) return new Point(x, y);
  1328. bool collision;
  1329. do
  1330. {
  1331. collision = false;
  1332. var dropRect = new Rect(x, y, NodeControl.DefaultWidth, NodeControl.DefaultHeight);
  1333. foreach (var node in _graph.Nodes)
  1334. {
  1335. var nodeRect = new Rect(node.X, node.Y, node.NodeWidth, node.NodeHeight);
  1336. if (nodeRect.IntersectsWith(dropRect))
  1337. {
  1338. x += 20;
  1339. y += 20;
  1340. collision = true;
  1341. break;
  1342. }
  1343. }
  1344. } while (collision);
  1345. return new Point(x, y);
  1346. }
  1347. #endregion
  1348. #region 自动拉线
  1349. /// <summary>
  1350. /// 自动拉线 - 单节点拖拽结束时,若节点贴近其他节点则自动建立连接。
  1351. /// 规则:靠下和靠右时其他节点当头(本节点作目标);靠上和靠左时本节点当头(本节点作源)。
  1352. /// 阈值:贴边距离 40px 内,且对应方向重叠量达到较小节点尺寸的 80%。
  1353. /// 拖拽过程中通过 UpdateAutoConnectPreview 显示虚线预览,拉远自动消失。
  1354. /// </summary>
  1355. public void TryAutoConnect(FlowNode node)
  1356. {
  1357. if (node == null || _graph == null || _graph.Nodes == null || _graph.Connections == null)
  1358. {
  1359. ClearPreviewLine();
  1360. return;
  1361. }
  1362. const double threshold = 40; // 贴边距离阈值(px)
  1363. var pos = GetNodeVisualBounds(node);
  1364. foreach (var other in _graph.Nodes)
  1365. {
  1366. if (other == node) continue;
  1367. var oPos = GetNodeVisualBounds(other);
  1368. // 1. 本节点在 other 正下方 → 上连下(other.Bottom → node.Top)
  1369. if (HorizontalOverlap(pos, oPos) && VerticalGap(pos.Top, oPos.Bottom) <= threshold)
  1370. {
  1371. if (TryAutoAdd(other, PortSide.Bottom, node, PortSide.Top))
  1372. {
  1373. ClearPreviewLine();
  1374. return; // 每次拖拽只自动连一条
  1375. }
  1376. }
  1377. // 2. 本节点在 other 正上方 → 本节点当头(node.Bottom → other.Top)
  1378. if (HorizontalOverlap(pos, oPos) && VerticalGap(oPos.Top, pos.Bottom) <= threshold)
  1379. {
  1380. if (TryAutoAdd(node, PortSide.Bottom, other, PortSide.Top))
  1381. {
  1382. ClearPreviewLine();
  1383. return;
  1384. }
  1385. }
  1386. // 3. 本节点在 other 正右方 →(other.Right → node.Left)
  1387. if (VerticalOverlap(pos, oPos) && HorizontalGap(pos.Left, oPos.Right) <= threshold)
  1388. {
  1389. if (TryAutoAdd(other, PortSide.Right, node, PortSide.Left))
  1390. {
  1391. ClearPreviewLine();
  1392. return;
  1393. }
  1394. }
  1395. // 4. 本节点在 other 正左方 → 本节点当头(node.Right → other.Left)
  1396. if (VerticalOverlap(pos, oPos) && HorizontalGap(oPos.Left, pos.Right) <= threshold)
  1397. {
  1398. if (TryAutoAdd(node, PortSide.Right, other, PortSide.Left))
  1399. {
  1400. ClearPreviewLine();
  1401. return;
  1402. }
  1403. }
  1404. }
  1405. // 没有成功连接 → 清除预览
  1406. ClearPreviewLine();
  1407. }
  1408. /// <summary>
  1409. /// 拖拽过程中更新自动拉线虚线预览;靠近且可连接时显示,否则清除
  1410. /// </summary>
  1411. public void UpdateAutoConnectPreview(FlowNode node)
  1412. {
  1413. ClearPreviewLine();
  1414. if (node == null || _graph == null || _graph.Nodes == null || _graph.Connections == null)
  1415. return;
  1416. const double threshold = 40; // 贴边距离阈值(px)
  1417. var pos = GetNodeVisualBounds(node);
  1418. foreach (var other in _graph.Nodes)
  1419. {
  1420. if (other == node) continue;
  1421. var oPos = GetNodeVisualBounds(other);
  1422. FlowNode source = null;
  1423. PortSide sourceSide = PortSide.Bottom;
  1424. FlowNode target = null;
  1425. PortSide targetSide = PortSide.Top;
  1426. // 1. 本节点在 other 正下方 → 上连下(other.Bottom → node.Top)
  1427. if (HorizontalOverlap(pos, oPos) && VerticalGap(pos.Top, oPos.Bottom) <= threshold)
  1428. {
  1429. source = other; sourceSide = PortSide.Bottom;
  1430. target = node; targetSide = PortSide.Top;
  1431. }
  1432. // 2. 本节点在 other 正上方 → 本节点当头(node.Bottom → other.Top)
  1433. else if (HorizontalOverlap(pos, oPos) && VerticalGap(oPos.Top, pos.Bottom) <= threshold)
  1434. {
  1435. source = node; sourceSide = PortSide.Bottom;
  1436. target = other; targetSide = PortSide.Top;
  1437. }
  1438. // 3. 本节点在 other 正右方 →(other.Right → node.Left)
  1439. else if (VerticalOverlap(pos, oPos) && HorizontalGap(pos.Left, oPos.Right) <= threshold)
  1440. {
  1441. source = other; sourceSide = PortSide.Right;
  1442. target = node; targetSide = PortSide.Left;
  1443. }
  1444. // 4. 本节点在 other 正左方 → 本节点当头(node.Right → other.Left)
  1445. else if (VerticalOverlap(pos, oPos) && HorizontalGap(oPos.Left, pos.Right) <= threshold)
  1446. {
  1447. source = node; sourceSide = PortSide.Right;
  1448. target = other; targetSide = PortSide.Left;
  1449. }
  1450. if (source == null) continue;
  1451. // 预检:能连才显示预览(复用防重/防环逻辑)
  1452. var connection = new FlowConnection
  1453. {
  1454. SourceNodeId = source.NodeId,
  1455. SourceSide = sourceSide,
  1456. TargetNodeId = target.NodeId,
  1457. TargetSide = targetSide
  1458. };
  1459. if (!_graph.CanConnect(connection))
  1460. continue;
  1461. Point start = GetPortPosition(source, sourceSide);
  1462. Point end = GetPortPosition(target, targetSide);
  1463. if (_previewLine == null)
  1464. {
  1465. _previewLine = new Path
  1466. {
  1467. Stroke = new SolidColorBrush(Color.FromRgb(232, 145, 73)),
  1468. StrokeThickness = 2,
  1469. StrokeDashArray = new DoubleCollection { 4, 3 },
  1470. IsHitTestVisible = false
  1471. };
  1472. Panel.SetZIndex(_previewLine, 10000);
  1473. Children.Add(_previewLine);
  1474. }
  1475. _previewLine.Data = CreateConnectionGeometry(start, end, sourceSide, targetSide);
  1476. return;
  1477. }
  1478. }
  1479. /// <summary>
  1480. /// 清除自动拉线虚线预览
  1481. /// </summary>
  1482. public void ClearPreviewLine()
  1483. {
  1484. if (_previewLine != null)
  1485. {
  1486. Children.Remove(_previewLine);
  1487. _previewLine = null;
  1488. }
  1489. }
  1490. /// <summary>
  1491. /// 构造连线并交给 Graph 添加(复用已有防重/防环逻辑)
  1492. /// </summary>
  1493. private bool TryAutoAdd(FlowNode source, PortSide sourceSide, FlowNode target, PortSide targetSide)
  1494. {
  1495. var connection = new FlowConnection
  1496. {
  1497. SourceNodeId = source.NodeId,
  1498. SourceSide = sourceSide,
  1499. TargetNodeId = target.NodeId,
  1500. TargetSide = targetSide
  1501. };
  1502. // 预检:能连才添加(复用防重/防环逻辑)
  1503. if (!_graph.CanConnect(connection))
  1504. return false;
  1505. return _graph.TryAddConnection(connection);
  1506. }
  1507. private Rect GetNodeVisualBounds(FlowNode node)
  1508. {
  1509. double left = node.X;
  1510. double top = node.Y;
  1511. double w = NodeControl.DefaultWidth;
  1512. double h = NodeControl.DefaultHeight;
  1513. return new Rect(left, top, w, h);
  1514. }
  1515. private static bool HorizontalOverlap(Rect a, Rect b)
  1516. {
  1517. double overlap = Math.Min(a.Right, b.Right) - Math.Max(a.Left, b.Left);
  1518. double minW = Math.Min(a.Width, b.Width);
  1519. return minW > 0 && overlap >= minW * 0.8;
  1520. }
  1521. private static bool VerticalOverlap(Rect a, Rect b)
  1522. {
  1523. double overlap = Math.Min(a.Bottom, b.Bottom) - Math.Max(a.Top, b.Top);
  1524. double minH = Math.Min(a.Height, b.Height);
  1525. return minH > 0 && overlap >= minH * 0.8;
  1526. }
  1527. private static double VerticalGap(double aBottom, double bTop)
  1528. {
  1529. return Math.Abs(bTop - aBottom);
  1530. }
  1531. private static double HorizontalGap(double aRight, double bLeft)
  1532. {
  1533. return Math.Abs(bLeft - aRight);
  1534. }
  1535. #endregion
  1536. #region 鸟瞰图支持
  1537. /// <summary>
  1538. /// 动态更新画布大小,使内容超出时自动扩展
  1539. /// </summary>
  1540. public void UpdateCanvasSize()
  1541. {
  1542. if (_graph?.Nodes == null || _graph.Nodes.Count == 0) return;
  1543. var bounds = GetContentBounds();
  1544. double minW = ActualWidth > 0 ? ActualWidth : 800;
  1545. double minH = ActualHeight > 0 ? ActualHeight : 600;
  1546. double newW = Math.Max(bounds.Right + 300, minW);
  1547. double newH = Math.Max(bounds.Bottom + 300, minH);
  1548. if (Math.Abs(Width - newW) > 1 || Math.Abs(Height - newH) > 1)
  1549. {
  1550. Width = newW;
  1551. Height = newH;
  1552. }
  1553. }
  1554. /// <summary>
  1555. /// 适应内容 - 计算所有节点边界,缩放平移使内容完整显示并居中
  1556. /// </summary>
  1557. public void FitToContent()
  1558. {
  1559. var bounds = GetContentBounds();
  1560. var vp = GetViewportSize();
  1561. if (bounds.Width <= 0 || bounds.Height <= 0 || vp.Width <= 0 || vp.Height <= 0)
  1562. {
  1563. // 无内容或视口异常:复位 100% 原点
  1564. _scale.ScaleX = 1.0;
  1565. _scale.ScaleY = 1.0;
  1566. _translate.X = 0;
  1567. _translate.Y = 0;
  1568. ZoomChanged?.Invoke(1.0);
  1569. return;
  1570. }
  1571. const double padding = 50; // 四周留白
  1572. double scaleX = (vp.Width - padding * 2) / bounds.Width;
  1573. double scaleY = (vp.Height - padding * 2) / bounds.Height;
  1574. double newScale = Math.Min(scaleX, scaleY);
  1575. newScale = Math.Max(MinZoom, Math.Min(1.0, newScale)); // 内容小时不放大超过100%
  1576. _scale.ScaleX = newScale;
  1577. _scale.ScaleY = newScale;
  1578. // 居中:内容中心对齐视口中心,但 0,0 原点固定在左上角(不跑到画布外)
  1579. double centerX = bounds.X + bounds.Width / 2;
  1580. double centerY = bounds.Y + bounds.Height / 2;
  1581. _translate.X = Math.Min(0, vp.Width / 2 - centerX * newScale);
  1582. _translate.Y = Math.Min(0, vp.Height / 2 - centerY * newScale);
  1583. ZoomChanged?.Invoke(newScale);
  1584. }
  1585. /// <summary>
  1586. /// 获取所有节点的内容边界
  1587. /// </summary>
  1588. public Rect GetContentBounds()
  1589. {
  1590. if (_graph?.Nodes == null || _graph.Nodes.Count == 0)
  1591. return new Rect(0, 0, 0, 0);
  1592. double minX = double.MaxValue, minY = double.MaxValue;
  1593. double maxX = double.MinValue, maxY = double.MinValue;
  1594. foreach (var node in _graph.Nodes)
  1595. {
  1596. minX = Math.Min(minX, node.X);
  1597. minY = Math.Min(minY, node.Y);
  1598. maxX = Math.Max(maxX, node.X + node.NodeWidth);
  1599. maxY = Math.Max(maxY, node.Y + node.NodeHeight);
  1600. }
  1601. return new Rect(minX, minY, maxX - minX, maxY - minY);
  1602. }
  1603. /// <summary>
  1604. /// 获取当前视口(画布坐标空间中的可见区域)
  1605. /// </summary>
  1606. public Rect GetViewport()
  1607. {
  1608. var vp = GetViewportSize();
  1609. double x = -_translate.X / _scale.ScaleX;
  1610. double y = -_translate.Y / _scale.ScaleY;
  1611. double w = vp.Width / _scale.ScaleX;
  1612. double h = vp.Height / _scale.ScaleY;
  1613. return new Rect(x, y, w, h);
  1614. }
  1615. /// <summary>
  1616. /// 获取可视区域大小(屏幕像素)- 取父容器的实际尺寸
  1617. /// </summary>
  1618. private Size GetViewportSize()
  1619. {
  1620. var parent = System.Windows.Media.VisualTreeHelper.GetParent(this) as FrameworkElement;
  1621. if (parent != null && parent.ActualWidth > 0 && parent.ActualHeight > 0)
  1622. return new Size(parent.ActualWidth, parent.ActualHeight);
  1623. return new Size(800, 600);
  1624. }
  1625. /// <summary>
  1626. /// 将画布视口居中到指定画布坐标点
  1627. /// </summary>
  1628. public void CenterOn(Point canvasPoint)
  1629. {
  1630. var vp = GetViewportSize();
  1631. _translate.X = vp.Width / 2 - canvasPoint.X * _scale.ScaleX;
  1632. _translate.Y = vp.Height / 2 - canvasPoint.Y * _scale.ScaleY;
  1633. ClampTranslate();
  1634. }
  1635. #endregion
  1636. #region 自动排版
  1637. /// <summary>
  1638. /// 层次化自动排版:从左到右分层,同层从上到下排列,最小化连线交叉。
  1639. /// 算法:拓扑排序分层 → 重心启发式排序 → 居中对齐 → 连线方向归一化
  1640. /// </summary>
  1641. public void AutoLayout()
  1642. {
  1643. if (_graph == null || _graph.Nodes.Count == 0) return;
  1644. if (IsRunning || IsReadOnly) return;
  1645. var nodes = _graph.Nodes.ToList();
  1646. var conns = _graph.Connections.ToList();
  1647. var nodeIds = nodes.Select(n => n.NodeId).ToHashSet();
  1648. // 1. 构建邻接表
  1649. var succ = new Dictionary<string, List<string>>();
  1650. var pred = new Dictionary<string, List<string>>();
  1651. foreach (var n in nodes)
  1652. {
  1653. succ[n.NodeId] = new List<string>();
  1654. pred[n.NodeId] = new List<string>();
  1655. }
  1656. foreach (var c in conns)
  1657. {
  1658. if (nodeIds.Contains(c.SourceNodeId) && nodeIds.Contains(c.TargetNodeId))
  1659. {
  1660. succ[c.SourceNodeId].Add(c.TargetNodeId);
  1661. pred[c.TargetNodeId].Add(c.SourceNodeId);
  1662. }
  1663. }
  1664. // 2. 层次分配(最长路径:node.layer = max(前驱 layer) + 1)
  1665. var layer = new Dictionary<string, int>();
  1666. var inDeg = nodes.ToDictionary(n => n.NodeId, n => pred[n.NodeId].Count);
  1667. var queue = new Queue<string>();
  1668. foreach (var n in nodes)
  1669. if (inDeg[n.NodeId] == 0) queue.Enqueue(n.NodeId);
  1670. while (queue.Count > 0)
  1671. {
  1672. var id = queue.Dequeue();
  1673. int maxPred = -1;
  1674. foreach (var p in pred[id])
  1675. if (layer.TryGetValue(p, out var pl)) maxPred = Math.Max(maxPred, pl);
  1676. layer[id] = maxPred + 1;
  1677. foreach (var s in succ[id])
  1678. if (--inDeg[s] == 0) queue.Enqueue(s);
  1679. }
  1680. // 未分配(孤立节点)放第 0 层
  1681. foreach (var n in nodes)
  1682. if (!layer.ContainsKey(n.NodeId)) layer[n.NodeId] = 0;
  1683. // 3. 按层分组
  1684. int maxLayer = layer.Values.Max();
  1685. var layers = new List<List<string>>();
  1686. for (int i = 0; i <= maxLayer; i++) layers.Add(new List<string>());
  1687. foreach (var kv in layer) layers[kv.Value].Add(kv.Key);
  1688. // 4. 层内排序:重心启发式(按前驱 Y 中位数排序,减少交叉)
  1689. const double layerGap = 300; // 层间距(水平)
  1690. const double nodeGap = 50; // 同层节点间距(垂直)
  1691. const double startX = 40;
  1692. const double startY = 40;
  1693. var yPos = new Dictionary<string, double>();
  1694. for (int li = 0; li < layers.Count; li++)
  1695. {
  1696. var layerNodes = layers[li];
  1697. if (li > 0 && layerNodes.Count > 1)
  1698. {
  1699. layerNodes.Sort((a, b) =>
  1700. {
  1701. double ma = MedianY(pred[a], yPos);
  1702. double mb = MedianY(pred[b], yPos);
  1703. return ma.CompareTo(mb);
  1704. });
  1705. }
  1706. double y = startY;
  1707. foreach (var id in layerNodes)
  1708. {
  1709. var node = _graph.GetNode(id);
  1710. if (node == null) continue;
  1711. node.X = startX + li * layerGap;
  1712. node.Y = y;
  1713. yPos[id] = y;
  1714. y += node.NodeHeight + nodeGap;
  1715. }
  1716. }
  1717. // 5. 各层垂直居中
  1718. double maxLayerH = 0;
  1719. var layerHeights = new double[layers.Count];
  1720. for (int li = 0; li < layers.Count; li++)
  1721. {
  1722. var ln = layers[li];
  1723. if (ln.Count == 0) continue;
  1724. var first = _graph.GetNode(ln[0]);
  1725. var last = _graph.GetNode(ln[ln.Count - 1]);
  1726. if (first == null || last == null) continue;
  1727. double h = (last.Y + last.NodeHeight) - first.Y;
  1728. layerHeights[li] = h;
  1729. maxLayerH = Math.Max(maxLayerH, h);
  1730. }
  1731. for (int li = 0; li < layers.Count; li++)
  1732. {
  1733. double offset = (maxLayerH - layerHeights[li]) / 2;
  1734. if (offset <= 0) continue;
  1735. foreach (var id in layers[li])
  1736. {
  1737. var node = _graph.GetNode(id);
  1738. if (node != null) node.Y += offset;
  1739. }
  1740. }
  1741. // 6. 连线方向归一化:跨层 Right→Left,同层 Bottom→Top
  1742. foreach (var c in conns)
  1743. {
  1744. var src = _graph.GetNode(c.SourceNodeId);
  1745. var tgt = _graph.GetNode(c.TargetNodeId);
  1746. if (src == null || tgt == null) continue;
  1747. if (layer.TryGetValue(c.SourceNodeId, out var sl) &&
  1748. layer.TryGetValue(c.TargetNodeId, out var tl) && sl == tl)
  1749. {
  1750. c.SourceSide = PortSide.Bottom;
  1751. c.TargetSide = PortSide.Top;
  1752. }
  1753. else
  1754. {
  1755. c.SourceSide = PortSide.Right;
  1756. c.TargetSide = PortSide.Left;
  1757. }
  1758. }
  1759. // 7. 刷新视觉
  1760. foreach (var node in nodes)
  1761. {
  1762. if (_nodeControls.TryGetValue(node.NodeId, out var ctrl))
  1763. {
  1764. SetLeft(ctrl, node.X);
  1765. SetTop(ctrl, node.Y);
  1766. }
  1767. }
  1768. foreach (var kvp in _connectionPaths)
  1769. if (kvp.Value.Tag is FlowConnection fc) UpdateConnectionPath(kvp.Value, fc);
  1770. // 8. 适应内容
  1771. UpdateCanvasSize();
  1772. FitToContent();
  1773. }
  1774. private static double MedianY(List<string> predIds, Dictionary<string, double> yPos)
  1775. {
  1776. var ys = predIds.Where(yPos.ContainsKey).Select(id => yPos[id]).OrderBy(v => v).ToList();
  1777. if (ys.Count == 0) return 0;
  1778. int mid = ys.Count / 2;
  1779. return ys.Count % 2 == 0 ? (ys[mid - 1] + ys[mid]) / 2 : ys[mid];
  1780. }
  1781. #endregion
  1782. #region 平移限制
  1783. /// <summary>
  1784. /// 限制平移范围,确保不会看到内容区域之外
  1785. /// </summary>
  1786. private void ClampTranslate()
  1787. {
  1788. // 原点固定,不超出画布边界
  1789. var vp = GetViewportSize();
  1790. // 用画布尺寸判断平移范围,而非节点范围
  1791. double canvasW = (Width > 0 ? Width : 5000) * _scale.ScaleX;
  1792. double canvasH = (Height > 0 ? Height : 3500) * _scale.ScaleY;
  1793. // X 轴:原点不动,不超出右下边界
  1794. if (canvasW <= vp.Width)
  1795. _translate.X = 0;
  1796. else
  1797. {
  1798. double minX = vp.Width - canvasW;
  1799. if (_translate.X < minX) _translate.X = minX;
  1800. if (_translate.X > 0) _translate.X = 0;
  1801. }
  1802. // Y 轴
  1803. if (canvasH <= vp.Height)
  1804. _translate.Y = 0;
  1805. else
  1806. {
  1807. double minY = vp.Height - canvasH;
  1808. if (_translate.Y < minY) _translate.Y = minY;
  1809. if (_translate.Y > 0) _translate.Y = 0;
  1810. }
  1811. }
  1812. #endregion
  1813. #region 对齐引导线
  1814. private readonly List<System.Windows.Documents.Adorner> _alignLines = new List<System.Windows.Documents.Adorner>();
  1815. public void AddAlignLine(Point start, Point end)
  1816. {
  1817. var layer = System.Windows.Documents.AdornerLayer.GetAdornerLayer(this);
  1818. if (layer == null) return;
  1819. // Adorner 坐标空间与画布布局坐标空间一致,直接传画布坐标
  1820. var line = new SelectionAlignLine(this, start, end);
  1821. layer.Add(line);
  1822. _alignLines.Add(line);
  1823. }
  1824. public void ClearAlignLines()
  1825. {
  1826. var layer = System.Windows.Documents.AdornerLayer.GetAdornerLayer(this);
  1827. if (layer != null)
  1828. {
  1829. foreach (var line in _alignLines)
  1830. layer.Remove(line);
  1831. }
  1832. _alignLines.Clear();
  1833. }
  1834. /// <summary>
  1835. /// 检查节点对齐并返回对齐偏移量
  1836. /// </summary>
  1837. public void CheckAlignment(FlowNode draggingNode, ref double x, ref double y)
  1838. {
  1839. ClearAlignLines();
  1840. const double threshold = 5.0;
  1841. double nodeLeft = x;
  1842. double nodeTop = y;
  1843. double nodeRight = x + draggingNode.NodeWidth;
  1844. double nodeBottom = y + draggingNode.NodeHeight;
  1845. double nodeCenterX = x + draggingNode.NodeWidth / 2;
  1846. double nodeCenterY = y + draggingNode.NodeHeight / 2;
  1847. double snapX = double.NaN;
  1848. double snapY = double.NaN;
  1849. if (_graph?.Nodes == null) return;
  1850. foreach (var other in _graph.Nodes)
  1851. {
  1852. if (other.NodeId == draggingNode.NodeId) continue;
  1853. double otherLeft = other.X;
  1854. double otherTop = other.Y;
  1855. double otherRight = other.X + other.NodeWidth;
  1856. double otherBottom = other.Y + other.NodeHeight;
  1857. double otherCenterX = other.X + other.NodeWidth / 2;
  1858. double otherCenterY = other.Y + other.NodeHeight / 2;
  1859. // 垂直对齐检测(显示垂直引导线,调整 X)
  1860. if (!double.IsNaN(snapX))
  1861. {
  1862. // 已有 X 对齐,只检查 Y
  1863. }
  1864. else if (Math.Abs(nodeLeft - otherLeft) < threshold)
  1865. {
  1866. snapX = otherLeft;
  1867. AddAlignLine(new Point(otherLeft, Math.Min(nodeTop, otherTop)),
  1868. new Point(otherLeft, Math.Max(nodeBottom, otherBottom)));
  1869. }
  1870. else if (Math.Abs(nodeRight - otherRight) < threshold)
  1871. {
  1872. snapX = otherRight - draggingNode.NodeWidth;
  1873. AddAlignLine(new Point(otherRight, Math.Min(nodeTop, otherTop)),
  1874. new Point(otherRight, Math.Max(nodeBottom, otherBottom)));
  1875. }
  1876. else if (Math.Abs(nodeCenterX - otherCenterX) < threshold)
  1877. {
  1878. snapX = otherCenterX - draggingNode.NodeWidth / 2;
  1879. AddAlignLine(new Point(otherCenterX, Math.Min(nodeTop, otherTop)),
  1880. new Point(otherCenterX, Math.Max(nodeBottom, otherBottom)));
  1881. }
  1882. // 水平对齐检测(显示水平引导线,调整 Y)
  1883. if (!double.IsNaN(snapY))
  1884. {
  1885. // 已有 Y 对齐
  1886. }
  1887. else if (Math.Abs(nodeTop - otherTop) < threshold)
  1888. {
  1889. snapY = otherTop;
  1890. AddAlignLine(new Point(Math.Min(nodeLeft, otherLeft), otherTop),
  1891. new Point(Math.Max(nodeRight, otherRight), otherTop));
  1892. }
  1893. else if (Math.Abs(nodeBottom - otherBottom) < threshold)
  1894. {
  1895. snapY = otherBottom - draggingNode.NodeHeight;
  1896. AddAlignLine(new Point(Math.Min(nodeLeft, otherLeft), otherBottom),
  1897. new Point(Math.Max(nodeRight, otherRight), otherBottom));
  1898. }
  1899. else if (Math.Abs(nodeCenterY - otherCenterY) < threshold)
  1900. {
  1901. snapY = otherCenterY - draggingNode.NodeHeight / 2;
  1902. AddAlignLine(new Point(Math.Min(nodeLeft, otherLeft), otherCenterY),
  1903. new Point(Math.Max(nodeRight, otherRight), otherCenterY));
  1904. }
  1905. }
  1906. if (!double.IsNaN(snapX)) x = snapX;
  1907. if (!double.IsNaN(snapY)) y = snapY;
  1908. }
  1909. #endregion
  1910. }
  1911. }