FlowEditorViewModel.cs 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823
  1. using Prism.Commands;
  2. using Prism.Mvvm;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Collections.ObjectModel;
  6. using System.Collections.Specialized;
  7. using System.ComponentModel;
  8. using System.IO;
  9. using System.Linq;
  10. using System.Reflection;
  11. using System.Runtime.Serialization;
  12. using System.Runtime.Serialization.Formatters.Binary;
  13. using System.Text;
  14. using System.Threading;
  15. using System.Threading.Tasks;
  16. using System.Windows;
  17. using TeamAAS.FlowEditor.Execution;
  18. using TeamAAS.FlowEditor.Models;
  19. using TeamAAS.FlowEditor.Plugins;
  20. using TeamAAS.FlowEngine.Execution;
  21. using TeamAAS.FlowEngine;
  22. namespace TeamAAS.FlowEditor
  23. {
  24. /// <summary>
  25. /// 流程编辑器 ViewModel(单流程)
  26. /// </summary>
  27. public class FlowEditorViewModel : TeamAAS.BindableBase
  28. {
  29. #region 属性
  30. private FlowGraph _graph;
  31. public FlowGraph Graph
  32. {
  33. get => _graph;
  34. set
  35. {
  36. if (SetProperty(ref _graph, value))
  37. HookGraph(_graph);
  38. }
  39. }
  40. public PropertyChangedEventHandler GraphDataChanged { get; set; }
  41. #region 脏标记(决定"切换产品时是否提示保存")
  42. private bool _isDirty;
  43. /// <summary>流程是否被修改过(点击保存或加载后恢复为 false)</summary>
  44. public bool IsDirty
  45. {
  46. get => _isDirty;
  47. private set => SetProperty(ref _isDirty, value);
  48. }
  49. /// <summary>标记流程已修改(节点增删/移动/连线/属性编辑等)</summary>
  50. public void MarkDirty()
  51. {
  52. IsDirty = true;
  53. }
  54. /// <summary>标记流程已保存/刚加载(清除修改标记)</summary>
  55. public void MarkClean()
  56. {
  57. IsDirty = false;
  58. }
  59. private bool _dirtyTrackingHooked;
  60. private void HookGraph(FlowGraph graph)
  61. {
  62. if (graph == null) return;
  63. if (_dirtyTrackingHooked)
  64. {
  65. graph.Nodes.CollectionChanged -= OnNodesCollectionChanged;
  66. graph.Connections.CollectionChanged -= OnConnectionsCollectionChanged;
  67. foreach (var node in graph.Nodes)
  68. node.PropertyChanged -= OnNodePropertyChanged;
  69. }
  70. graph.Nodes.CollectionChanged += OnNodesCollectionChanged;
  71. graph.Connections.CollectionChanged += OnConnectionsCollectionChanged;
  72. foreach (var node in graph.Nodes)
  73. node.PropertyChanged += OnNodePropertyChanged;
  74. _dirtyTrackingHooked = true;
  75. }
  76. private void OnNodesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
  77. {
  78. if (e.OldItems != null)
  79. foreach (FlowNode node in e.OldItems)
  80. node.PropertyChanged -= OnNodePropertyChanged;
  81. if (e.NewItems != null)
  82. foreach (FlowNode node in e.NewItems)
  83. node.PropertyChanged += OnNodePropertyChanged;
  84. MarkDirty();
  85. }
  86. private void OnConnectionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
  87. {
  88. MarkDirty();
  89. }
  90. // 只有"用户编辑类"属性才计入脏标记;运行状态(Status/CostTime/结果等)不算
  91. private static readonly HashSet<string> EditProperties = new HashSet<string>
  92. {
  93. nameof(FlowNode.X),
  94. nameof(FlowNode.Y),
  95. nameof(FlowNode.NodeName),
  96. nameof(FlowNode.IsEnabled),
  97. nameof(FlowNode.IsSkipWarning),
  98. nameof(FlowNode.NodeWidth),
  99. nameof(FlowNode.NodeHeight),
  100. };
  101. private void OnNodePropertyChanged(object sender, PropertyChangedEventArgs e)
  102. {
  103. if (EditProperties.Contains(e.PropertyName))
  104. MarkDirty();
  105. }
  106. #endregion
  107. private bool _isSubFlowEditor;
  108. /// <summary>
  109. /// 是否为子流程编辑器
  110. /// </summary>
  111. public bool IsSubFlowEditor
  112. {
  113. get => _isSubFlowEditor;
  114. set
  115. {
  116. if (SetProperty(ref _isSubFlowEditor, value))
  117. {
  118. RunFlowCommand?.RaiseCanExecuteChanged();
  119. }
  120. }
  121. }
  122. public ObservableCollection<ToolboxGroup> ToolboxGroups { get; private set; }
  123. private FlowNode _selectedNode;
  124. public FlowNode SelectedNode
  125. {
  126. get => _selectedNode;
  127. set
  128. {
  129. if (SetProperty(ref _selectedNode, value))
  130. {
  131. // 切换节点时,自动选中最新历史记录(倒序,最新在 index 0)
  132. if (_selectedNode != null && _selectedNode.ExecutionHistory?.Count > 0)
  133. _selectedNode.SelectedHistoryEntry = _selectedNode.ExecutionHistory[0];
  134. }
  135. }
  136. }
  137. private double _zoom = 1.0;
  138. public double Zoom
  139. {
  140. get => _zoom;
  141. set => SetProperty(ref _zoom, value);
  142. }
  143. private bool _isRunning;
  144. public bool IsRunning
  145. {
  146. get => _isRunning;
  147. set => SetProperty(ref _isRunning, value);
  148. }
  149. private bool _isReadOnly;
  150. /// <summary>
  151. /// 只读监控模式(运行产品运行中):可选中节点查看结果,禁止一切修改与编辑器内运行。
  152. /// </summary>
  153. public bool IsReadOnly
  154. {
  155. get => _isReadOnly;
  156. private set => SetProperty(ref _isReadOnly, value);
  157. }
  158. /// <summary>由 Shell/产品级状态设置只读模式</summary>
  159. public void SetReadOnly(bool value)
  160. {
  161. IsReadOnly = value;
  162. RunFlowCommand?.RaiseCanExecuteChanged();
  163. StopFlowCommand?.RaiseCanExecuteChanged();
  164. }
  165. private bool _isLoading;
  166. /// <summary>导入流程时是否正在加载(画布显示转圈动画)</summary>
  167. public bool IsLoading
  168. {
  169. get => _isLoading;
  170. set => SetProperty(ref _isLoading, value);
  171. }
  172. private double _canvasWidth = 3500;
  173. /// <summary>画布宽度</summary>
  174. public double CanvasWidth
  175. {
  176. get => _canvasWidth;
  177. set => SetProperty(ref _canvasWidth, value);
  178. }
  179. private double _canvasHeight = 3500;
  180. /// <summary>画布高度</summary>
  181. public double CanvasHeight
  182. {
  183. get => _canvasHeight;
  184. set => SetProperty(ref _canvasHeight, value);
  185. }
  186. #endregion
  187. #region 命令
  188. [field: NonSerialized]
  189. public DelegateCommand ClearSelectionCommand { get; private set; }
  190. public DelegateCommand DeleteSelectedCommand { get; private set; }
  191. public DelegateCommand RunFlowCommand { get; private set; }
  192. public DelegateCommand StopFlowCommand { get; private set; }
  193. public DelegateCommand ImportFlowCommand { get; private set; }
  194. public DelegateCommand ExportFlowCommand { get; private set; }
  195. #endregion
  196. #region 节点创建
  197. /// <summary>
  198. /// 从插件描述符创建节点
  199. /// </summary>
  200. public static FlowNode CreateNodeFromPlugin(string NodeName,FlowGraph flowGraph, NodePluginInfo info, double x, double y)
  201. {
  202. var desc = PluginLoader.Instance.GetDescriptor(info.PluginId);
  203. var node = new FlowNode
  204. {
  205. NodeName = NodeName,
  206. Category = desc?.NodeShape ?? info.Category,
  207. FlowName = flowGraph.GraphName,
  208. FlowId = flowGraph.GraphId,
  209. PluginId = info.PluginId,
  210. X = x,
  211. Y = y,
  212. IconGeometry = info.IconGeometry
  213. };
  214. // 创建插件实例并获取默认模型
  215. var plugin = PluginLoader.Instance.CreateInstance(info.DisplayName);
  216. if (plugin != null)
  217. {
  218. plugin.GetModel.NodeId = node.NodeId;
  219. plugin.GetModel.ToolName = info.DisplayName;
  220. plugin.GetModel.NodeName = node.NodeName;
  221. plugin.GetModel.PluginId = node.PluginId;
  222. plugin.GetModel.FlowName = flowGraph.GraphName;
  223. plugin.GetModel.FlowId = flowGraph.GraphId;
  224. node.Registry = flowGraph.Registry;
  225. plugin.Registry = flowGraph.Registry; // InitRun 前需就绪,使默认输出注册到正确的注册表
  226. // 新建节点立即执行一次初始化运行:触发 DeclareOutputs 生成默认输出、注册到 ResultRegistry(供绑定树可见)、
  227. // 并填充 LastResults;随后 PluginModel setter 会用 LastResults 刷新节点输出列表。
  228. // 修复:从工具箱新拖入的节点不显示输出(此前仅 InitializeNode 加载/粘贴路径才 InitRun)。
  229. plugin.InitRun();
  230. node.PluginModel = plugin;
  231. }
  232. return node;
  233. }
  234. #endregion
  235. #region 构造函数
  236. public FlowEditorViewModel(FlowGraph graph = null, bool isSubFlowEditor = false, FlowToolboxScope toolboxScope = FlowToolboxScope.Main)
  237. {
  238. Graph = graph ?? new FlowGraph { GraphName = "流程" };
  239. IsSubFlowEditor = isSubFlowEditor;
  240. ToolboxScope = toolboxScope;
  241. InitCommands();
  242. MarkClean();
  243. }
  244. /// <summary>
  245. /// 工具箱作用域:决定本编辑器工具箱展示哪一批插件、按什么分组。
  246. /// Main=主流程(排除 IsSubFlowNode 子节点,按 PluginCategory 分组);
  247. /// Halcon=Halcon 子流程(只显 IsSubFlowNode 子节点,按 VisionPlugin 分组)。
  248. /// </summary>
  249. public FlowToolboxScope ToolboxScope { get; private set; } = FlowToolboxScope.Main;
  250. private void InitCommands()
  251. {
  252. // 从 PluginLoader 构建工具箱(按作用域过滤/分组)
  253. ToolboxGroups = new ObservableCollection<ToolboxGroup>();
  254. var infos = PluginLoader.Instance.GetAllPluginInfos();
  255. if (ToolboxScope != FlowToolboxScope.Main)
  256. {
  257. // 平台子流程编辑器(Halcon/Vpp/Vm):只取标记为子流程节点的插件,并按平台
  258. // (PluginCategory: Halocn模块/Vpp模块/Vm模块)隔离 —— 各平台只见到自己的算子,
  259. // 平台内按 VisionPlugin 枚举分组
  260. PluginCategory platform;
  261. switch (ToolboxScope)
  262. {
  263. case FlowToolboxScope.VisionVpp: platform = PluginCategory.Vpp模块; break;
  264. case FlowToolboxScope.VisionVm: platform = PluginCategory.Vm模块; break;
  265. default: platform = PluginCategory.Halocn模块; break;
  266. }
  267. var subInfos = infos.Where(i => i.IsSubFlowNode && i.Group == platform).ToList();
  268. foreach (var g in subInfos.GroupBy(i => i.VisionCategory).OrderBy(g => g.Key))
  269. {
  270. ToolboxGroups.Add(new ToolboxGroup
  271. {
  272. GroupName = g.Key.ToString(),
  273. Category = platform,
  274. GroupIcon = VisionCategoryIconMap.GetIcon(g.Key),
  275. Items = g.ToList()
  276. });
  277. }
  278. }
  279. else
  280. {
  281. // 主流程:排除子流程专用节点(保持既有行为——现有插件 IsSubFlowNode 均为 false,不受影响)
  282. var mainInfos = infos.Where(i => !i.IsSubFlowNode).ToList();
  283. foreach (var g in mainInfos.GroupBy(i => i.Group))
  284. {
  285. ToolboxGroups.Add(new ToolboxGroup
  286. {
  287. GroupName = g.Key.ToString(),
  288. Category = g.Key,
  289. GroupIcon = CategoryIconMap.GetIcon(g.Key),
  290. Items = g.ToList()
  291. });
  292. }
  293. }
  294. ClearSelectionCommand = new DelegateCommand(() => SelectedNode = null);
  295. DeleteSelectedCommand = new DelegateCommand(() =>
  296. {
  297. if (SelectedNode != null)
  298. {
  299. Graph.RemoveNode(SelectedNode.NodeId);
  300. SelectedNode = null;
  301. }
  302. });
  303. RunFlowCommand = new DelegateCommand(async () => await RunFlowAsync(), () => !IsRunning && !IsSubFlowEditor && !IsReadOnly);
  304. StopFlowCommand = new DelegateCommand(() => StopFlow(), () => IsRunning);
  305. // 导入/导出统一走命令(工具栏按钮与 Shell 右键菜单共用,避免重复代码)
  306. ImportFlowCommand = new DelegateCommand(async () =>
  307. {
  308. if (IsReadOnly)
  309. {
  310. DialogHelper.Info("运行产品监控中,无法导入流程");
  311. return;
  312. }
  313. var dlg = new Microsoft.Win32.OpenFileDialog
  314. {
  315. Filter = "流程文件|*.aas|所有文件|*.*",
  316. Title = "导入流程"
  317. };
  318. if (dlg.ShowDialog() == true)
  319. {
  320. if (Graph != null && await ImportFlowAsync(Graph.GraphName, dlg.FileName) == true)
  321. DialogHelper.Success("导入成功");
  322. else
  323. DialogHelper.Error("导入失败");
  324. }
  325. });
  326. ExportFlowCommand = new DelegateCommand(() =>
  327. {
  328. if (Graph == null) return;
  329. var dlg = new Microsoft.Win32.SaveFileDialog
  330. {
  331. Filter = "流程文件|*.aas|所有文件|*.*",
  332. Title = "导出流程",
  333. FileName = Graph.GraphName + ".aas"
  334. };
  335. if (dlg.ShowDialog() == true)
  336. {
  337. if (ExportFlow(Graph.GraphName, dlg.FileName) == true)
  338. DialogHelper.Success("导出成功");
  339. else
  340. DialogHelper.Error("导出失败");
  341. }
  342. });
  343. }
  344. #endregion
  345. #region 执行
  346. private CancellationTokenSource _cts;
  347. private FlowExecutor _executor;
  348. public async Task RunFlowAsync()
  349. {
  350. if (IsRunning) return;
  351. _cts = new CancellationTokenSource();
  352. _executor = new FlowExecutor(Graph);
  353. IsRunning = true;
  354. RunFlowCommand.RaiseCanExecuteChanged();
  355. StopFlowCommand.RaiseCanExecuteChanged();
  356. _executor.ExecutionCompleted += (success) =>
  357. {
  358. IsRunning = false;
  359. RunFlowCommand.RaiseCanExecuteChanged();
  360. StopFlowCommand.RaiseCanExecuteChanged();
  361. };
  362. try
  363. {
  364. await _executor.ExecuteAsync(_cts.Token);
  365. }
  366. finally
  367. {
  368. // 兜底:即使 ExecutionCompleted 因 UI 线程繁忙被延迟、或执行器抛异常,
  369. // 也保证运行态复位,避免"执行完仍被锁住、无法再次运行"。
  370. IsRunning = false;
  371. RunFlowCommand?.RaiseCanExecuteChanged();
  372. StopFlowCommand?.RaiseCanExecuteChanged();
  373. }
  374. }
  375. public void StopFlow()
  376. {
  377. _cts?.Cancel();
  378. }
  379. /// <summary>
  380. /// 仅运行单个节点(不启动后继)
  381. /// </summary>
  382. public async Task RunSingleNodeAsync(FlowNode node)
  383. {
  384. if (IsRunning || IsReadOnly || node == null) return;
  385. _cts = new CancellationTokenSource();
  386. _executor = new FlowExecutor(Graph);
  387. IsRunning = true;
  388. RunFlowCommand?.RaiseCanExecuteChanged();
  389. StopFlowCommand?.RaiseCanExecuteChanged();
  390. _executor.ExecutionCompleted += (success) =>
  391. {
  392. IsRunning = false;
  393. RunFlowCommand?.RaiseCanExecuteChanged();
  394. StopFlowCommand?.RaiseCanExecuteChanged();
  395. };
  396. try
  397. {
  398. await _executor.ExecuteSingleNodeAsync(node, _cts.Token);
  399. }
  400. finally
  401. {
  402. IsRunning = false;
  403. RunFlowCommand?.RaiseCanExecuteChanged();
  404. StopFlowCommand?.RaiseCanExecuteChanged();
  405. }
  406. }
  407. /// <summary>
  408. /// 从指定节点开始运行(向下流转)
  409. /// </summary>
  410. public async Task RunFromNodeAsync(FlowNode node)
  411. {
  412. if (IsRunning || IsReadOnly || node == null) return;
  413. _cts = new CancellationTokenSource();
  414. _executor = new FlowExecutor(Graph);
  415. IsRunning = true;
  416. RunFlowCommand?.RaiseCanExecuteChanged();
  417. StopFlowCommand?.RaiseCanExecuteChanged();
  418. _executor.ExecutionCompleted += (success) =>
  419. {
  420. IsRunning = false;
  421. RunFlowCommand?.RaiseCanExecuteChanged();
  422. StopFlowCommand?.RaiseCanExecuteChanged();
  423. };
  424. try
  425. {
  426. await _executor.ExecuteFromNodeAsync(node, _cts.Token);
  427. }
  428. finally
  429. {
  430. IsRunning = false;
  431. RunFlowCommand?.RaiseCanExecuteChanged();
  432. StopFlowCommand?.RaiseCanExecuteChanged();
  433. }
  434. }
  435. /// <summary>
  436. /// 右键属性 - 显示节点通用属性(非插件编辑器)
  437. /// </summary>
  438. public void ShowNodeProperties(FlowNode node)
  439. {
  440. if (node == null) return;
  441. string oldName = node.NodeName;
  442. if (DialogHelper.EditProperties(node, $"节点属性 - {node.NodeName}"))
  443. {
  444. // 重命名后确保唯一性
  445. if (node.NodeName != oldName)
  446. {
  447. string newName = node.NodeName;
  448. int suffix = 1;
  449. while (Graph.Nodes.Any(n => n != node && n.NodeName == newName))
  450. newName = $"{node.NodeName}_{suffix++}";
  451. if (newName != node.NodeName)
  452. {
  453. node.NodeName = newName;
  454. DialogHelper.Info($"名称已存在,自动改为: {newName}");
  455. }
  456. }
  457. }
  458. }
  459. #endregion
  460. #region 方法
  461. public void CreateNode(double x, double y, NodePluginInfo info)
  462. {
  463. string baseName = info.DisplayName;
  464. int suffix = 1;
  465. string candidate = baseName + suffix;
  466. while (Graph.Nodes.Any(n => n.NodeName == candidate))
  467. {
  468. suffix++;
  469. candidate = baseName + suffix;
  470. }
  471. var node = CreateNodeFromPlugin(candidate,Graph, info, x, y);
  472. // 重名加序号
  473. Graph.AddNode(node);
  474. }
  475. /// <summary>
  476. /// 双击节点 - 打开属性编辑器或插件自定义窗体
  477. /// </summary>
  478. public void OpenNodeEditor(FlowNode node)
  479. {
  480. if (node == null) return;
  481. // 异常分支节点:无属性可编辑,双击不弹任何窗
  482. if (node.Category == NodeCategory.ExceptionBranch) return;
  483. // 只读监控模式:禁止打开编辑器修改节点(允许选中查看结果)
  484. if (IsReadOnly)
  485. {
  486. DialogHelper.Info("运行产品监控中,节点处于只读状态");
  487. return;
  488. }
  489. var desc = PluginLoader.Instance.GetDescriptor(node.ToolName ?? node.PluginId);
  490. if (desc == null) return;
  491. var plugin = PluginLoader.Instance.CreateInstance(desc.DisplayName);
  492. if (plugin == null) return;
  493. if (node.PluginModel is IFlowNodePlugin savedModel)
  494. plugin.GetModel = savedModel.GetModel;
  495. PluginLoader.Instance.SelectFlow = node;
  496. if (desc.HasCustomView)
  497. {
  498. try
  499. {
  500. var view = System.Activator.CreateInstance(desc.Attribute.ViewType) as System.Windows.FrameworkElement;
  501. if (view != null)
  502. {
  503. view.DataContext = plugin.GetModel;
  504. System.Action<object, System.Threading.CancellationToken> execAction = (obj, token) =>
  505. {
  506. // 承载内嵌流程画布的自定义视图(如组合模块编辑器)实现 IPluginRunFeedback:
  507. // 执行前后同步其运行态,使"窗体执行"与"画布内 ▶ 运行"的反馈完全一致
  508. // (执行中:运行/停止按钮切换、画布锁定编辑、子流程节点依次亮灯可见)。
  509. var feedback = view as IPluginRunFeedback;
  510. feedback?.BeginRun();
  511. try
  512. {
  513. // 执行前先把视图当前编辑回写模型(如脚本编辑器),
  514. // 让「窗体执行」运行编辑器所见代码,而非上次「确定」时的旧代码。
  515. // execAction 在后台线程触发,编辑器控件读取必须封送回 UI 线程。
  516. var pendingSave = view as IPluginViewSave;
  517. if (pendingSave != null)
  518. System.Windows.Application.Current?.Dispatcher.Invoke(() => pendingSave.SaveChanges());
  519. plugin.GetModel = obj as BasePluginModel;
  520. var status = plugin.Run(token);
  521. var costMs = plugin.CostTime;
  522. System.Windows.Application.Current?.Dispatcher.Invoke(() =>
  523. {
  524. // 执行完成的 CT 信息优先写入承载窗体底部提示栏(组合模块等实现
  525. // 了 IPluginViewTip 的视图),不再弹全局 Growl;无提示栏时退回 Growl。
  526. var ctText = $"执行完成: {status}, 耗时 {costMs}ms";
  527. if (view is IPluginViewTip tipView)
  528. tipView.ShowTip(ctText);
  529. else
  530. DialogHelper.Info(ctText);
  531. });
  532. }
  533. finally
  534. {
  535. feedback?.EndRun();
  536. }
  537. };
  538. // 用户点击"确定"才回写模型并标记已修改
  539. bool confirmed = DialogHelper.ShowPluginView(view, desc.DisplayName, execAction);
  540. if (confirmed)
  541. {
  542. if (view is IPluginViewSave saveable)
  543. saveable.SaveChanges();
  544. node.PluginModel = plugin;
  545. MarkDirty();
  546. }
  547. }
  548. }
  549. catch (System.Exception ex)
  550. {
  551. DialogHelper.Error($"打开插件窗体失败: {ex.Message}");
  552. }
  553. }
  554. else
  555. {
  556. var model = plugin.GetModel;
  557. System.Action<object, System.Threading.CancellationToken> execAction = (obj, token) =>
  558. {
  559. plugin.GetModel = obj as BasePluginModel;
  560. var status = plugin.Run(token);
  561. System.Windows.Application.Current?.Dispatcher.Invoke(() =>
  562. DialogHelper.Info($"执行完成: {status}, 耗时 {plugin.CostTime}ms"));
  563. };
  564. // 无自定义视图的插件走属性编辑器:设置公式数据源上下文(与自定义视图的
  565. // BtnProperties_Click 行为对齐),否则 FormulaEditor 拿不到前序节点输出候选
  566. // 例外:视觉平台子流程编辑器(Halcon/Vpp/Vm)内编辑子节点时,若已挂容器
  567. // ParentGroupModel(含 ModuleInputs),保留它以便公式树显示容器「输入」变量(&{输入.图像});
  568. // 主流程与组合模块(ToolboxScope=Main)行为完全不变。
  569. bool keepParent = ToolboxScope != FlowToolboxScope.Main
  570. && PluginLoader.Instance.ParentGroupModel != null
  571. && PluginLoader.Instance.ParentGroupModel.GetType().GetProperty("ModuleInputs") != null;
  572. if (!keepParent)
  573. PluginLoader.Instance.ParentGroupModel = model;
  574. try
  575. {
  576. if (DialogHelper.EditProperties(model, $"属性编辑 - {node.NodeName}", execAction))
  577. {
  578. node.PluginModel = plugin;
  579. node.NotifyStatusChanged();
  580. MarkDirty(); // 确认修改 → 标记流程已更改
  581. }
  582. }
  583. finally
  584. {
  585. if (!keepParent)
  586. PluginLoader.Instance.ParentGroupModel = null;
  587. }
  588. }
  589. }
  590. #endregion
  591. #region 导入/导出
  592. /// <summary>
  593. /// 导出当前流程到文件(.aas,二进制格式)
  594. /// </summary>
  595. public bool ExportFlow(string FlowName, string filePath)
  596. {
  597. return FlowFileStore.Save(Graph, filePath);
  598. }
  599. /// <summary>
  600. /// 从文件(.aas)导入流程(异步:后台反序列化 + 画布加载动画)
  601. /// </summary>
  602. public async System.Threading.Tasks.Task<bool> ImportFlowAsync(string FlowName, string filePath)
  603. {
  604. IsLoading = true;
  605. try
  606. {
  607. // 清理当前流程的旧执行结果,防止新节点引用到残留数据
  608. Graph.Registry?.ClearFlow(Graph.GraphId);
  609. // 后台线程反序列化,避免卡 UI(让加载动画正常转动)
  610. FlowGraph Result = await System.Threading.Tasks.Task.Run(() => FlowFileStore.Load(filePath));
  611. if (Result != null)
  612. {
  613. Result.GraphId = Graph.GraphId;
  614. Result.GraphName = Graph.GraphName;
  615. var idMap = new Dictionary<string, string>();
  616. foreach (var item in Result.Nodes)
  617. {
  618. string oldNodeId = item.NodeId;
  619. item.InitializeNode(Graph.GraphId, Graph.GraphName, Graph.Registry);
  620. if (!string.IsNullOrEmpty(oldNodeId))
  621. idMap[oldNodeId] = item.NodeId;
  622. }
  623. // 重映射顶层连接线的 SourceNodeId/TargetNodeId
  624. if (Result.Connections != null && idMap.Count > 0)
  625. {
  626. foreach (var conn in Result.Connections)
  627. {
  628. if (idMap.TryGetValue(conn.SourceNodeId, out var newSrcId))
  629. conn.SourceNodeId = newSrcId;
  630. if (idMap.TryGetValue(conn.TargetNodeId, out var newTgtId))
  631. conn.TargetNodeId = newTgtId;
  632. }
  633. }
  634. // 将导入内容复制到现有 Graph,保持对象引用不变
  635. // (FlowTabItem.Graph、FlowCanvas._graph 等不会失效)
  636. // 释放被替换掉的旧节点插件资源(相机/图像等大对象),配合开头 ClearFlow 彻底清掉旧节点运行数据
  637. foreach (var oldNode in Graph.Nodes)
  638. {
  639. try { oldNode.PluginModel?.Dispose(); } catch { }
  640. }
  641. Graph.Nodes.Clear();
  642. foreach (var n in Result.Nodes)
  643. Graph.Nodes.Add(n);
  644. Graph.Connections.Clear();
  645. foreach (var c in Result.Connections)
  646. Graph.Connections.Add(c);
  647. GraphDataChanged?.Invoke(Graph, null);
  648. MarkDirty(); // 导入的新内容尚未保存
  649. }
  650. return Result != null;
  651. }
  652. finally
  653. {
  654. IsLoading = false;
  655. }
  656. }
  657. #endregion
  658. }
  659. /// <summary>
  660. /// 工具箱作用域。决定一个流程编辑器展示哪一批插件、按什么枚举分组。
  661. /// </summary>
  662. public enum FlowToolboxScope
  663. {
  664. /// <summary>主流程:排除子流程专用节点,按 PluginCategory 分组。</summary>
  665. Main = 0,
  666. /// <summary>Halcon 子流程:只显 Halocn模块 的子流程专用节点,按 VisionPlugin 分组。</summary>
  667. Vision = 1,
  668. /// <summary>VisionPro 子流程:只显 Vpp模块 的子流程专用节点(与 Halcon 平台互相隔离)。</summary>
  669. VisionVpp = 2,
  670. /// <summary>VisionMaster 子流程:只显 Vm模块 的子流程专用节点(与 Halcon/Vpp 平台互相隔离)。</summary>
  671. VisionVm = 3,
  672. }
  673. /// <summary>
  674. /// 工具箱分组
  675. /// </summary>
  676. public class ToolboxGroup
  677. {
  678. public string GroupName { get; set; }
  679. public PluginCategory Category { get; set; }
  680. public string GroupIcon { get; set; }
  681. public List<NodePluginInfo> Items { get; set; } = new List<NodePluginInfo>();
  682. }
  683. internal static class CategoryIconMap
  684. {
  685. public static string GetIcon(PluginCategory category)
  686. {
  687. switch (category)
  688. {
  689. // 视觉模块:相机 Camera
  690. case PluginCategory.视觉模块: return "Camera";
  691. // 硬件模块:主板芯片 Memory
  692. case PluginCategory.硬件模块: return "Memory";
  693. // 通讯模块:信号 Wifi
  694. case PluginCategory.通讯模块: return "Wifi";
  695. // Mes模块:表格 Table
  696. case PluginCategory.Mes模块: return "Table";
  697. // 逻辑判断:分支 CallSplit
  698. case PluginCategory.逻辑判断: return "CallSplit";
  699. // 深度学习:神经网络 Brain
  700. case PluginCategory.深度学习: return "Brain";
  701. // 文件操作:文档 FileDocumentOutline
  702. case PluginCategory.文件操作: return "FileDocumentOutline";
  703. // Vpp模块:Cognex VisionPro,图像对焦检测 ImageFilterCenterFocus
  704. case PluginCategory.Vpp模块: return "ImageFilterCenterFocus";
  705. // Vm模块:VisionMaster,相机光圈 CameraIris
  706. case PluginCategory.Vm模块: return "CameraIris";
  707. // Halocn模块:HALCON,六边形 HexagonOutline
  708. case PluginCategory.Halocn模块: return "HexagonOutline";
  709. // OpenCv模块:OpenCV,代码括号 CodeBraces
  710. case PluginCategory.OpenCv模块: return "CodeBraces";
  711. // 三维视觉:立方体 CubeOutline
  712. case PluginCategory.三维视觉: return "CubeOutline";
  713. default: return "CircleOutline";
  714. }
  715. }
  716. }
  717. /// <summary>
  718. /// VisionPlugin 枚举 → MahApps Material 图标 Kind 名映射(子流程工具箱分组图标用)。
  719. /// Kind 名无法识别时 IconKindConverter 会回退 CircleOutline,不会崩溃。
  720. /// </summary>
  721. internal static class VisionCategoryIconMap
  722. {
  723. public static string GetIcon(VisionPlugin category)
  724. {
  725. switch (category)
  726. {
  727. // 图像处理:滤镜 ImageFilterVintage
  728. case VisionPlugin.图像处理: return "ImageFilterVintage";
  729. // 检测识别:目标框 SelectionEllipseArrowInside
  730. case VisionPlugin.检测识别: return "FeatureSearchOutline";
  731. // 几何测量:尺子 Ruler
  732. case VisionPlugin.几何测量: return "Ruler";
  733. // 坐标标定:坐标系 AxisArrow
  734. case VisionPlugin.坐标标定: return "AxisArrow";
  735. // 深度学习:神经网络 Brain
  736. case VisionPlugin.深度学习: return "Brain";
  737. // 三维视觉:立方体 CubeOutline
  738. case VisionPlugin.三维视觉: return "CubeOutline";
  739. default: return "HexagonOutline";
  740. }
  741. }
  742. }
  743. }