| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185 |
- using System;
- using System.Collections.Generic;
- using System.Collections.Specialized;
- using System.Linq;
- using System.Windows;
- using System.Windows.Controls;
- using System.Windows.Input;
- using System.Windows.Media;
- using System.Windows.Shapes;
- using System.Xml.Linq;
- using TeamAAS.Dialogs;
- using TeamAAS.FlowEditor.Models;
- using TeamAAS.FlowEditor.Plugins;
- using TeamAAS.FlowEngine;
- namespace TeamAAS.FlowEditor.Controls
- {
- /// <summary>
- /// 流程编辑器画布 - 支持缩放、平移、拖拽节点、绘制连线、框选
- /// </summary>
- public class FlowCanvas : Canvas
- {
- #region 拖放坐标(高DPI安全)
- [System.Runtime.InteropServices.DllImport("user32.dll")]
- private static extern bool GetCursorPos(out POINT lpPoint);
- [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
- private struct POINT { public int X; public int Y; }
- /// <summary>
- /// 用系统光标位置反推画布本地坐标。
- /// 不能用拖放事件的 e.GetPosition:PerMonitorV2 + 高 DPI 下 WPF 的拖放事件坐标
- /// 换算因子错误(已知问题),节点会落在光标旁边。
- /// 注意:PointFromScreen 的结果就是正确的 WPF 坐标,不要再乘 TransformToDevice——
- /// 进程按系统 DPI 虚拟化运行时,乘上去会造成预览超速偏移。
- /// </summary>
- public Point GetCursorCanvasPosition()
- {
- GetCursorPos(out POINT pt);
- var parent = (Visual)VisualTreeHelper.GetParent(this);
- var parentPt = parent.PointFromScreen(new Point(pt.X, pt.Y));
- // 逆画布 RenderTransform(缩放 + 平移)→ 画布本地坐标
- var t = RenderTransform;
- return t != null ? t.Inverse.Transform(parentPt) : parentPt;
- }
- #endregion
- #region 常量
- private const double MinZoom = 0.2;
- private const double MaxZoom = 3.0;
- private const double ZoomFactor = 1.15;
- #endregion
- #region 字段
- private readonly ScaleTransform _scale;
- private readonly TranslateTransform _translate;
- private FlowGraph _graph;
- private readonly Dictionary<string, NodeControl> _nodeControls = new Dictionary<string, NodeControl>();
- private readonly Dictionary<string, Path> _connectionPaths = new Dictionary<string, Path>();
- // 自动拉线预览(虚线)
- private Path _previewLine;
- // 平移状态
- private bool _isPanning;
- private Point _panStart;
- private Point _panOrigin;
- // 连线绘制状态
- private bool _isConnecting;
- private FlowNode _connectSourceNode;
- private PortSide _connectSourceSide;
- private Path _tempPath;
- // 框选状态
- private bool _isBoxSelecting;
- private Point _boxSelectStart;
- private Rectangle _selectionBox;
- // 选中项
- private FlowNode _selectedNode;
- private FlowConnection _selectedConnection;
- private readonly List<FlowNode> _selectedNodes = new List<FlowNode>();
- private const string ClipboardPrefix = "TeamAASFlow:";
- #endregion
- #region 事件
- public event Action<FlowNode> NodeDoubleClicked;
- public event Action<FlowNode> NodeSelected;
- public event Action<FlowConnection> ConnectionSelected;
- public event Action SelectionCleared;
- public event Action<double, double, NodePluginInfo> NodeDropRequested;
- public event Action<double> ZoomChanged;
- public event Action<FlowNode> NodeRunRequested;
- public event Action<FlowNode> NodeRunFromHereRequested;
- public event Action<FlowNode> NodePropertiesRequested;
- #endregion
- #region 属性
- public double Zoom => _scale.ScaleX;
- public double TranslateX => _translate.X;
- public double TranslateY => _translate.Y;
- public void SetTranslateX(double x) { _translate.X = x; }
- public void SetTranslateY(double y) { _translate.Y = y; }
- public Size ViewportSize => GetViewportSize();
- public FlowGraph Graph
- {
- get => _graph;
- set
- {
- if (_graph != null) UnsubscribeGraph();
- ClearCanvas();
- _graph = value;
- if (_graph != null) SubscribeGraph();
- }
- }
- /// <summary>流程运行中:锁定节点移动和双击编辑</summary>
- public static readonly DependencyProperty IsRunningProperty =
- DependencyProperty.Register(nameof(IsRunning), typeof(bool), typeof(FlowCanvas), new PropertyMetadata(false));
- public bool IsRunning
- {
- get => (bool)GetValue(IsRunningProperty);
- set => SetValue(IsRunningProperty, value);
- }
- /// <summary>只读模式(运行产品监控中):允许选中节点查看结果,禁止一切修改操作</summary>
- public static readonly DependencyProperty IsReadOnlyProperty =
- DependencyProperty.Register(nameof(IsReadOnly), typeof(bool), typeof(FlowCanvas), new PropertyMetadata(false));
- public bool IsReadOnly
- {
- get => (bool)GetValue(IsReadOnlyProperty);
- set => SetValue(IsReadOnlyProperty, value);
- }
- #endregion
- #region 构造函数
- public FlowCanvas()
- {
- _scale = new ScaleTransform(1, 1);
- _translate = new TranslateTransform(0, 0);
- var group = new TransformGroup();
- group.Children.Add(_scale);
- group.Children.Add(_translate);
- RenderTransform = group;
- ClipToBounds = true;
- Focusable = true;
- Background = new SolidColorBrush(Colors.White);
- AllowDrop = true;
- // 初始画布尺寸
- Width = 5000;
- Height = 3500;
- // 初始偏移,显示画布的一部分
- _translate.X = 0;
- _translate.Y = 0;
- }
- #endregion
- #region Graph 订阅
- private void SubscribeGraph()
- {
- _graph.Nodes.CollectionChanged += OnNodesChanged;
- _graph.Connections.CollectionChanged += OnConnectionsChanged;
- foreach (var node in _graph.Nodes)
- AddNodeVisual(node);
- foreach (var conn in _graph.Connections)
- AddConnectionVisual(conn);
- }
- private void UnsubscribeGraph()
- {
- _graph.Nodes.CollectionChanged -= OnNodesChanged;
- _graph.Connections.CollectionChanged -= OnConnectionsChanged;
- }
- private void ClearCanvas()
- {
- foreach (var node in _nodeControls.Values)
- {
- if (node.DataContext is FlowNode fn)
- fn.PropertyChanged -= OnNodePropertyChanged;
- }
- _nodeControls.Clear();
- _connectionPaths.Clear();
- Children.Clear();
- }
- private void OnNodesChanged(object sender, NotifyCollectionChangedEventArgs e)
- {
- // Clear()/整体替换会触发 Reset(NewItems/OldItems 均为 null),
- // 不处理会导致导入流程后旧节点可视化残留成“影子”。
- if (e.Action == NotifyCollectionChangedAction.Reset)
- {
- RebuildVisuals();
- return;
- }
- if (e.NewItems != null)
- foreach (FlowNode node in e.NewItems)
- AddNodeVisual(node);
- if (e.OldItems != null)
- foreach (FlowNode node in e.OldItems)
- RemoveNodeVisual(node);
- }
- private void OnConnectionsChanged(object sender, NotifyCollectionChangedEventArgs e)
- {
- if (e.Action == NotifyCollectionChangedAction.Reset)
- {
- RebuildVisuals();
- return;
- }
- if (e.NewItems != null)
- foreach (FlowConnection conn in e.NewItems)
- AddConnectionVisual(conn);
- if (e.OldItems != null)
- foreach (FlowConnection conn in e.OldItems)
- RemoveConnectionVisual(conn);
- }
- /// <summary>
- /// 集合被整体 Clear(Reset)时重建全部节点/连线可视化,避免旧图形残留(如导入流程后)。
- /// </summary>
- private void RebuildVisuals()
- {
- foreach (var kv in _nodeControls.ToList())
- {
- if (kv.Value.DataContext is FlowNode fn)
- fn.PropertyChanged -= OnNodePropertyChanged;
- Children.Remove(kv.Value);
- }
- _nodeControls.Clear();
- foreach (var kv in _connectionPaths.ToList())
- Children.Remove(kv.Value);
- _connectionPaths.Clear();
- if (_graph == null) return;
- foreach (var node in _graph.Nodes)
- AddNodeVisual(node);
- foreach (var conn in _graph.Connections)
- AddConnectionVisual(conn);
- }
- #endregion
- #region 节点可视化
- private void AddNodeVisual(FlowNode node)
- {
- var ctrl = new NodeControl { DataContext = node };
- SetLeft(ctrl, node.X);
- SetTop(ctrl, node.Y);
- Children.Add(ctrl);
- _nodeControls[node.NodeId] = ctrl;
- // 节点渲染后更新实际尺寸,用于端口位置和碰撞检测
- ctrl.SizeChanged += (s, e) =>
- {
- node.NodeWidth = ctrl.ActualWidth;
- node.NodeHeight = ctrl.ActualHeight;
- UpdateConnectionsForNode(node.NodeId);
- UpdateCanvasSize();
- };
- node.PropertyChanged += OnNodePropertyChanged;
- }
- private void RemoveNodeVisual(FlowNode node)
- {
- if (_nodeControls.TryGetValue(node.NodeId, out var ctrl))
- {
- Children.Remove(ctrl);
- _nodeControls.Remove(node.NodeId);
- node.PropertyChanged -= OnNodePropertyChanged;
- }
- // 移除相关连线可视化
- var toRemove = _connectionPaths
- .Where(kvp => kvp.Value.Tag is FlowConnection fc &&
- (fc.SourceNodeId == node.NodeId || fc.TargetNodeId == node.NodeId))
- .ToList();
- foreach (var kvp in toRemove)
- {
- Children.Remove(kvp.Value);
- _connectionPaths.Remove(kvp.Key);
- }
- if (_selectedNode == node)
- _selectedNode = null;
- _selectedNodes.Remove(node);
- }
- private void OnNodePropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
- {
- var node = (FlowNode)sender;
- if (e.PropertyName == nameof(FlowNode.X) || e.PropertyName == nameof(FlowNode.Y))
- {
- if (_nodeControls.TryGetValue(node.NodeId, out var ctrl))
- {
- SetLeft(ctrl, node.X);
- SetTop(ctrl, node.Y);
- }
- UpdateConnectionsForNode(node.NodeId);
- UpdateCanvasSize();
- }
- else if (e.PropertyName == nameof(FlowNode.IsEnabled))
- {
- if (_nodeControls.TryGetValue(node.NodeId, out var ctrl))
- ctrl.UpdateDisabledVisual();
- }
- else if (e.PropertyName == nameof(FlowNode.Status))
- {
- node.NotifyStatusChanged();
- }
- }
- #endregion
- #region 连线可视化
- private void AddConnectionVisual(FlowConnection conn)
- {
- var path = new Path
- {
- Stroke = new SolidColorBrush(Color.FromRgb(232, 145, 73)),
- StrokeThickness = 2,
- Fill = new SolidColorBrush(Color.FromRgb(232, 145, 73)),
- Tag = conn,
- Cursor = Cursors.Hand
- };
- UpdateConnectionPath(path, conn);
- Children.Insert(0, path);
- _connectionPaths[conn.ConnectionId] = path;
- path.MouseLeftButtonDown += (s, e) =>
- {
- SelectConnection(conn);
- e.Handled = true;
- };
- }
- private void RemoveConnectionVisual(FlowConnection conn)
- {
- if (_connectionPaths.TryGetValue(conn.ConnectionId, out var path))
- {
- Children.Remove(path);
- _connectionPaths.Remove(conn.ConnectionId);
- }
- if (_selectedConnection == conn)
- _selectedConnection = null;
- }
- private void UpdateConnectionPath(Path path, FlowConnection conn)
- {
- var sourceNode = _graph?.GetNode(conn.SourceNodeId);
- var targetNode = _graph?.GetNode(conn.TargetNodeId);
- if (sourceNode == null || targetNode == null) return;
- Point start = GetPortPosition(sourceNode, conn.SourceSide);
- Point end = GetPortPosition(targetNode, conn.TargetSide);
- path.Data = CreateConnectionGeometry(start, end, conn.SourceSide, conn.TargetSide);
- if (conn.IsSelected)
- {
- path.Stroke = Brushes.White;
- path.Fill = Brushes.White;
- path.StrokeThickness = 3;
- }
- else
- {
- // 连接到异常分支节点的线用红色,其余橙色
- bool isException = targetNode.Category == NodeCategory.ExceptionBranch;
- var color = isException
- ? Color.FromRgb(0xB7, 0x1C, 0x1C)
- : Color.FromRgb(232, 145, 73);
- path.Stroke = new SolidColorBrush(color);
- path.Fill = new SolidColorBrush(color);
- path.StrokeThickness = 2;
- }
- }
- private void UpdateConnectionsForNode(string nodeId)
- {
- foreach (var kvp in _connectionPaths)
- {
- if (kvp.Value.Tag is FlowConnection conn)
- {
- if (conn.SourceNodeId == nodeId || conn.TargetNodeId == nodeId)
- UpdateConnectionPath(kvp.Value, conn);
- }
- }
- }
- #endregion
- #region 端口位置 & 连线几何
- public static Point GetPortPosition(FlowNode node, PortDirection direction, int portIndex)
- {
- double w = node.NodeWidth;
- double h = node.NodeHeight;
- if (direction == PortDirection.Input)
- {
- // portIndex 0 = Left, 1 = Top
- if (portIndex == 0) return new Point(node.X, node.Y + h / 2);
- return new Point(node.X + w / 2, node.Y);
- }
- else // Output
- {
- // portIndex 0 = Right, 1 = Bottom
- if (portIndex == 0) return new Point(node.X + w, node.Y + h / 2);
- return new Point(node.X + w / 2, node.Y + h);
- }
- }
- /// <summary>
- /// 根据边获取端口位置(通用,不区分输入输出)
- /// </summary>
- public static Point GetPortPosition(FlowNode node, PortSide side)
- {
- double w = node.NodeWidth;
- double h = node.NodeHeight;
- switch (side)
- {
- case PortSide.Left: return new Point(node.X, node.Y + h / 2);
- case PortSide.Top: return new Point(node.X + w / 2, node.Y);
- case PortSide.Right: return new Point(node.X + w, node.Y + h / 2);
- case PortSide.Bottom: return new Point(node.X + w / 2, node.Y + h);
- default: return new Point(node.X, node.Y + h / 2);
- }
- }
- /// <summary>
- /// 根据端口方向和索引推断所在边
- /// </summary>
- public static PortSide GetPortSide(PortDirection direction, int portIndex)
- {
- if (direction == PortDirection.Input)
- return portIndex == 0 ? PortSide.Left : PortSide.Top;
- return portIndex == 0 ? PortSide.Right : PortSide.Bottom;
- }
- public static Geometry CreateConnectionGeometry(Point start, Point end, PortSide startSide = PortSide.Right, PortSide endSide = PortSide.Left)
- {
- // 偏移起止点,留出间距让箭头可见
- Point adjStart = OffsetPoint(start, startSide, 8);
- Point adjEnd = OffsetPoint(end, endSide, 12);
- var geo = new StreamGeometry();
- using (var ctx = geo.Open())
- {
- // 控制点方向(端口朝外方向)
- Vector startDir = GetSideDirection(startSide);
- Vector endDir = GetSideDirection(endSide);
- // 控制点偏移量:基于距离动态调整,保证曲度自然
- double dist = (adjEnd - adjStart).Length;
- double offset = Math.Max(50, dist * 0.4);
- Point cp1 = adjStart + startDir * offset;
- Point cp2 = adjEnd + endDir * offset;
- // 绘制平滑贝塞尔曲线
- ctx.BeginFigure(adjStart, false, false);
- ctx.BezierTo(cp1, cp2, adjEnd, true, false);
- // 箭头方向 = 贝塞尔终点切线 (end - cp2)
- double angle = Math.Atan2(adjEnd.Y - cp2.Y, adjEnd.X - cp2.X);
- double arrowSize = 7;
- var p1 = new Point(
- adjEnd.X - arrowSize * Math.Cos(angle - Math.PI / 6),
- adjEnd.Y - arrowSize * Math.Sin(angle - Math.PI / 6));
- var p2 = new Point(
- adjEnd.X - arrowSize * Math.Cos(angle + Math.PI / 6),
- adjEnd.Y - arrowSize * Math.Sin(angle + Math.PI / 6));
- ctx.BeginFigure(adjEnd, true, true);
- ctx.LineTo(p1, true, true);
- ctx.LineTo(p2, true, true);
- }
- geo.Freeze();
- return geo;
- }
- /// <summary>
- /// 端口朝外方向单位向量
- /// </summary>
- private static Vector GetSideDirection(PortSide side)
- {
- switch (side)
- {
- case PortSide.Left: return new Vector(-1, 0);
- case PortSide.Right: return new Vector(1, 0);
- case PortSide.Top: return new Vector(0, -1);
- case PortSide.Bottom: return new Vector(0, 1);
- default: return new Vector(1, 0);
- }
- }
- /// <summary>
- /// 根据鼠标位置推断目标端口边
- /// </summary>
- private static PortSide DetermineEndSide(Point start, Point end, PortSide startSide)
- {
- double dx = end.X - start.X;
- double dy = end.Y - start.Y;
- switch (startSide)
- {
- case PortSide.Right:
- return dx >= 0 ? PortSide.Left : PortSide.Right;
- case PortSide.Bottom:
- return dy >= 0 ? PortSide.Top : PortSide.Bottom;
- case PortSide.Left:
- return dx <= 0 ? PortSide.Right : PortSide.Left;
- case PortSide.Top:
- return dy <= 0 ? PortSide.Bottom : PortSide.Top;
- default:
- return PortSide.Left;
- }
- }
- /// <summary>
- /// 沿端口方向偏移点,留出间距
- /// </summary>
- private static Point OffsetPoint(Point p, PortSide side, double offset)
- {
- switch (side)
- {
- case PortSide.Left: return new Point(p.X - offset, p.Y);
- case PortSide.Right: return new Point(p.X + offset, p.Y);
- case PortSide.Top: return new Point(p.X, p.Y - offset);
- case PortSide.Bottom: return new Point(p.X, p.Y + offset);
- default: return p;
- }
- }
- #endregion
- #region 坐标转换
- public Point ToCanvasPoint(Point screenPoint)
- {
- return new Point(
- (screenPoint.X - _translate.X) / _scale.ScaleX,
- (screenPoint.Y - _translate.Y) / _scale.ScaleY);
- }
- #endregion
- #region 缩放 & 平移
- public void ZoomAt(Point center, double newScale)
- {
- newScale = Math.Max(MinZoom, Math.Min(MaxZoom, newScale));
- if (Math.Abs(newScale - _scale.ScaleX) < 0.001) return;
- // center 是 GetPosition(this) 返回的画布本地坐标
- // 保持 center 对应的屏幕点不变:screen = center * oldScale + oldTranslate = center * newScale + newTranslate
- _translate.X = _translate.X + center.X * (_scale.ScaleX - newScale);
- _translate.Y = _translate.Y + center.Y * (_scale.ScaleY - newScale);
- _scale.ScaleX = newScale;
- _scale.ScaleY = newScale;
- ClampTranslate();
- ZoomChanged?.Invoke(newScale);
- }
- protected override void OnMouseWheel(MouseWheelEventArgs e)
- {
- if ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
- {
- // Ctrl + 滚轮 = 缩放
- var mousePos = e.GetPosition(this);
- double factor = e.Delta > 0 ? ZoomFactor : 1 / ZoomFactor;
- ZoomAt(mousePos, _scale.ScaleX * factor);
- e.Handled = true;
- }
- else if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift)
- {
- // Shift + 滚轮 = 水平平移
- _translate.X += e.Delta * 0.5;
- ClampTranslate();
- e.Handled = true;
- }
- else
- {
- // 滚轮 = 上下平移
- _translate.Y += e.Delta * 0.5;
- ClampTranslate();
- e.Handled = true;
- }
- }
- protected override void OnMouseDown(MouseButtonEventArgs e)
- {
- base.OnMouseDown(e);
- if (e.ChangedButton == MouseButton.Middle)
- {
- _isPanning = true;
- _panStart = e.GetPosition(Parent as IInputElement);
- _panOrigin = new Point(_translate.X, _translate.Y);
- CaptureMouse();
- e.Handled = true;
- }
- }
- protected override void OnMouseMove(MouseEventArgs e)
- {
- base.OnMouseMove(e);
- var pos = e.GetPosition(this);
- if (_isPanning)
- {
- var panPos = e.GetPosition(Parent as IInputElement);
- _translate.X = _panOrigin.X + (panPos.X - _panStart.X);
- _translate.Y = _panOrigin.Y + (panPos.Y - _panStart.Y);
- ClampTranslate();
- }
- else if (_isConnecting && _tempPath != null)
- {
- Point start = GetPortPosition(_connectSourceNode, _connectSourceSide);
- PortSide endSide = DetermineEndSide(start, pos, _connectSourceSide);
- _tempPath.Data = CreateConnectionGeometry(start, pos, _connectSourceSide, endSide);
- }
- else if (_isBoxSelecting)
- {
- UpdateSelectionBox(pos);
- }
- }
- protected override void OnMouseUp(MouseButtonEventArgs e)
- {
- base.OnMouseUp(e);
- if (e.ChangedButton == MouseButton.Middle)
- {
- _isPanning = false;
- ReleaseMouseCapture();
- e.Handled = true;
- }
- }
- #endregion
- #region 左键交互
- // 双击检测 + 单击选中都用 Preview(隧道事件),在 MoveThumb 捕获鼠标之前触发
- protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
- {
- if (e.ClickCount == 2)
- {
- var dblNode = FindAncestor<NodeControl>(e.OriginalSource as DependencyObject);
- if (dblNode != null)
- {
- // 流程运行中 / 只读监控模式禁止双击打开编辑器
- if (IsRunning || IsReadOnly) { e.Handled = true; return; }
- NodeDoubleClicked?.Invoke(dblNode.GetNode());
- e.Handled = true;
- return;
- }
- }
- else if (e.ClickCount == 1)
- {
- // 单击节点选中(在 MoveThumb 捕获鼠标之前处理)
- var nodeCtrl = FindAncestor<NodeControl>(e.OriginalSource as DependencyObject);
- if (nodeCtrl != null)
- {
- Focus();
- bool isCtrl = (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control;
- SelectNode(nodeCtrl.GetNode(), isCtrl);
- // 不设 Handled,让 MoveThumb 继续处理拖拽
- }
- }
- base.OnPreviewMouseLeftButtonDown(e);
- }
- protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
- {
- base.OnMouseLeftButtonDown(e);
- Focus();
- var pos = e.GetPosition(this);
- var src = e.OriginalSource as DependencyObject;
- // 1. 检查连接点
- var connector = FindAncestor<ConnectorControl>(src);
- if (connector != null)
- {
- StartConnection(connector);
- e.Handled = true;
- return;
- }
- // 2. 检查连线(Path with FlowConnection Tag)
- if (src is Path path && path.Tag is FlowConnection conn)
- {
- SelectConnection(conn);
- e.Handled = true;
- return;
- }
- // 3. 检查节点
- var nodeCtrl = FindAncestor<NodeControl>(src);
- if (nodeCtrl != null)
- {
- bool isCtrl = (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control;
- SelectNode(nodeCtrl.GetNode(), isCtrl);
- return; // 不设 Handled,让 MoveThumb 处理拖拽
- }
- // 4. 空白区域 - 框选
- ClearSelection();
- StartBoxSelect(pos);
- e.Handled = true;
- }
- protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
- {
- base.OnMouseLeftButtonUp(e);
- if (_isConnecting)
- EndConnection(e.GetPosition(this));
- if (_isBoxSelecting)
- EndBoxSelect();
- }
- #endregion
- #region 右键菜单
- protected override void OnMouseRightButtonDown(MouseButtonEventArgs e)
- {
- base.OnMouseRightButtonDown(e);
- Focus();
- var pos = e.GetPosition(this);
- var src = e.OriginalSource as DependencyObject;
- var nodeCtrl = FindAncestor<NodeControl>(src);
- if (nodeCtrl != null)
- {
- var node = nodeCtrl.GetNode();
- // 如果右键的节点不在已选列表中,单选它
- if (!_selectedNodes.Contains(node) && _selectedNode != node)
- SelectNode(node, false);
- else if (_selectedNode == null && _selectedNodes.Count == 0)
- SelectNode(node, false);
- ShowNodeContextMenu(node);
- e.Handled = true;
- }
- else
- {
- ShowCanvasContextMenu(pos);
- e.Handled = true;
- }
- }
- private void ShowNodeContextMenu(FlowNode node)
- {
- var menu = new ContextMenu();
- bool readOnly = IsReadOnly || IsRunning;
- // 复制
- var miCopy = new MenuItem { Header = "复制", IsEnabled = !readOnly };
- miCopy.Click += (s, e) => CopySelectedNodes();
- menu.Items.Add(miCopy);
- // 禁用/启用节点
- var miToggleEnabled = new MenuItem { Header = node.IsEnabled ? "禁用节点" : "启用节点", IsEnabled = !readOnly };
- miToggleEnabled.Click += (s, e) => ToggleNodeEnabled(node);
- menu.Items.Add(miToggleEnabled);
- menu.Items.Add(new Separator());
- // 删除节点
- var miDelNode = new MenuItem { Header = "删除节点", IsEnabled = !readOnly };
- miDelNode.Click += (s, e) => DeleteSelectedNodes();
- menu.Items.Add(miDelNode);
- // 删除连接线
- var miDelConn = new MenuItem { Header = "删除连接线", IsEnabled = !readOnly };
- miDelConn.Click += (s, e) => DeleteSelectedConnections();
- menu.Items.Add(miDelConn);
- // 运行和从此处运行:仅单选时显示(只读模式下不允许从编辑器发起运行)
- bool isSingle = _selectedNodes.Count <= 1 && _selectedNode != null;
- if (isSingle)
- {
- menu.Items.Add(new Separator());
- var miRun = new MenuItem { Header = "运行", IsEnabled = !readOnly };
- miRun.Click += (s, e) => NodeRunRequested?.Invoke(node);
- menu.Items.Add(miRun);
- var miRunFrom = new MenuItem { Header = "此处开始运行", IsEnabled = !readOnly };
- miRunFrom.Click += (s, e) => NodeRunFromHereRequested?.Invoke(node);
- menu.Items.Add(miRunFrom);
- menu.Items.Add(new Separator());
- var miProp = new MenuItem { Header = "属性", IsEnabled = !readOnly };
- miProp.Click += (s, e) => NodePropertiesRequested?.Invoke(node);
- menu.Items.Add(miProp);
- }
- menu.IsOpen = true;
- }
- /// <summary>
- /// 切换节点启用状态(禁用后不执行、背景变灰)
- /// </summary>
- private void ToggleNodeEnabled(FlowNode node)
- {
- if (node == null) return;
- node.IsEnabled = !node.IsEnabled;
- }
- private void ShowCanvasContextMenu(Point pos)
- {
- var menu = new ContextMenu();
- var miLayout = new MenuItem { Header = "自动排版" };
- miLayout.Click += (s, e) => AutoLayout();
- menu.Items.Add(miLayout);
- menu.Items.Add(new Separator());
- var miPaste = new MenuItem { Header = "粘贴" };
- miPaste.Click += (s, e) => PasteNodesAt(pos);
- miPaste.IsEnabled = !IsReadOnly && !IsRunning && HasClipboardData();
- menu.Items.Add(miPaste);
- menu.IsOpen = true;
- }
- #endregion
- #region 连线绘制
- private void StartConnection(ConnectorControl connector)
- {
- if (IsRunning || IsReadOnly) return; // 流程运行中 / 只读模式禁止新建连线
- var node = connector.GetNode();
- if (node == null) return;
- _isConnecting = true;
- _connectSourceNode = node;
- _connectSourceSide = connector.Side;
- _tempPath = new Path
- {
- Stroke = new SolidColorBrush(Color.FromRgb(232, 145, 73)),
- StrokeThickness = 2,
- StrokeDashArray = new DoubleCollection { 4, 2 },
- IsHitTestVisible = false
- };
- Children.Add(_tempPath);
- CaptureMouse();
- }
- private void EndConnection(Point mousePos)
- {
- _isConnecting = false;
- ReleaseMouseCapture();
- if (_tempPath != null)
- {
- Children.Remove(_tempPath);
- _tempPath = null;
- }
- if (_connectSourceNode == null || _graph == null) return;
- // 手动命中检测:遍历所有节点的4个端口,找到最近的
- const double hitRadius = 22.0;
- double minDist = double.MaxValue;
- FlowNode targetNode = null;
- PortSide targetSide = PortSide.Left;
- var sides = new[] { PortSide.Left, PortSide.Top, PortSide.Right, PortSide.Bottom };
- foreach (var node in _graph.Nodes)
- {
- if (node.NodeId == _connectSourceNode.NodeId) continue;
- foreach (var side in sides)
- {
- var pos = GetPortPosition(node, side);
- double d = PointDist(pos, mousePos);
- if (d < hitRadius && d < minDist)
- {
- minDist = d;
- targetNode = node;
- targetSide = side;
- }
- }
- }
- if (targetNode == null) { _connectSourceNode = null; return; }
- // 源 = 拖出端,目标 = 放入端。不检查方向类型。
- var conn = new FlowConnection
- {
- SourceNodeId = _connectSourceNode.NodeId,
- SourceSide = _connectSourceSide,
- TargetNodeId = targetNode.NodeId,
- TargetSide = targetSide
- };
- _graph?.TryAddConnection(conn);
- _connectSourceNode = null;
- }
- private static double PointDist(Point a, Point b)
- {
- double dx = a.X - b.X;
- double dy = a.Y - b.Y;
- return Math.Sqrt(dx * dx + dy * dy);
- }
- #endregion
- #region 框选
- private void StartBoxSelect(Point pos)
- {
- _isBoxSelecting = true;
- _boxSelectStart = pos;
- _selectionBox = new Rectangle
- {
- Stroke = new SolidColorBrush(Color.FromArgb(100, 0, 122, 204)),
- Fill = new SolidColorBrush(Color.FromArgb(30, 0, 122, 204)),
- StrokeThickness = 1,
- IsHitTestVisible = false
- };
- Children.Add(_selectionBox);
- CaptureMouse();
- }
- private void UpdateSelectionBox(Point pos)
- {
- if (_selectionBox == null) return;
- double x = Math.Min(_boxSelectStart.X, pos.X);
- double y = Math.Min(_boxSelectStart.Y, pos.Y);
- double w = Math.Abs(pos.X - _boxSelectStart.X);
- double h = Math.Abs(pos.Y - _boxSelectStart.Y);
- SetLeft(_selectionBox, x);
- SetTop(_selectionBox, y);
- _selectionBox.Width = w;
- _selectionBox.Height = h;
- }
- private void EndBoxSelect()
- {
- _isBoxSelecting = false;
- ReleaseMouseCapture();
- if (_selectionBox != null)
- {
- double x = GetLeft(_selectionBox);
- double y = GetTop(_selectionBox);
- double w = _selectionBox.Width;
- double h = _selectionBox.Height;
- var rect = new Rect(x, y, w, h);
- // 清除旧选择
- foreach (var n in _selectedNodes)
- n.IsSelected = false;
- _selectedNodes.Clear();
- // 多选模式:选中框选范围内的所有节点
- foreach (var node in _graph?.Nodes ?? Enumerable.Empty<FlowNode>())
- {
- var nodeRect = new Rect(node.X, node.Y, node.NodeWidth, node.NodeHeight);
- if (rect.IntersectsWith(nodeRect))
- {
- node.IsSelected = true;
- _selectedNodes.Add(node);
- }
- }
- if (_selectedNodes.Count > 0)
- {
- _selectedNode = _selectedNodes[_selectedNodes.Count - 1];
- NodeSelected?.Invoke(_selectedNode);
- }
- Children.Remove(_selectionBox);
- _selectionBox = null;
- }
- }
- #endregion
- #region 选择
- public void SelectNode(FlowNode node, bool isMultiSelect = false)
- {
- if (isMultiSelect && node != null)
- {
- // Ctrl+Click: 切换该节点的选中状态
- if (_selectedNodes.Contains(node))
- {
- _selectedNodes.Remove(node);
- node.IsSelected = false;
- }
- else
- {
- _selectedNodes.Add(node);
- node.IsSelected = true;
- }
- _selectedNode = node;
- if (_selectedConnection != null)
- {
- _selectedConnection.IsSelected = false;
- if (_connectionPaths.TryGetValue(_selectedConnection.ConnectionId, out var p))
- UpdateConnectionPath(p, _selectedConnection);
- _selectedConnection = null;
- }
- NodeSelected?.Invoke(node);
- return;
- }
- // 普通点击:如果点击的节点已在多选列表中,保持多选不变(用于拖拽)
- if (node != null && _selectedNodes.Contains(node))
- {
- _selectedNode = node;
- if (_selectedConnection != null)
- {
- _selectedConnection.IsSelected = false;
- if (_connectionPaths.TryGetValue(_selectedConnection.ConnectionId, out var p))
- UpdateConnectionPath(p, _selectedConnection);
- _selectedConnection = null;
- }
- NodeSelected?.Invoke(node);
- return;
- }
- // 否则清除多选,单选
- foreach (var n in _selectedNodes)
- n.IsSelected = false;
- _selectedNodes.Clear();
- if (_selectedNode == node && _selectedConnection == null) return;
- if (_selectedNode != null)
- _selectedNode.IsSelected = false;
- if (_selectedConnection != null)
- {
- _selectedConnection.IsSelected = false;
- if (_connectionPaths.TryGetValue(_selectedConnection.ConnectionId, out var p))
- UpdateConnectionPath(p, _selectedConnection);
- }
- _selectedNode = node;
- _selectedConnection = null;
- if (node != null)
- node.IsSelected = true;
- NodeSelected?.Invoke(node);
- }
- private void SelectConnection(FlowConnection conn)
- {
- foreach (var n in _selectedNodes)
- n.IsSelected = false;
- _selectedNodes.Clear();
- if (_selectedNode != null)
- _selectedNode.IsSelected = false;
- if (_selectedConnection != null)
- {
- _selectedConnection.IsSelected = false;
- if (_connectionPaths.TryGetValue(_selectedConnection.ConnectionId, out var p))
- UpdateConnectionPath(p, _selectedConnection);
- }
- _selectedNode = null;
- _selectedConnection = conn;
- if (conn != null)
- {
- conn.IsSelected = true;
- if (_connectionPaths.TryGetValue(conn.ConnectionId, out var p))
- UpdateConnectionPath(p, conn);
- }
- ConnectionSelected?.Invoke(conn);
- }
- public void ClearSelection()
- {
- foreach (var n in _selectedNodes)
- n.IsSelected = false;
- _selectedNodes.Clear();
- if (_selectedNode != null)
- _selectedNode.IsSelected = false;
- if (_selectedConnection != null)
- {
- _selectedConnection.IsSelected = false;
- if (_connectionPaths.TryGetValue(_selectedConnection.ConnectionId, out var p))
- UpdateConnectionPath(p, _selectedConnection);
- }
- _selectedNode = null;
- _selectedConnection = null;
- SelectionCleared?.Invoke();
- }
- #endregion
- #region 拖放
- protected override void OnDrop(DragEventArgs e)
- {
- base.OnDrop(e);
- NodePluginInfo pluginInfo = null;
- // 尝试从 DataObject 获取插件信息
- if (e.Data.GetDataPresent(typeof(NodePluginInfo)))
- {
- pluginInfo = e.Data.GetData(typeof(NodePluginInfo)) as NodePluginInfo;
- }
- else if (e.Data.GetDataPresent(DataFormats.StringFormat))
- {
- var str = e.Data.GetData(DataFormats.StringFormat) as string;
- if (!string.IsNullOrEmpty(str))
- pluginInfo = PluginLoader.Instance.GetPluginInfo(str);
- }
- if (pluginInfo != null)
- {
- if (IsRunning || IsReadOnly)
- {
- e.Handled = true; // 流程运行中 / 只读模式禁止添加插件
- return;
- }
- // 预览与放置共用 e.GetPosition(实测正确);不用 GetCursorPos/PointFromScreen
- //(其内部窗口原点与 PointToScreen 不一致,PerMonitorV2 下会算出错误位置)
- var pos = e.GetPosition(this);
- double dropX = pos.X - NodeControl.DefaultWidth / 2;
- double dropY = pos.Y - NodeControl.DefaultHeight / 2;
- // 避免与现有节点重叠
- var freePos = FindFreePosition(dropX, dropY);
- NodeDropRequested?.Invoke(freePos.X, freePos.Y, pluginInfo);
- UpdateCanvasSize();
- e.Handled = true;
- }
- }
- #endregion
- #region 键盘
- protected override void OnKeyDown(KeyEventArgs e)
- {
- base.OnKeyDown(e);
- if (e.Key == Key.Delete || e.Key == Key.Back)
- {
- if (IsRunning || IsReadOnly)
- {
- e.Handled = true;
- return;
- }
- // 批量删除多选节点
- if (_selectedNodes.Count > 0)
- {
- foreach (var node in _selectedNodes.ToList())
- _graph?.RemoveNode(node.NodeId);
- _selectedNodes.Clear();
- _selectedNode = null;
- e.Handled = true;
- }
- else if (_selectedNode != null)
- {
- _graph?.RemoveNode(_selectedNode.NodeId);
- _selectedNode = null;
- e.Handled = true;
- }
- else if (_selectedConnection != null)
- {
- _graph?.RemoveConnection(_selectedConnection.ConnectionId);
- _selectedConnection = null;
- e.Handled = true;
- }
- }
- else if (e.Key == Key.C && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
- {
- if (IsRunning || IsReadOnly)
- {
- e.Handled = true;
- return;
- }
- CopySelectedNodes();
- e.Handled = true;
- }
- else if (e.Key == Key.V && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
- {
- if (IsRunning || IsReadOnly)
- {
- e.Handled = true;
- return;
- }
- PasteNodes();
- e.Handled = true;
- }
- else if (e.Key == Key.A && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
- {
- SelectAllNodes();
- e.Handled = true;
- }
- }
- #endregion
- #region 复制粘贴
- /// <summary>
- /// 获取当前选中的节点列表(供 MoveThumb 多选拖拽使用)
- /// </summary>
- public List<FlowNode> GetSelectedNodes() => _selectedNodes;
- /// <summary>
- /// 全选所有节点
- /// </summary>
- private void SelectAllNodes()
- {
- foreach (var n in _selectedNodes)
- n.IsSelected = false;
- _selectedNodes.Clear();
- foreach (var node in _graph?.Nodes ?? Enumerable.Empty<FlowNode>())
- {
- node.IsSelected = true;
- _selectedNodes.Add(node);
- }
- if (_selectedNodes.Count > 0)
- {
- _selectedNode = _selectedNodes[_selectedNodes.Count - 1];
- NodeSelected?.Invoke(_selectedNode);
- }
- }
- private bool HasClipboardData()
- {
- try
- {
- var text = System.Windows.Clipboard.GetText();
- return !string.IsNullOrEmpty(text) && text.StartsWith(ClipboardPrefix);
- }
- catch { return false; }
- }
- private void CopySelectedNodes()
- {
- // 如果多选列表为空但单选不为空,将单选加入多选列表
- if (_selectedNodes.Count == 0 && _selectedNode != null)
- {
- _selectedNode.IsSelected = true;
- _selectedNodes.Add(_selectedNode);
- }
- if (_selectedNodes.Count == 0) return;
- var nodes = new List<FlowNode>();
- var idMap = new Dictionary<string, string>();
- foreach (var node in _selectedNodes)
- {
- var clone = CloneNode(node);
- clone.NodeId = System.Guid.NewGuid().ToString("N");
- idMap[node.NodeId] = clone.NodeId;
- nodes.Add(clone);
- }
- // 复制选中节点之间的连接线(映射到剪贴板节点的ID)
- var nodeIds = new HashSet<string>(_selectedNodes.Select(n => n.NodeId));
- var connections = _graph?.Connections
- .Where(c => nodeIds.Contains(c.SourceNodeId) && nodeIds.Contains(c.TargetNodeId))
- .Select(c => new FlowConnection
- {
- SourceNodeId = idMap[c.SourceNodeId],
- SourceSide = c.SourceSide,
- TargetNodeId = idMap[c.TargetNodeId],
- TargetSide = c.TargetSide,
- BranchCondition = c.BranchCondition
- })
- .ToList();
- // 序列化到系统剪贴板
- try
- {
- var data = new ClipboardData { Nodes = nodes, Connections = connections };
- var formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
- using (var ms = new System.IO.MemoryStream())
- {
- formatter.Serialize(ms, data);
- var base64 = Convert.ToBase64String(ms.ToArray());
- System.Windows.Clipboard.SetText(ClipboardPrefix + base64);
- }
- }
- catch { }
- }
- private void PasteNodes()
- {
- PasteNodesAt(Mouse.GetPosition(this));
- }
- /// <summary>
- /// 在指定位置粘贴节点
- /// </summary>
- public void PasteNodesAt(Point pos)
- {
- if (_graph == null || IsRunning || IsReadOnly) return;
- // 从系统剪贴板读取
- List<FlowNode> clipNodes = null;
- List<FlowConnection> clipConns = null;
- try
- {
- var text = System.Windows.Clipboard.GetText();
- if (!string.IsNullOrEmpty(text) && text.StartsWith(ClipboardPrefix))
- {
- var bytes = Convert.FromBase64String(text.Substring(ClipboardPrefix.Length));
- var formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
- using (var ms = new System.IO.MemoryStream(bytes))
- {
- var data = (ClipboardData)formatter.Deserialize(ms);
- clipNodes = data.Nodes;
- clipConns = data.Connections;
- }
- }
- }
- catch { return; }
- if (clipNodes == null || clipNodes.Count == 0) return;
- ClearSelection();
- // 计算偏移:让剪贴板内容的左上角对齐到鼠标位置
- double minX = double.MaxValue, minY = double.MaxValue;
- foreach (var data in clipNodes)
- {
- if (data.X < minX) minX = data.X;
- if (data.Y < minY) minY = data.Y;
- }
- double offsetX = pos.X - minX;
- double offsetY = pos.Y - minY;
- var newNodes = new List<FlowNode>();
- foreach (var data in clipNodes)
- {
- var newNode = CloneNode(data);
- newNode.X = data.X + offsetX;
- newNode.Y = data.Y + offsetY;
- newNode.IsSelected = true;
- int suffix = 1;
- while (Graph.Nodes.Any(n => n.NodeName == newNode.NodeName))
- newNode.NodeName = $"{newNode.PluginModel.GetModel.ToolName}{++suffix}";
- // 重新初始化所有ID(FlowId/FlowName/NodeId/PluginModel/Group子节点)
- newNode.InitializeNode(_graph.GraphId, _graph.GraphName, _graph.Registry);
- _graph.AddNode(newNode);
- newNodes.Add(newNode);
- }
- _selectedNodes.AddRange(newNodes);
- if (newNodes.Count > 0)
- _selectedNode = newNodes[0];
- // 复制连接线
- if (clipConns != null && clipConns.Count > 0)
- {
- var idMap = new Dictionary<string, string>();
- for (int i = 0; i < clipNodes.Count && i < newNodes.Count; i++)
- idMap[clipNodes[i].NodeId] = newNodes[i].NodeId;
- foreach (var conn in clipConns)
- {
- if (idMap.TryGetValue(conn.SourceNodeId, out var newSrcId) &&
- idMap.TryGetValue(conn.TargetNodeId, out var newTgtId))
- {
- _graph.TryAddConnection(new FlowConnection
- {
- SourceNodeId = newSrcId,
- SourceSide = conn.SourceSide,
- TargetNodeId = newTgtId,
- TargetSide = conn.TargetSide,
- BranchCondition = conn.BranchCondition
- });
- }
- }
- }
- UpdateCanvasSize();
- }
- /// <summary>
- /// 删除所有选中节点
- /// </summary>
- public void DeleteSelectedNodes()
- {
- if (_graph == null || IsRunning || IsReadOnly) return;
- if (_selectedNodes.Count > 0)
- {
- foreach (var node in _selectedNodes.ToList())
- _graph.RemoveNode(node.NodeId);
- _selectedNodes.Clear();
- _selectedNode = null;
- }
- else if (_selectedNode != null)
- {
- _graph.RemoveNode(_selectedNode.NodeId);
- _selectedNode = null;
- }
- }
- /// <summary>
- /// 删除选中节点的所有连接线
- /// </summary>
- public void DeleteSelectedConnections()
- {
- if (_graph == null || IsRunning || IsReadOnly) return;
- var nodes = _selectedNodes.Count > 0 ? _selectedNodes.ToList() :
- (_selectedNode != null ? new List<FlowNode> { _selectedNode } : new List<FlowNode>());
- if (nodes.Count == 0) return;
- var nodeIds = new HashSet<string>(nodes.Select(n => n.NodeId));
- var connectionsToRemove = _graph.Connections
- .Where(c => nodeIds.Contains(c.SourceNodeId) || nodeIds.Contains(c.TargetNodeId))
- .ToList();
- foreach (var conn in connectionsToRemove)
- _graph.RemoveConnection(conn.ConnectionId);
- }
- /// <summary>
- /// 深拷贝节点(使用 BinaryFormatter,完整复制 PluginModel)
- /// </summary>
- private static FlowNode CloneNode(FlowNode source)
- {
- try
- {
- #pragma warning disable SYSLIB0011
- var formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
- using (var stream = new System.IO.MemoryStream())
- {
- formatter.Serialize(stream, source);
- stream.Position = 0;
- return (FlowNode)formatter.Deserialize(stream);
- }
- #pragma warning restore SYSLIB0011
- }
- catch(Exception ex)
- {
- System.Diagnostics.Debug.WriteLine($"[CloneNode] 序列化失败: {ex}");
- DialogHelper.Error($"节点复制失败: {ex.Message}");
- return default;
- }
- }
- #endregion
- [System.Serializable]
- private class ClipboardData
- {
- public List<FlowNode> Nodes;
- public List<FlowConnection> Connections;
- }
- #region 辅助
- private static T FindAncestor<T>(DependencyObject current) where T : DependencyObject
- {
- while (current != null && !(current is T))
- current = VisualTreeHelper.GetParent(current);
- return current as T;
- }
- #endregion
- #region 节点碰撞检测
- /// <summary>
- /// 检查指定位置是否会与其他节点碰撞
- /// </summary>
- public bool CheckNodeCollision(FlowNode draggingNode, double newX, double newY)
- {
- return CheckNodeCollision(draggingNode, newX, newY, null);
- }
- /// <summary>
- /// 检查指定位置是否会与其他节点碰撞。
- /// ignoreNodes 中的节点视为"与拖拽节点同步移动"(多选拖拽的同组成员):
- /// 它们相对位置不变、不会与拖拽成员相撞,因此不作为障碍参与检测。
- /// </summary>
- public bool CheckNodeCollision(FlowNode draggingNode, double newX, double newY, System.Collections.Generic.ICollection<FlowNode> ignoreNodes)
- {
- if (_graph?.Nodes == null) return false;
- var newRect = new Rect(newX, newY, draggingNode.NodeWidth, draggingNode.NodeHeight);
- foreach (var node in _graph.Nodes)
- {
- if (node.NodeId == draggingNode.NodeId) continue;
- if (ignoreNodes != null && ignoreNodes.Contains(node)) continue;
- var nodeRect = new Rect(node.X, node.Y, node.NodeWidth, node.NodeHeight);
- if (newRect.IntersectsWith(nodeRect))
- return true;
- }
- return false;
- }
- /// <summary>
- /// 从指定位置开始查找不与任何节点重叠的空闲位置
- /// </summary>
- public Point FindFreePosition(double startX, double startY)
- {
- double x = startX, y = startY;
- if (_graph?.Nodes == null) return new Point(x, y);
- bool collision;
- do
- {
- collision = false;
- var dropRect = new Rect(x, y, NodeControl.DefaultWidth, NodeControl.DefaultHeight);
- foreach (var node in _graph.Nodes)
- {
- var nodeRect = new Rect(node.X, node.Y, node.NodeWidth, node.NodeHeight);
- if (nodeRect.IntersectsWith(dropRect))
- {
- x += 20;
- y += 20;
- collision = true;
- break;
- }
- }
- } while (collision);
- return new Point(x, y);
- }
- #endregion
- #region 自动拉线
- /// <summary>
- /// 自动拉线 - 单节点拖拽结束时,若节点贴近其他节点则自动建立连接。
- /// 规则:靠下和靠右时其他节点当头(本节点作目标);靠上和靠左时本节点当头(本节点作源)。
- /// 阈值:贴边距离 40px 内,且对应方向重叠量达到较小节点尺寸的 80%。
- /// 拖拽过程中通过 UpdateAutoConnectPreview 显示虚线预览,拉远自动消失。
- /// </summary>
- public void TryAutoConnect(FlowNode node)
- {
- if (node == null || _graph == null || _graph.Nodes == null || _graph.Connections == null)
- {
- ClearPreviewLine();
- return;
- }
- const double threshold = 40; // 贴边距离阈值(px)
- var pos = GetNodeVisualBounds(node);
- foreach (var other in _graph.Nodes)
- {
- if (other == node) continue;
- var oPos = GetNodeVisualBounds(other);
- // 1. 本节点在 other 正下方 → 上连下(other.Bottom → node.Top)
- if (HorizontalOverlap(pos, oPos) && VerticalGap(pos.Top, oPos.Bottom) <= threshold)
- {
- if (TryAutoAdd(other, PortSide.Bottom, node, PortSide.Top))
- {
- ClearPreviewLine();
- return; // 每次拖拽只自动连一条
- }
- }
- // 2. 本节点在 other 正上方 → 本节点当头(node.Bottom → other.Top)
- if (HorizontalOverlap(pos, oPos) && VerticalGap(oPos.Top, pos.Bottom) <= threshold)
- {
- if (TryAutoAdd(node, PortSide.Bottom, other, PortSide.Top))
- {
- ClearPreviewLine();
- return;
- }
- }
- // 3. 本节点在 other 正右方 →(other.Right → node.Left)
- if (VerticalOverlap(pos, oPos) && HorizontalGap(pos.Left, oPos.Right) <= threshold)
- {
- if (TryAutoAdd(other, PortSide.Right, node, PortSide.Left))
- {
- ClearPreviewLine();
- return;
- }
- }
- // 4. 本节点在 other 正左方 → 本节点当头(node.Right → other.Left)
- if (VerticalOverlap(pos, oPos) && HorizontalGap(oPos.Left, pos.Right) <= threshold)
- {
- if (TryAutoAdd(node, PortSide.Right, other, PortSide.Left))
- {
- ClearPreviewLine();
- return;
- }
- }
- }
- // 没有成功连接 → 清除预览
- ClearPreviewLine();
- }
- /// <summary>
- /// 拖拽过程中更新自动拉线虚线预览;靠近且可连接时显示,否则清除
- /// </summary>
- public void UpdateAutoConnectPreview(FlowNode node)
- {
- ClearPreviewLine();
- if (node == null || _graph == null || _graph.Nodes == null || _graph.Connections == null)
- return;
- const double threshold = 40; // 贴边距离阈值(px)
- var pos = GetNodeVisualBounds(node);
- foreach (var other in _graph.Nodes)
- {
- if (other == node) continue;
- var oPos = GetNodeVisualBounds(other);
- FlowNode source = null;
- PortSide sourceSide = PortSide.Bottom;
- FlowNode target = null;
- PortSide targetSide = PortSide.Top;
- // 1. 本节点在 other 正下方 → 上连下(other.Bottom → node.Top)
- if (HorizontalOverlap(pos, oPos) && VerticalGap(pos.Top, oPos.Bottom) <= threshold)
- {
- source = other; sourceSide = PortSide.Bottom;
- target = node; targetSide = PortSide.Top;
- }
- // 2. 本节点在 other 正上方 → 本节点当头(node.Bottom → other.Top)
- else if (HorizontalOverlap(pos, oPos) && VerticalGap(oPos.Top, pos.Bottom) <= threshold)
- {
- source = node; sourceSide = PortSide.Bottom;
- target = other; targetSide = PortSide.Top;
- }
- // 3. 本节点在 other 正右方 →(other.Right → node.Left)
- else if (VerticalOverlap(pos, oPos) && HorizontalGap(pos.Left, oPos.Right) <= threshold)
- {
- source = other; sourceSide = PortSide.Right;
- target = node; targetSide = PortSide.Left;
- }
- // 4. 本节点在 other 正左方 → 本节点当头(node.Right → other.Left)
- else if (VerticalOverlap(pos, oPos) && HorizontalGap(oPos.Left, pos.Right) <= threshold)
- {
- source = node; sourceSide = PortSide.Right;
- target = other; targetSide = PortSide.Left;
- }
- if (source == null) continue;
- // 预检:能连才显示预览(复用防重/防环逻辑)
- var connection = new FlowConnection
- {
- SourceNodeId = source.NodeId,
- SourceSide = sourceSide,
- TargetNodeId = target.NodeId,
- TargetSide = targetSide
- };
- if (!_graph.CanConnect(connection))
- continue;
- Point start = GetPortPosition(source, sourceSide);
- Point end = GetPortPosition(target, targetSide);
- if (_previewLine == null)
- {
- _previewLine = new Path
- {
- Stroke = new SolidColorBrush(Color.FromRgb(232, 145, 73)),
- StrokeThickness = 2,
- StrokeDashArray = new DoubleCollection { 4, 3 },
- IsHitTestVisible = false
- };
- Panel.SetZIndex(_previewLine, 10000);
- Children.Add(_previewLine);
- }
- _previewLine.Data = CreateConnectionGeometry(start, end, sourceSide, targetSide);
- return;
- }
- }
- /// <summary>
- /// 清除自动拉线虚线预览
- /// </summary>
- public void ClearPreviewLine()
- {
- if (_previewLine != null)
- {
- Children.Remove(_previewLine);
- _previewLine = null;
- }
- }
- /// <summary>
- /// 构造连线并交给 Graph 添加(复用已有防重/防环逻辑)
- /// </summary>
- private bool TryAutoAdd(FlowNode source, PortSide sourceSide, FlowNode target, PortSide targetSide)
- {
- var connection = new FlowConnection
- {
- SourceNodeId = source.NodeId,
- SourceSide = sourceSide,
- TargetNodeId = target.NodeId,
- TargetSide = targetSide
- };
- // 预检:能连才添加(复用防重/防环逻辑)
- if (!_graph.CanConnect(connection))
- return false;
- return _graph.TryAddConnection(connection);
- }
- private Rect GetNodeVisualBounds(FlowNode node)
- {
- double left = node.X;
- double top = node.Y;
- double w = NodeControl.DefaultWidth;
- double h = NodeControl.DefaultHeight;
- return new Rect(left, top, w, h);
- }
- private static bool HorizontalOverlap(Rect a, Rect b)
- {
- double overlap = Math.Min(a.Right, b.Right) - Math.Max(a.Left, b.Left);
- double minW = Math.Min(a.Width, b.Width);
- return minW > 0 && overlap >= minW * 0.8;
- }
- private static bool VerticalOverlap(Rect a, Rect b)
- {
- double overlap = Math.Min(a.Bottom, b.Bottom) - Math.Max(a.Top, b.Top);
- double minH = Math.Min(a.Height, b.Height);
- return minH > 0 && overlap >= minH * 0.8;
- }
- private static double VerticalGap(double aBottom, double bTop)
- {
- return Math.Abs(bTop - aBottom);
- }
- private static double HorizontalGap(double aRight, double bLeft)
- {
- return Math.Abs(bLeft - aRight);
- }
- #endregion
- #region 鸟瞰图支持
- /// <summary>
- /// 动态更新画布大小,使内容超出时自动扩展
- /// </summary>
- public void UpdateCanvasSize()
- {
- if (_graph?.Nodes == null || _graph.Nodes.Count == 0) return;
- var bounds = GetContentBounds();
- double minW = ActualWidth > 0 ? ActualWidth : 800;
- double minH = ActualHeight > 0 ? ActualHeight : 600;
- double newW = Math.Max(bounds.Right + 300, minW);
- double newH = Math.Max(bounds.Bottom + 300, minH);
- if (Math.Abs(Width - newW) > 1 || Math.Abs(Height - newH) > 1)
- {
- Width = newW;
- Height = newH;
- }
- }
- /// <summary>
- /// 适应内容 - 计算所有节点边界,缩放平移使内容完整显示并居中
- /// </summary>
- public void FitToContent()
- {
- var bounds = GetContentBounds();
- var vp = GetViewportSize();
- if (bounds.Width <= 0 || bounds.Height <= 0 || vp.Width <= 0 || vp.Height <= 0)
- {
- // 无内容或视口异常:复位 100% 原点
- _scale.ScaleX = 1.0;
- _scale.ScaleY = 1.0;
- _translate.X = 0;
- _translate.Y = 0;
- ZoomChanged?.Invoke(1.0);
- return;
- }
- const double padding = 50; // 四周留白
- double scaleX = (vp.Width - padding * 2) / bounds.Width;
- double scaleY = (vp.Height - padding * 2) / bounds.Height;
- double newScale = Math.Min(scaleX, scaleY);
- newScale = Math.Max(MinZoom, Math.Min(1.0, newScale)); // 内容小时不放大超过100%
- _scale.ScaleX = newScale;
- _scale.ScaleY = newScale;
- // 居中:内容中心对齐视口中心,但 0,0 原点固定在左上角(不跑到画布外)
- double centerX = bounds.X + bounds.Width / 2;
- double centerY = bounds.Y + bounds.Height / 2;
- _translate.X = Math.Min(0, vp.Width / 2 - centerX * newScale);
- _translate.Y = Math.Min(0, vp.Height / 2 - centerY * newScale);
- ZoomChanged?.Invoke(newScale);
- }
- /// <summary>
- /// 获取所有节点的内容边界
- /// </summary>
- public Rect GetContentBounds()
- {
- if (_graph?.Nodes == null || _graph.Nodes.Count == 0)
- return new Rect(0, 0, 0, 0);
- double minX = double.MaxValue, minY = double.MaxValue;
- double maxX = double.MinValue, maxY = double.MinValue;
- foreach (var node in _graph.Nodes)
- {
- minX = Math.Min(minX, node.X);
- minY = Math.Min(minY, node.Y);
- maxX = Math.Max(maxX, node.X + node.NodeWidth);
- maxY = Math.Max(maxY, node.Y + node.NodeHeight);
- }
- return new Rect(minX, minY, maxX - minX, maxY - minY);
- }
- /// <summary>
- /// 获取当前视口(画布坐标空间中的可见区域)
- /// </summary>
- public Rect GetViewport()
- {
- var vp = GetViewportSize();
- double x = -_translate.X / _scale.ScaleX;
- double y = -_translate.Y / _scale.ScaleY;
- double w = vp.Width / _scale.ScaleX;
- double h = vp.Height / _scale.ScaleY;
- return new Rect(x, y, w, h);
- }
- /// <summary>
- /// 获取可视区域大小(屏幕像素)- 取父容器的实际尺寸
- /// </summary>
- private Size GetViewportSize()
- {
- var parent = System.Windows.Media.VisualTreeHelper.GetParent(this) as FrameworkElement;
- if (parent != null && parent.ActualWidth > 0 && parent.ActualHeight > 0)
- return new Size(parent.ActualWidth, parent.ActualHeight);
- return new Size(800, 600);
- }
- /// <summary>
- /// 将画布视口居中到指定画布坐标点
- /// </summary>
- public void CenterOn(Point canvasPoint)
- {
- var vp = GetViewportSize();
- _translate.X = vp.Width / 2 - canvasPoint.X * _scale.ScaleX;
- _translate.Y = vp.Height / 2 - canvasPoint.Y * _scale.ScaleY;
- ClampTranslate();
- }
- #endregion
- #region 自动排版
- /// <summary>
- /// 层次化自动排版:从左到右分层,同层从上到下排列,最小化连线交叉。
- /// 算法:拓扑排序分层 → 重心启发式排序 → 居中对齐 → 连线方向归一化
- /// </summary>
- public void AutoLayout()
- {
- if (_graph == null || _graph.Nodes.Count == 0) return;
- if (IsRunning || IsReadOnly) return;
- var nodes = _graph.Nodes.ToList();
- var conns = _graph.Connections.ToList();
- var nodeIds = nodes.Select(n => n.NodeId).ToHashSet();
- // 1. 构建邻接表
- var succ = new Dictionary<string, List<string>>();
- var pred = new Dictionary<string, List<string>>();
- foreach (var n in nodes)
- {
- succ[n.NodeId] = new List<string>();
- pred[n.NodeId] = new List<string>();
- }
- foreach (var c in conns)
- {
- if (nodeIds.Contains(c.SourceNodeId) && nodeIds.Contains(c.TargetNodeId))
- {
- succ[c.SourceNodeId].Add(c.TargetNodeId);
- pred[c.TargetNodeId].Add(c.SourceNodeId);
- }
- }
- // 2. 层次分配(最长路径:node.layer = max(前驱 layer) + 1)
- var layer = new Dictionary<string, int>();
- var inDeg = nodes.ToDictionary(n => n.NodeId, n => pred[n.NodeId].Count);
- var queue = new Queue<string>();
- foreach (var n in nodes)
- if (inDeg[n.NodeId] == 0) queue.Enqueue(n.NodeId);
- while (queue.Count > 0)
- {
- var id = queue.Dequeue();
- int maxPred = -1;
- foreach (var p in pred[id])
- if (layer.TryGetValue(p, out var pl)) maxPred = Math.Max(maxPred, pl);
- layer[id] = maxPred + 1;
- foreach (var s in succ[id])
- if (--inDeg[s] == 0) queue.Enqueue(s);
- }
- // 未分配(孤立节点)放第 0 层
- foreach (var n in nodes)
- if (!layer.ContainsKey(n.NodeId)) layer[n.NodeId] = 0;
- // 3. 按层分组
- int maxLayer = layer.Values.Max();
- var layers = new List<List<string>>();
- for (int i = 0; i <= maxLayer; i++) layers.Add(new List<string>());
- foreach (var kv in layer) layers[kv.Value].Add(kv.Key);
- // 4. 层内排序:重心启发式(按前驱 Y 中位数排序,减少交叉)
- const double layerGap = 300; // 层间距(水平)
- const double nodeGap = 50; // 同层节点间距(垂直)
- const double startX = 40;
- const double startY = 40;
- var yPos = new Dictionary<string, double>();
- for (int li = 0; li < layers.Count; li++)
- {
- var layerNodes = layers[li];
- if (li > 0 && layerNodes.Count > 1)
- {
- layerNodes.Sort((a, b) =>
- {
- double ma = MedianY(pred[a], yPos);
- double mb = MedianY(pred[b], yPos);
- return ma.CompareTo(mb);
- });
- }
- double y = startY;
- foreach (var id in layerNodes)
- {
- var node = _graph.GetNode(id);
- if (node == null) continue;
- node.X = startX + li * layerGap;
- node.Y = y;
- yPos[id] = y;
- y += node.NodeHeight + nodeGap;
- }
- }
- // 5. 各层垂直居中
- double maxLayerH = 0;
- var layerHeights = new double[layers.Count];
- for (int li = 0; li < layers.Count; li++)
- {
- var ln = layers[li];
- if (ln.Count == 0) continue;
- var first = _graph.GetNode(ln[0]);
- var last = _graph.GetNode(ln[ln.Count - 1]);
- if (first == null || last == null) continue;
- double h = (last.Y + last.NodeHeight) - first.Y;
- layerHeights[li] = h;
- maxLayerH = Math.Max(maxLayerH, h);
- }
- for (int li = 0; li < layers.Count; li++)
- {
- double offset = (maxLayerH - layerHeights[li]) / 2;
- if (offset <= 0) continue;
- foreach (var id in layers[li])
- {
- var node = _graph.GetNode(id);
- if (node != null) node.Y += offset;
- }
- }
- // 6. 连线方向归一化:跨层 Right→Left,同层 Bottom→Top
- foreach (var c in conns)
- {
- var src = _graph.GetNode(c.SourceNodeId);
- var tgt = _graph.GetNode(c.TargetNodeId);
- if (src == null || tgt == null) continue;
- if (layer.TryGetValue(c.SourceNodeId, out var sl) &&
- layer.TryGetValue(c.TargetNodeId, out var tl) && sl == tl)
- {
- c.SourceSide = PortSide.Bottom;
- c.TargetSide = PortSide.Top;
- }
- else
- {
- c.SourceSide = PortSide.Right;
- c.TargetSide = PortSide.Left;
- }
- }
- // 7. 刷新视觉
- foreach (var node in nodes)
- {
- if (_nodeControls.TryGetValue(node.NodeId, out var ctrl))
- {
- SetLeft(ctrl, node.X);
- SetTop(ctrl, node.Y);
- }
- }
- foreach (var kvp in _connectionPaths)
- if (kvp.Value.Tag is FlowConnection fc) UpdateConnectionPath(kvp.Value, fc);
- // 8. 适应内容
- UpdateCanvasSize();
- FitToContent();
- }
- private static double MedianY(List<string> predIds, Dictionary<string, double> yPos)
- {
- var ys = predIds.Where(yPos.ContainsKey).Select(id => yPos[id]).OrderBy(v => v).ToList();
- if (ys.Count == 0) return 0;
- int mid = ys.Count / 2;
- return ys.Count % 2 == 0 ? (ys[mid - 1] + ys[mid]) / 2 : ys[mid];
- }
- #endregion
- #region 平移限制
- /// <summary>
- /// 限制平移范围,确保不会看到内容区域之外
- /// </summary>
- private void ClampTranslate()
- {
- // 原点固定,不超出画布边界
- var vp = GetViewportSize();
- // 用画布尺寸判断平移范围,而非节点范围
- double canvasW = (Width > 0 ? Width : 5000) * _scale.ScaleX;
- double canvasH = (Height > 0 ? Height : 3500) * _scale.ScaleY;
- // X 轴:原点不动,不超出右下边界
- if (canvasW <= vp.Width)
- _translate.X = 0;
- else
- {
- double minX = vp.Width - canvasW;
- if (_translate.X < minX) _translate.X = minX;
- if (_translate.X > 0) _translate.X = 0;
- }
- // Y 轴
- if (canvasH <= vp.Height)
- _translate.Y = 0;
- else
- {
- double minY = vp.Height - canvasH;
- if (_translate.Y < minY) _translate.Y = minY;
- if (_translate.Y > 0) _translate.Y = 0;
- }
- }
- #endregion
- #region 对齐引导线
- private readonly List<System.Windows.Documents.Adorner> _alignLines = new List<System.Windows.Documents.Adorner>();
- public void AddAlignLine(Point start, Point end)
- {
- var layer = System.Windows.Documents.AdornerLayer.GetAdornerLayer(this);
- if (layer == null) return;
- // Adorner 坐标空间与画布布局坐标空间一致,直接传画布坐标
- var line = new SelectionAlignLine(this, start, end);
- layer.Add(line);
- _alignLines.Add(line);
- }
- public void ClearAlignLines()
- {
- var layer = System.Windows.Documents.AdornerLayer.GetAdornerLayer(this);
- if (layer != null)
- {
- foreach (var line in _alignLines)
- layer.Remove(line);
- }
- _alignLines.Clear();
- }
- /// <summary>
- /// 检查节点对齐并返回对齐偏移量
- /// </summary>
- public void CheckAlignment(FlowNode draggingNode, ref double x, ref double y)
- {
- ClearAlignLines();
- const double threshold = 5.0;
- double nodeLeft = x;
- double nodeTop = y;
- double nodeRight = x + draggingNode.NodeWidth;
- double nodeBottom = y + draggingNode.NodeHeight;
- double nodeCenterX = x + draggingNode.NodeWidth / 2;
- double nodeCenterY = y + draggingNode.NodeHeight / 2;
- double snapX = double.NaN;
- double snapY = double.NaN;
- if (_graph?.Nodes == null) return;
- foreach (var other in _graph.Nodes)
- {
- if (other.NodeId == draggingNode.NodeId) continue;
- double otherLeft = other.X;
- double otherTop = other.Y;
- double otherRight = other.X + other.NodeWidth;
- double otherBottom = other.Y + other.NodeHeight;
- double otherCenterX = other.X + other.NodeWidth / 2;
- double otherCenterY = other.Y + other.NodeHeight / 2;
- // 垂直对齐检测(显示垂直引导线,调整 X)
- if (!double.IsNaN(snapX))
- {
- // 已有 X 对齐,只检查 Y
- }
- else if (Math.Abs(nodeLeft - otherLeft) < threshold)
- {
- snapX = otherLeft;
- AddAlignLine(new Point(otherLeft, Math.Min(nodeTop, otherTop)),
- new Point(otherLeft, Math.Max(nodeBottom, otherBottom)));
- }
- else if (Math.Abs(nodeRight - otherRight) < threshold)
- {
- snapX = otherRight - draggingNode.NodeWidth;
- AddAlignLine(new Point(otherRight, Math.Min(nodeTop, otherTop)),
- new Point(otherRight, Math.Max(nodeBottom, otherBottom)));
- }
- else if (Math.Abs(nodeCenterX - otherCenterX) < threshold)
- {
- snapX = otherCenterX - draggingNode.NodeWidth / 2;
- AddAlignLine(new Point(otherCenterX, Math.Min(nodeTop, otherTop)),
- new Point(otherCenterX, Math.Max(nodeBottom, otherBottom)));
- }
- // 水平对齐检测(显示水平引导线,调整 Y)
- if (!double.IsNaN(snapY))
- {
- // 已有 Y 对齐
- }
- else if (Math.Abs(nodeTop - otherTop) < threshold)
- {
- snapY = otherTop;
- AddAlignLine(new Point(Math.Min(nodeLeft, otherLeft), otherTop),
- new Point(Math.Max(nodeRight, otherRight), otherTop));
- }
- else if (Math.Abs(nodeBottom - otherBottom) < threshold)
- {
- snapY = otherBottom - draggingNode.NodeHeight;
- AddAlignLine(new Point(Math.Min(nodeLeft, otherLeft), otherBottom),
- new Point(Math.Max(nodeRight, otherRight), otherBottom));
- }
- else if (Math.Abs(nodeCenterY - otherCenterY) < threshold)
- {
- snapY = otherCenterY - draggingNode.NodeHeight / 2;
- AddAlignLine(new Point(Math.Min(nodeLeft, otherLeft), otherCenterY),
- new Point(Math.Max(nodeRight, otherRight), otherCenterY));
- }
- }
- if (!double.IsNaN(snapX)) x = snapX;
- if (!double.IsNaN(snapY)) y = snapY;
- }
- #endregion
- }
- }
|