using Prism.Commands; using Prism.Mvvm; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters.Binary; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows; using TeamAAS.FlowEditor.Execution; using TeamAAS.FlowEditor.Models; using TeamAAS.FlowEditor.Plugins; using TeamAAS.FlowEngine.Execution; using TeamAAS.FlowEngine; namespace TeamAAS.FlowEditor { /// /// 流程编辑器 ViewModel(单流程) /// public class FlowEditorViewModel : TeamAAS.BindableBase { #region 属性 private FlowGraph _graph; public FlowGraph Graph { get => _graph; set { if (SetProperty(ref _graph, value)) HookGraph(_graph); } } public PropertyChangedEventHandler GraphDataChanged { get; set; } #region 脏标记(决定"切换产品时是否提示保存") private bool _isDirty; /// 流程是否被修改过(点击保存或加载后恢复为 false) public bool IsDirty { get => _isDirty; private set => SetProperty(ref _isDirty, value); } /// 标记流程已修改(节点增删/移动/连线/属性编辑等) public void MarkDirty() { IsDirty = true; } /// 标记流程已保存/刚加载(清除修改标记) public void MarkClean() { IsDirty = false; } private bool _dirtyTrackingHooked; private void HookGraph(FlowGraph graph) { if (graph == null) return; if (_dirtyTrackingHooked) { graph.Nodes.CollectionChanged -= OnNodesCollectionChanged; graph.Connections.CollectionChanged -= OnConnectionsCollectionChanged; foreach (var node in graph.Nodes) node.PropertyChanged -= OnNodePropertyChanged; } graph.Nodes.CollectionChanged += OnNodesCollectionChanged; graph.Connections.CollectionChanged += OnConnectionsCollectionChanged; foreach (var node in graph.Nodes) node.PropertyChanged += OnNodePropertyChanged; _dirtyTrackingHooked = true; } private void OnNodesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { if (e.OldItems != null) foreach (FlowNode node in e.OldItems) node.PropertyChanged -= OnNodePropertyChanged; if (e.NewItems != null) foreach (FlowNode node in e.NewItems) node.PropertyChanged += OnNodePropertyChanged; MarkDirty(); } private void OnConnectionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { MarkDirty(); } // 只有"用户编辑类"属性才计入脏标记;运行状态(Status/CostTime/结果等)不算 private static readonly HashSet EditProperties = new HashSet { nameof(FlowNode.X), nameof(FlowNode.Y), nameof(FlowNode.NodeName), nameof(FlowNode.IsEnabled), nameof(FlowNode.IsSkipWarning), nameof(FlowNode.NodeWidth), nameof(FlowNode.NodeHeight), }; private void OnNodePropertyChanged(object sender, PropertyChangedEventArgs e) { if (EditProperties.Contains(e.PropertyName)) MarkDirty(); } #endregion private bool _isSubFlowEditor; /// /// 是否为子流程编辑器 /// public bool IsSubFlowEditor { get => _isSubFlowEditor; set { if (SetProperty(ref _isSubFlowEditor, value)) { RunFlowCommand?.RaiseCanExecuteChanged(); } } } public ObservableCollection ToolboxGroups { get; private set; } private FlowNode _selectedNode; public FlowNode SelectedNode { get => _selectedNode; set { if (SetProperty(ref _selectedNode, value)) { // 切换节点时,自动选中最新历史记录(倒序,最新在 index 0) if (_selectedNode != null && _selectedNode.ExecutionHistory?.Count > 0) _selectedNode.SelectedHistoryEntry = _selectedNode.ExecutionHistory[0]; } } } private double _zoom = 1.0; public double Zoom { get => _zoom; set => SetProperty(ref _zoom, value); } private bool _isRunning; public bool IsRunning { get => _isRunning; set => SetProperty(ref _isRunning, value); } private bool _isReadOnly; /// /// 只读监控模式(运行产品运行中):可选中节点查看结果,禁止一切修改与编辑器内运行。 /// public bool IsReadOnly { get => _isReadOnly; private set => SetProperty(ref _isReadOnly, value); } /// 由 Shell/产品级状态设置只读模式 public void SetReadOnly(bool value) { IsReadOnly = value; RunFlowCommand?.RaiseCanExecuteChanged(); StopFlowCommand?.RaiseCanExecuteChanged(); } private bool _isLoading; /// 导入流程时是否正在加载(画布显示转圈动画) public bool IsLoading { get => _isLoading; set => SetProperty(ref _isLoading, value); } private double _canvasWidth = 3500; /// 画布宽度 public double CanvasWidth { get => _canvasWidth; set => SetProperty(ref _canvasWidth, value); } private double _canvasHeight = 3500; /// 画布高度 public double CanvasHeight { get => _canvasHeight; set => SetProperty(ref _canvasHeight, value); } #endregion #region 命令 [field: NonSerialized] public DelegateCommand ClearSelectionCommand { get; private set; } public DelegateCommand DeleteSelectedCommand { get; private set; } public DelegateCommand RunFlowCommand { get; private set; } public DelegateCommand StopFlowCommand { get; private set; } public DelegateCommand ImportFlowCommand { get; private set; } public DelegateCommand ExportFlowCommand { get; private set; } #endregion #region 节点创建 /// /// 从插件描述符创建节点 /// public static FlowNode CreateNodeFromPlugin(string NodeName,FlowGraph flowGraph, NodePluginInfo info, double x, double y) { var desc = PluginLoader.Instance.GetDescriptor(info.PluginId); var node = new FlowNode { NodeName = NodeName, Category = desc?.NodeShape ?? info.Category, FlowName = flowGraph.GraphName, FlowId = flowGraph.GraphId, PluginId = info.PluginId, X = x, Y = y, IconGeometry = info.IconGeometry }; // 创建插件实例并获取默认模型 var plugin = PluginLoader.Instance.CreateInstance(info.DisplayName); if (plugin != null) { plugin.GetModel.NodeId = node.NodeId; plugin.GetModel.ToolName = info.DisplayName; plugin.GetModel.NodeName = node.NodeName; plugin.GetModel.PluginId = node.PluginId; plugin.GetModel.FlowName = flowGraph.GraphName; plugin.GetModel.FlowId = flowGraph.GraphId; node.Registry = flowGraph.Registry; plugin.Registry = flowGraph.Registry; // InitRun 前需就绪,使默认输出注册到正确的注册表 // 新建节点立即执行一次初始化运行:触发 DeclareOutputs 生成默认输出、注册到 ResultRegistry(供绑定树可见)、 // 并填充 LastResults;随后 PluginModel setter 会用 LastResults 刷新节点输出列表。 // 修复:从工具箱新拖入的节点不显示输出(此前仅 InitializeNode 加载/粘贴路径才 InitRun)。 plugin.InitRun(); node.PluginModel = plugin; } return node; } #endregion #region 构造函数 public FlowEditorViewModel(FlowGraph graph = null, bool isSubFlowEditor = false, FlowToolboxScope toolboxScope = FlowToolboxScope.Main) { Graph = graph ?? new FlowGraph { GraphName = "流程" }; IsSubFlowEditor = isSubFlowEditor; ToolboxScope = toolboxScope; InitCommands(); MarkClean(); } /// /// 工具箱作用域:决定本编辑器工具箱展示哪一批插件、按什么分组。 /// Main=主流程(排除 IsSubFlowNode 子节点,按 PluginCategory 分组); /// Halcon=Halcon 子流程(只显 IsSubFlowNode 子节点,按 VisionPlugin 分组)。 /// public FlowToolboxScope ToolboxScope { get; private set; } = FlowToolboxScope.Main; private void InitCommands() { // 从 PluginLoader 构建工具箱(按作用域过滤/分组) ToolboxGroups = new ObservableCollection(); var infos = PluginLoader.Instance.GetAllPluginInfos(); if (ToolboxScope != FlowToolboxScope.Main) { // 平台子流程编辑器(Halcon/Vpp/Vm):只取标记为子流程节点的插件,并按平台 // (PluginCategory: Halocn模块/Vpp模块/Vm模块)隔离 —— 各平台只见到自己的算子, // 平台内按 VisionPlugin 枚举分组 PluginCategory platform; switch (ToolboxScope) { case FlowToolboxScope.VisionVpp: platform = PluginCategory.Vpp模块; break; case FlowToolboxScope.VisionVm: platform = PluginCategory.Vm模块; break; default: platform = PluginCategory.Halocn模块; break; } var subInfos = infos.Where(i => i.IsSubFlowNode && i.Group == platform).ToList(); foreach (var g in subInfos.GroupBy(i => i.VisionCategory).OrderBy(g => g.Key)) { ToolboxGroups.Add(new ToolboxGroup { GroupName = g.Key.ToString(), Category = platform, GroupIcon = VisionCategoryIconMap.GetIcon(g.Key), Items = g.ToList() }); } } else { // 主流程:排除子流程专用节点(保持既有行为——现有插件 IsSubFlowNode 均为 false,不受影响) var mainInfos = infos.Where(i => !i.IsSubFlowNode).ToList(); foreach (var g in mainInfos.GroupBy(i => i.Group)) { ToolboxGroups.Add(new ToolboxGroup { GroupName = g.Key.ToString(), Category = g.Key, GroupIcon = CategoryIconMap.GetIcon(g.Key), Items = g.ToList() }); } } ClearSelectionCommand = new DelegateCommand(() => SelectedNode = null); DeleteSelectedCommand = new DelegateCommand(() => { if (SelectedNode != null) { Graph.RemoveNode(SelectedNode.NodeId); SelectedNode = null; } }); RunFlowCommand = new DelegateCommand(async () => await RunFlowAsync(), () => !IsRunning && !IsSubFlowEditor && !IsReadOnly); StopFlowCommand = new DelegateCommand(() => StopFlow(), () => IsRunning); // 导入/导出统一走命令(工具栏按钮与 Shell 右键菜单共用,避免重复代码) ImportFlowCommand = new DelegateCommand(async () => { if (IsReadOnly) { DialogHelper.Info("运行产品监控中,无法导入流程"); return; } var dlg = new Microsoft.Win32.OpenFileDialog { Filter = "流程文件|*.aas|所有文件|*.*", Title = "导入流程" }; if (dlg.ShowDialog() == true) { if (Graph != null && await ImportFlowAsync(Graph.GraphName, dlg.FileName) == true) DialogHelper.Success("导入成功"); else DialogHelper.Error("导入失败"); } }); ExportFlowCommand = new DelegateCommand(() => { if (Graph == null) return; var dlg = new Microsoft.Win32.SaveFileDialog { Filter = "流程文件|*.aas|所有文件|*.*", Title = "导出流程", FileName = Graph.GraphName + ".aas" }; if (dlg.ShowDialog() == true) { if (ExportFlow(Graph.GraphName, dlg.FileName) == true) DialogHelper.Success("导出成功"); else DialogHelper.Error("导出失败"); } }); } #endregion #region 执行 private CancellationTokenSource _cts; private FlowExecutor _executor; public async Task RunFlowAsync() { if (IsRunning) return; _cts = new CancellationTokenSource(); _executor = new FlowExecutor(Graph); IsRunning = true; RunFlowCommand.RaiseCanExecuteChanged(); StopFlowCommand.RaiseCanExecuteChanged(); _executor.ExecutionCompleted += (success) => { IsRunning = false; RunFlowCommand.RaiseCanExecuteChanged(); StopFlowCommand.RaiseCanExecuteChanged(); }; try { await _executor.ExecuteAsync(_cts.Token); } finally { // 兜底:即使 ExecutionCompleted 因 UI 线程繁忙被延迟、或执行器抛异常, // 也保证运行态复位,避免"执行完仍被锁住、无法再次运行"。 IsRunning = false; RunFlowCommand?.RaiseCanExecuteChanged(); StopFlowCommand?.RaiseCanExecuteChanged(); } } public void StopFlow() { _cts?.Cancel(); } /// /// 仅运行单个节点(不启动后继) /// public async Task RunSingleNodeAsync(FlowNode node) { if (IsRunning || IsReadOnly || node == null) return; _cts = new CancellationTokenSource(); _executor = new FlowExecutor(Graph); IsRunning = true; RunFlowCommand?.RaiseCanExecuteChanged(); StopFlowCommand?.RaiseCanExecuteChanged(); _executor.ExecutionCompleted += (success) => { IsRunning = false; RunFlowCommand?.RaiseCanExecuteChanged(); StopFlowCommand?.RaiseCanExecuteChanged(); }; try { await _executor.ExecuteSingleNodeAsync(node, _cts.Token); } finally { IsRunning = false; RunFlowCommand?.RaiseCanExecuteChanged(); StopFlowCommand?.RaiseCanExecuteChanged(); } } /// /// 从指定节点开始运行(向下流转) /// public async Task RunFromNodeAsync(FlowNode node) { if (IsRunning || IsReadOnly || node == null) return; _cts = new CancellationTokenSource(); _executor = new FlowExecutor(Graph); IsRunning = true; RunFlowCommand?.RaiseCanExecuteChanged(); StopFlowCommand?.RaiseCanExecuteChanged(); _executor.ExecutionCompleted += (success) => { IsRunning = false; RunFlowCommand?.RaiseCanExecuteChanged(); StopFlowCommand?.RaiseCanExecuteChanged(); }; try { await _executor.ExecuteFromNodeAsync(node, _cts.Token); } finally { IsRunning = false; RunFlowCommand?.RaiseCanExecuteChanged(); StopFlowCommand?.RaiseCanExecuteChanged(); } } /// /// 右键属性 - 显示节点通用属性(非插件编辑器) /// public void ShowNodeProperties(FlowNode node) { if (node == null) return; string oldName = node.NodeName; if (DialogHelper.EditProperties(node, $"节点属性 - {node.NodeName}")) { // 重命名后确保唯一性 if (node.NodeName != oldName) { string newName = node.NodeName; int suffix = 1; while (Graph.Nodes.Any(n => n != node && n.NodeName == newName)) newName = $"{node.NodeName}_{suffix++}"; if (newName != node.NodeName) { node.NodeName = newName; DialogHelper.Info($"名称已存在,自动改为: {newName}"); } } } } #endregion #region 方法 public void CreateNode(double x, double y, NodePluginInfo info) { string baseName = info.DisplayName; int suffix = 1; string candidate = baseName + suffix; while (Graph.Nodes.Any(n => n.NodeName == candidate)) { suffix++; candidate = baseName + suffix; } var node = CreateNodeFromPlugin(candidate,Graph, info, x, y); // 重名加序号 Graph.AddNode(node); } /// /// 双击节点 - 打开属性编辑器或插件自定义窗体 /// public void OpenNodeEditor(FlowNode node) { if (node == null) return; // 异常分支节点:无属性可编辑,双击不弹任何窗 if (node.Category == NodeCategory.ExceptionBranch) return; // 只读监控模式:禁止打开编辑器修改节点(允许选中查看结果) if (IsReadOnly) { DialogHelper.Info("运行产品监控中,节点处于只读状态"); return; } var desc = PluginLoader.Instance.GetDescriptor(node.ToolName ?? node.PluginId); if (desc == null) return; var plugin = PluginLoader.Instance.CreateInstance(desc.DisplayName); if (plugin == null) return; if (node.PluginModel is IFlowNodePlugin savedModel) plugin.GetModel = savedModel.GetModel; PluginLoader.Instance.SelectFlow = node; if (desc.HasCustomView) { try { var view = System.Activator.CreateInstance(desc.Attribute.ViewType) as System.Windows.FrameworkElement; if (view != null) { view.DataContext = plugin.GetModel; System.Action execAction = (obj, token) => { // 承载内嵌流程画布的自定义视图(如组合模块编辑器)实现 IPluginRunFeedback: // 执行前后同步其运行态,使"窗体执行"与"画布内 ▶ 运行"的反馈完全一致 // (执行中:运行/停止按钮切换、画布锁定编辑、子流程节点依次亮灯可见)。 var feedback = view as IPluginRunFeedback; feedback?.BeginRun(); try { // 执行前先把视图当前编辑回写模型(如脚本编辑器), // 让「窗体执行」运行编辑器所见代码,而非上次「确定」时的旧代码。 // execAction 在后台线程触发,编辑器控件读取必须封送回 UI 线程。 var pendingSave = view as IPluginViewSave; if (pendingSave != null) System.Windows.Application.Current?.Dispatcher.Invoke(() => pendingSave.SaveChanges()); plugin.GetModel = obj as BasePluginModel; var status = plugin.Run(token); var costMs = plugin.CostTime; System.Windows.Application.Current?.Dispatcher.Invoke(() => { // 执行完成的 CT 信息优先写入承载窗体底部提示栏(组合模块等实现 // 了 IPluginViewTip 的视图),不再弹全局 Growl;无提示栏时退回 Growl。 var ctText = $"执行完成: {status}, 耗时 {costMs}ms"; if (view is IPluginViewTip tipView) tipView.ShowTip(ctText); else DialogHelper.Info(ctText); }); } finally { feedback?.EndRun(); } }; // 用户点击"确定"才回写模型并标记已修改 bool confirmed = DialogHelper.ShowPluginView(view, desc.DisplayName, execAction); if (confirmed) { if (view is IPluginViewSave saveable) saveable.SaveChanges(); node.PluginModel = plugin; MarkDirty(); } } } catch (System.Exception ex) { DialogHelper.Error($"打开插件窗体失败: {ex.Message}"); } } else { var model = plugin.GetModel; System.Action execAction = (obj, token) => { plugin.GetModel = obj as BasePluginModel; var status = plugin.Run(token); System.Windows.Application.Current?.Dispatcher.Invoke(() => DialogHelper.Info($"执行完成: {status}, 耗时 {plugin.CostTime}ms")); }; // 无自定义视图的插件走属性编辑器:设置公式数据源上下文(与自定义视图的 // BtnProperties_Click 行为对齐),否则 FormulaEditor 拿不到前序节点输出候选 // 例外:视觉平台子流程编辑器(Halcon/Vpp/Vm)内编辑子节点时,若已挂容器 // ParentGroupModel(含 ModuleInputs),保留它以便公式树显示容器「输入」变量(&{输入.图像}); // 主流程与组合模块(ToolboxScope=Main)行为完全不变。 bool keepParent = ToolboxScope != FlowToolboxScope.Main && PluginLoader.Instance.ParentGroupModel != null && PluginLoader.Instance.ParentGroupModel.GetType().GetProperty("ModuleInputs") != null; if (!keepParent) PluginLoader.Instance.ParentGroupModel = model; try { if (DialogHelper.EditProperties(model, $"属性编辑 - {node.NodeName}", execAction)) { node.PluginModel = plugin; node.NotifyStatusChanged(); MarkDirty(); // 确认修改 → 标记流程已更改 } } finally { if (!keepParent) PluginLoader.Instance.ParentGroupModel = null; } } } #endregion #region 导入/导出 /// /// 导出当前流程到文件(.aas,二进制格式) /// public bool ExportFlow(string FlowName, string filePath) { return FlowFileStore.Save(Graph, filePath); } /// /// 从文件(.aas)导入流程(异步:后台反序列化 + 画布加载动画) /// public async System.Threading.Tasks.Task ImportFlowAsync(string FlowName, string filePath) { IsLoading = true; try { // 清理当前流程的旧执行结果,防止新节点引用到残留数据 Graph.Registry?.ClearFlow(Graph.GraphId); // 后台线程反序列化,避免卡 UI(让加载动画正常转动) FlowGraph Result = await System.Threading.Tasks.Task.Run(() => FlowFileStore.Load(filePath)); if (Result != null) { Result.GraphId = Graph.GraphId; Result.GraphName = Graph.GraphName; var idMap = new Dictionary(); foreach (var item in Result.Nodes) { string oldNodeId = item.NodeId; item.InitializeNode(Graph.GraphId, Graph.GraphName, Graph.Registry); if (!string.IsNullOrEmpty(oldNodeId)) idMap[oldNodeId] = item.NodeId; } // 重映射顶层连接线的 SourceNodeId/TargetNodeId if (Result.Connections != null && idMap.Count > 0) { foreach (var conn in Result.Connections) { if (idMap.TryGetValue(conn.SourceNodeId, out var newSrcId)) conn.SourceNodeId = newSrcId; if (idMap.TryGetValue(conn.TargetNodeId, out var newTgtId)) conn.TargetNodeId = newTgtId; } } // 将导入内容复制到现有 Graph,保持对象引用不变 // (FlowTabItem.Graph、FlowCanvas._graph 等不会失效) // 释放被替换掉的旧节点插件资源(相机/图像等大对象),配合开头 ClearFlow 彻底清掉旧节点运行数据 foreach (var oldNode in Graph.Nodes) { try { oldNode.PluginModel?.Dispose(); } catch { } } Graph.Nodes.Clear(); foreach (var n in Result.Nodes) Graph.Nodes.Add(n); Graph.Connections.Clear(); foreach (var c in Result.Connections) Graph.Connections.Add(c); GraphDataChanged?.Invoke(Graph, null); MarkDirty(); // 导入的新内容尚未保存 } return Result != null; } finally { IsLoading = false; } } #endregion } /// /// 工具箱作用域。决定一个流程编辑器展示哪一批插件、按什么枚举分组。 /// public enum FlowToolboxScope { /// 主流程:排除子流程专用节点,按 PluginCategory 分组。 Main = 0, /// Halcon 子流程:只显 Halocn模块 的子流程专用节点,按 VisionPlugin 分组。 Vision = 1, /// VisionPro 子流程:只显 Vpp模块 的子流程专用节点(与 Halcon 平台互相隔离)。 VisionVpp = 2, /// VisionMaster 子流程:只显 Vm模块 的子流程专用节点(与 Halcon/Vpp 平台互相隔离)。 VisionVm = 3, } /// /// 工具箱分组 /// public class ToolboxGroup { public string GroupName { get; set; } public PluginCategory Category { get; set; } public string GroupIcon { get; set; } public List Items { get; set; } = new List(); } internal static class CategoryIconMap { public static string GetIcon(PluginCategory category) { switch (category) { // 视觉模块:相机 Camera case PluginCategory.视觉模块: return "Camera"; // 硬件模块:主板芯片 Memory case PluginCategory.硬件模块: return "Memory"; // 通讯模块:信号 Wifi case PluginCategory.通讯模块: return "Wifi"; // Mes模块:表格 Table case PluginCategory.Mes模块: return "Table"; // 逻辑判断:分支 CallSplit case PluginCategory.逻辑判断: return "CallSplit"; // 深度学习:神经网络 Brain case PluginCategory.深度学习: return "Brain"; // 文件操作:文档 FileDocumentOutline case PluginCategory.文件操作: return "FileDocumentOutline"; // Vpp模块:Cognex VisionPro,图像对焦检测 ImageFilterCenterFocus case PluginCategory.Vpp模块: return "ImageFilterCenterFocus"; // Vm模块:VisionMaster,相机光圈 CameraIris case PluginCategory.Vm模块: return "CameraIris"; // Halocn模块:HALCON,六边形 HexagonOutline case PluginCategory.Halocn模块: return "HexagonOutline"; // OpenCv模块:OpenCV,代码括号 CodeBraces case PluginCategory.OpenCv模块: return "CodeBraces"; // 三维视觉:立方体 CubeOutline case PluginCategory.三维视觉: return "CubeOutline"; default: return "CircleOutline"; } } } /// /// VisionPlugin 枚举 → MahApps Material 图标 Kind 名映射(子流程工具箱分组图标用)。 /// Kind 名无法识别时 IconKindConverter 会回退 CircleOutline,不会崩溃。 /// internal static class VisionCategoryIconMap { public static string GetIcon(VisionPlugin category) { switch (category) { // 图像处理:滤镜 ImageFilterVintage case VisionPlugin.图像处理: return "ImageFilterVintage"; // 检测识别:目标框 SelectionEllipseArrowInside case VisionPlugin.检测识别: return "FeatureSearchOutline"; // 几何测量:尺子 Ruler case VisionPlugin.几何测量: return "Ruler"; // 坐标标定:坐标系 AxisArrow case VisionPlugin.坐标标定: return "AxisArrow"; // 深度学习:神经网络 Brain case VisionPlugin.深度学习: return "Brain"; // 三维视觉:立方体 CubeOutline case VisionPlugin.三维视觉: return "CubeOutline"; default: return "HexagonOutline"; } } } }