VmFlowEditorView.xaml.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. using System.Collections.Generic;
  2. using System.Linq;
  3. using System.Windows;
  4. using System.Windows.Controls;
  5. using TeamAAS;
  6. using TeamAAS.FlowEditor;
  7. using TeamAAS.FlowEditor.Models;
  8. using TeamAAS.FlowEditor.Plugins;
  9. using TeamAAS.FlowEngine;
  10. using TeamAAS.Vision;
  11. using Plugins.Vm.Models;
  12. namespace Plugins.Vm.Views
  13. {
  14. /// <summary>
  15. /// VM(VisionMaster)流程编辑器视图(UserControl)- 承载在 PluginViewDialog 中,
  16. /// 与 HalconFlowEditorView 完全同构:左侧子流程画布(工具箱作用域=Vm,只显 Vm模块 算子),
  17. /// 右侧大显示窗 + 节点图像筛选 ComboBox(VisionMaster 风格)。
  18. /// 实现 IPluginRunFeedback(窗体"执行"期间同步内嵌画布运行态)、IPluginViewTip(底部提示栏)。
  19. ///
  20. /// 图像来源:订阅 VisionLayerStore 图层总线(VM 算子后续发布图层后自动跟随显示;
  21. /// VM 图像结果键目前经 VmWindow.RefreshImage 按平台对象类型解释,不支持的类型安全跳过)。
  22. /// </summary>
  23. public partial class VmFlowEditorView : UserControl, IPluginViewSave, IPluginRunFeedback, IPluginViewTip
  24. {
  25. private FlowEditorViewModel _vm;
  26. private VmFlowModel _model;
  27. /// <summary>图层筛选条目(SDK):NodeName=null=最新运行;LayerName=null=节点全部图层。</summary>
  28. private readonly List<LayerComboEntry> _layerEntries = new List<LayerComboEntry>();
  29. private bool _suppressComboSync;
  30. public VmFlowEditorView()
  31. {
  32. InitializeComponent();
  33. // 视觉显示框注入子流程编辑器右侧面板(整区替换「节点信息」)——三个平台编辑器统一布局
  34. EditorRoot.Children.Remove(VisionPanel);
  35. SubFlowEditor.ReplaceNodeInfoPanel(VisionPanel);
  36. VisionPanel.Visibility = Visibility.Visible;
  37. Loaded += OnLoaded;
  38. Unloaded += OnUnloaded;
  39. }
  40. private void OnLoaded(object sender, RoutedEventArgs e)
  41. {
  42. if (DataContext is VmFlowModel model)
  43. {
  44. _model = model;
  45. model.EnsureSubGraph();
  46. TxtTitle.Text = model.ToolName ?? "VM流程";
  47. // 关键:工具箱作用域=Vm → 只显示 IsSubFlowNode 的 Vm模块 算子,按 VisionPlugin 分组
  48. _vm = new FlowEditorViewModel(model.SubGraph, isSubFlowEditor: true, toolboxScope: FlowToolboxScope.VisionVm);
  49. SubFlowEditor.DataContext = _vm;
  50. _vm.GraphDataChanged += _vm_GraphDataChanged;
  51. _vm.PropertyChanged += _vm_PropertyChanged;
  52. // 大显示窗(VM 原生渲染控件):独立面板随窗体尺寸自适应
  53. var preview = new VmWindow();
  54. preview.SetCaption("VM 显示");
  55. DisplayHost.Content = preview;
  56. // 图像总线挂接:节点发布图像 → 重建筛选项 + 跟随显示
  57. VisionLayerStore.Instance.LayerPublished += OnLayerPublished;
  58. VisionLayerStore.Instance.FlowCleared += OnFlowCleared;
  59. ResetLayerCombo();
  60. // 让子节点的公式树能显示容器「输入」变量(如 &{输入.图像})
  61. PluginLoader.Instance.ParentGroupModel = model;
  62. HookHoverHints(this);
  63. }
  64. }
  65. private void OnUnloaded(object sender, RoutedEventArgs e)
  66. {
  67. PluginLoader.Instance.SetGroupPluginModel = null;
  68. if (ReferenceEquals(PluginLoader.Instance.ParentGroupModel, _model))
  69. PluginLoader.Instance.ParentGroupModel = null;
  70. VisionLayerStore.Instance.LayerPublished -= OnLayerPublished;
  71. VisionLayerStore.Instance.FlowCleared -= OnFlowCleared;
  72. DisplayHost.Content = null;
  73. }
  74. private void _vm_GraphDataChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
  75. {
  76. if (DataContext is VmFlowModel model)
  77. {
  78. if (sender is FlowGraph flowGraph)
  79. model.SubGraph = flowGraph;
  80. }
  81. }
  82. /// <summary>画布点选节点 → 图像筛选自动切到该节点(点节点看该节点图)。</summary>
  83. private void _vm_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
  84. {
  85. if (e == null || e.PropertyName != "SelectedNode") return;
  86. var node = _vm?.SelectedNode;
  87. if (node == null || string.IsNullOrEmpty(node.NodeName)) return;
  88. var index = _layerEntries.FindIndex(x => x.NodeName == node.NodeName && x.LayerName == null);
  89. if (index < 0)
  90. index = 0;
  91. if (LayerCombo.SelectedIndex != index)
  92. {
  93. _suppressComboSync = true;
  94. LayerCombo.SelectedIndex = index;
  95. _suppressComboSync = false;
  96. }
  97. ApplyLayerSelection();
  98. }
  99. #region 图层筛选与显示
  100. /// <summary>当前选中的条目;越界/未选返回 null。</summary>
  101. private LayerComboEntry SelectedEntry()
  102. {
  103. var i = LayerCombo.SelectedIndex;
  104. if (i < 0 || i >= _layerEntries.Count) return null;
  105. return _layerEntries[i];
  106. }
  107. private void ResetLayerCombo()
  108. {
  109. _layerEntries.Clear();
  110. _layerEntries.Add(new LayerComboEntry { Display = "最新运行" });
  111. _suppressComboSync = true;
  112. LayerCombo.ItemsSource = _layerEntries.Select(e => e.Display).ToList();
  113. LayerCombo.SelectedIndex = 0;
  114. _suppressComboSync = false;
  115. RunStatusText.Text = "等待运行…(节点运行后自动显示其图像)";
  116. }
  117. private static System.DateTime _lastMismatchLog = System.DateTime.MinValue;
  118. /// <summary>图像发布回调:重建筛选项并按"值"保留选中;正在看该节点(或最新运行)时实时刷新显示。
  119. /// 发布的流程 Id 与编辑器不一致时(旧流程文件图 Id 漂移)不再直接丢弃 —— 记录日志并按节点名兜底显示。</summary>
  120. private void OnLayerPublished(LayerPublishArgs args)
  121. {
  122. if (args == null) return;
  123. var flowId = _model?.SubGraph?.GraphId;
  124. var matched = string.Equals(args.FlowId, flowId, System.StringComparison.OrdinalIgnoreCase);
  125. if (!matched && (System.DateTime.Now - _lastMismatchLog).TotalSeconds > 5)
  126. {
  127. _lastMismatchLog = System.DateTime.Now;
  128. TeamAAS.AppLogger.Warning($"图像发布流程Id与编辑器不一致: 发布={args.FlowId} 节点={args.NodeName} 编辑器期望={flowId}(已按节点名兜底显示)", "Vm编辑器");
  129. }
  130. try
  131. {
  132. System.Windows.Application.Current?.Dispatcher.Invoke(() =>
  133. {
  134. RebuildLayerEntries();
  135. var current = SelectedEntry();
  136. bool follow = current == null || current.NodeName == null ||
  137. string.Equals(current.NodeName, args.NodeName, System.StringComparison.OrdinalIgnoreCase);
  138. if (follow || !matched) ApplyLayerSelection();
  139. });
  140. }
  141. catch { }
  142. }
  143. private void OnFlowCleared(string flowId, string nodeName)
  144. {
  145. try
  146. {
  147. System.Windows.Application.Current?.Dispatcher.Invoke(() =>
  148. {
  149. RebuildLayerEntries();
  150. ApplyLayerSelection();
  151. });
  152. }
  153. catch { }
  154. }
  155. /// <summary>重建筛选条目:[最新运行] + 每节点(全部图层)+ 每图层。按 (节点,图层) 值保留当前选中。
  156. /// 本流程没有发布记录时(图 Id 漂移等)兜底展示全部发布,保证节点条目始终可见。</summary>
  157. private void RebuildLayerEntries()
  158. {
  159. var flowId = _model?.SubGraph?.GraphId;
  160. var own = VisionLayerStore.Instance.GetFlowLayers(flowId ?? "");
  161. var source = own.Count > 0 ? own : VisionLayerStore.Instance.GetAllLatest();
  162. var current = SelectedEntry();
  163. var entries = LayerComboHelper.BuildEntries(flowId ?? "", source);
  164. _suppressComboSync = true;
  165. _layerEntries.Clear();
  166. foreach (var e in entries)
  167. _layerEntries.Add(e);
  168. LayerCombo.ItemsSource = _layerEntries.Select(e => e.Display).ToList();
  169. var index = current == null
  170. ? 0
  171. : _layerEntries.FindIndex(x => x.NodeName == current.NodeName && x.LayerName == current.LayerName);
  172. LayerCombo.SelectedIndex = index >= 0 ? index : 0;
  173. _suppressComboSync = false;
  174. }
  175. private void LayerCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)
  176. {
  177. if (_suppressComboSync) return;
  178. ApplyLayerSelection();
  179. }
  180. /// <summary>按当前筛选显示:最新运行=最后一次发布整体;节点=底图+全部叠加;
  181. /// 节点.图层=图像层单独显示、其他叠加层自动附带该节点原图。
  182. /// VmWindow.RefreshImage 按平台对象类型(IVmModule/IImageData)解释,其余类型安全跳过。</summary>
  183. private void ApplyLayerSelection()
  184. {
  185. var preview = DisplayHost.Content as VmWindow;
  186. if (preview == null) return;
  187. var flowId = _model?.SubGraph?.GraphId;
  188. var entry = SelectedEntry();
  189. var store = VisionLayerStore.Instance;
  190. LayerPublishArgs args;
  191. if (entry == null || entry.NodeName == null)
  192. {
  193. var all = store.GetFlowLayers(flowId ?? "");
  194. if (all.Count == 0) all = store.GetAllLatest();
  195. if (all.Count == 0)
  196. {
  197. RunStatusText.Text = "等待运行…(节点运行后自动显示其图像)";
  198. return;
  199. }
  200. args = all[all.Count - 1];
  201. }
  202. else
  203. {
  204. // 图 Id 漂移兜底:按流程查不到时按节点名查(跨流程最近一次发布)
  205. args = store.GetLatest(flowId ?? "", entry.NodeName)
  206. ?? store.GetLatestByNode(entry.NodeName);
  207. if (args == null || args.Layers.Count == 0)
  208. {
  209. RunStatusText.Text = $"{entry.NodeName} · 尚未运行";
  210. return;
  211. }
  212. }
  213. var layers = LayerComboHelper.ResolveLayers(args, entry?.LayerName, flowId);
  214. var content = layers.FirstOrDefault(l => l.Kind == LayerKind.Image)?.Content;
  215. if (content != null)
  216. preview.RefreshImage(content);
  217. RunStatusText.Text = $"{args.NodeName} · {(entry?.LayerName ?? $"全部 {layers.Count} 图层")} · #{args.RunStamp}";
  218. }
  219. #endregion
  220. /// <summary>窗口关闭前保存更改回模型</summary>
  221. public void SaveChanges()
  222. {
  223. if (DataContext is VmFlowModel model && _vm != null)
  224. model.SubGraph = _vm.Graph;
  225. }
  226. /// <summary>窗体"执行"开始:内嵌子流程编辑器进入运行态(UI 线程同步,避免跨线程访问控件)</summary>
  227. public void BeginRun()
  228. {
  229. if (_vm == null) return;
  230. System.Windows.Application.Current?.Dispatcher.Invoke(() => { _vm.IsRunning = true; });
  231. }
  232. /// <summary>窗体"执行"结束(含停止/异常):恢复非运行态</summary>
  233. public void EndRun()
  234. {
  235. if (_vm == null) return;
  236. System.Windows.Application.Current?.Dispatcher.Invoke(() => { _vm.IsRunning = false; });
  237. }
  238. #region IPluginViewTip 底部提示栏
  239. private readonly HashSet<System.Windows.DependencyObject> _hoverHooked =
  240. new HashSet<System.Windows.DependencyObject>();
  241. /// <summary>承载本视图的基窗体(PluginViewDialog),未承载时为 null</summary>
  242. private TeamAAS.Dialogs.Dialogs.PluginViewDialog HostDialog =>
  243. System.Windows.Window.GetWindow(this) as TeamAAS.Dialogs.Dialogs.PluginViewDialog;
  244. public void ShowTip(string message) => HostDialog?.ShowTip(message);
  245. public void ShowHoverHint(string message) => HostDialog?.ShowHoverHint(message);
  246. private void HoverEl_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
  247. {
  248. if ((sender as System.Windows.FrameworkElement)?.Tag is string tip && !string.IsNullOrWhiteSpace(tip))
  249. ShowHoverHint(tip);
  250. }
  251. private void HoverEl_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e) => ShowHoverHint(null);
  252. /// <summary>扫描视觉树,给所有 Tag 为非空字符串的按钮挂上悬停提示。</summary>
  253. private void HookHoverHints(System.Windows.DependencyObject root)
  254. {
  255. if (root == null) return;
  256. var count = System.Windows.Media.VisualTreeHelper.GetChildrenCount(root);
  257. for (int i = 0; i < count; i++)
  258. {
  259. var child = System.Windows.Media.VisualTreeHelper.GetChild(root, i);
  260. if (child is System.Windows.Controls.Primitives.ButtonBase btn
  261. && !_hoverHooked.Contains(btn)
  262. && btn.Tag is string tip && !string.IsNullOrWhiteSpace(tip))
  263. {
  264. btn.MouseEnter += HoverEl_MouseEnter;
  265. btn.MouseLeave += HoverEl_MouseLeave;
  266. _hoverHooked.Add(btn);
  267. }
  268. HookHoverHints(child);
  269. }
  270. }
  271. #endregion
  272. private void BtnProperties_Click(object sender, RoutedEventArgs e)
  273. {
  274. if (DataContext is VmFlowModel model)
  275. {
  276. // 编辑容器自身的输入/输出映射:输入用 InputSourceDataProvider(查主流程前置节点),
  277. // 输出用 PluginDataProvider(查子流程节点)
  278. PluginLoader.Instance.SetGroupPluginModel = model.SubGraph;
  279. PluginLoader.Instance.ParentGroupModel = model;
  280. if (DialogHelper.EditProperties(model, $"VM 流程属性 - {model.NodeName}"))
  281. {
  282. TxtTitle.Text = model.ToolName ?? "VM流程";
  283. DialogHelper.Info($"已更新 VM 流程属性: {model.ToolName}");
  284. }
  285. PluginLoader.Instance.SetGroupPluginModel = null;
  286. // 保留 ParentGroupModel = model,供子节点公式树继续显示「输入」变量
  287. }
  288. }
  289. }
  290. }