using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.Serialization; using System.Threading; using System.Windows.Forms; using Cognex.VisionPro; using PropertyGridLib.Attributes; using PropertyGridLib.Controls; using System.ComponentModel; using TeamAAS.Camera.Images; using TeamAAS.FlowEditor.Execution; using TeamAAS.FlowEditor.Models; using TeamAAS.FlowEditor.Plugins; using TeamAAS.FlowEngine.FormulaData; using Plugins.Vpp.Converters; namespace Plugins.Vpp { /// /// VisionPro 原生工具反射支撑(算子级节点的基础设施)。 /// 设计:不依赖具体工具程序集(CogBlob/PMAlign/Caliper…),按工具类名在已加载程序集中 /// 反射创建/运行/取结果 —— 新增"某个 VP 工具"的算子节点时零引用成本; /// 工具实例挂在模型上(object 承载),配置随流程文件(BinaryFormatter)整体持久化, /// 与 VppToolBlockModel.ToolBlock 同一持久化模式。 /// internal static class VpToolSupport { /// /// VisionPro 工具/显示程序集清单(简单名)。这些程序集是懒加载的:应用没显式用到之前 /// 不在 AppDomain 里,反射扫描会"找不到 CogBlobTool"。按类型名查工具前先主动加载一轮, /// 加载失败(本机未装 VisionPro)静默跳过。 /// private static readonly string[] CognexToolAssemblies = { "Cognex.VisionPro.ToolGroup", "Cognex.VisionPro.Blob", "Cognex.VisionPro.Blob.Controls", "Cognex.VisionPro.PMAlign", "Cognex.VisionPro.PMAlign.Controls", "Cognex.VisionPro.Caliper", "Cognex.VisionPro.Caliper.Controls", "Cognex.VisionPro.ID", "Cognex.VisionPro.Display.Controls", "Cognex.VisionPro.Controls", "Cognex.VisionPro.ToolGroup.Controls", }; private static void EnsureCognexAssembliesLoaded() { foreach (var name in CognexToolAssemblies) { try { Assembly.Load(name); } catch { /* 未安装/缺失 → 跳过 */ } } } /// 按简单类名(如 "CogBlobTool")在已加载程序集中查找 ICogTool 实现。 /// 找不到时先主动加载 VisionPro 工具程序集再重扫一次(懒加载问题)。 public static Type FindToolType(string simpleName) { var found = FindToolTypeInLoaded(simpleName); if (found != null) return found; EnsureCognexAssembliesLoaded(); return FindToolTypeInLoaded(simpleName); } private static Type FindToolTypeInLoaded(string simpleName) { Type found = null; foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) { try { if (asm.IsDynamic) continue; foreach (var t in asm.GetTypes()) { if (t.Name == simpleName && typeof(ICogTool).IsAssignableFrom(t) && !t.IsAbstract) return t; if (t.Name == simpleName) found = t; } } catch { } } return found; } /// 创建默认工具实例。失败抛异常(节点报错并提示)。 public static object CreateTool(string simpleName) { var type = FindToolType(simpleName) ?? throw new InvalidOperationException($"未找到 VisionPro 工具类型 {simpleName}(检查 VPP 运行库是否已加载)"); return Activator.CreateInstance(type); } /// /// 设置输入图像(GenImage → ICogImage 自动转换)。 /// 兼容 ICogRecord(自动抽取其中的 ICogImage,如绑定到工具块的「输出图层N」); /// 类型不符时抛出带实际类型信息的异常(而不是反射的"Object 无法转换 ICogImage")。 /// public static void SetInputImage(object tool, object image) { if (tool == null || image == null) return; object cog; if (image is GenImage gi) cog = GenImageConverter.ToCogImage(gi); else if (image is ICogImage ci) cog = ci; else if (image is ICogRecord rec) { cog = FindRecordImage(rec) ?? throw new InvalidOperationException( $"输入图像绑定到了记录({rec.GetType().Name}),但其中没有可用的 ICogImage(检查「输入图像」应绑定图像输出而非记录/图层)"); } else cog = image; var prop = tool.GetType().GetProperty("InputImage"); var expect = prop?.PropertyType; if (cog != null && expect != null && !expect.IsInstanceOfType(cog)) throw new InvalidOperationException( $"输入图像类型不符:实际 {cog.GetType().Name},需要 {expect.Name}(检查「输入图像」绑定来源是否为图像输出)"); prop?.SetValue(tool, cog); } /// 从工具运行记录里递归找第一张 ICogImage(记录树:Content 或 SubRecords)。 private static ICogImage FindRecordImage(ICogRecord record) { if (record == null) return null; try { if (record.Content is ICogImage img) return img; var subs = record.SubRecords; if (subs != null) { foreach (ICogRecord sub in subs) { var found = FindRecordImage(sub); if (found != null) return found; } } } catch { } return null; } /// 运行工具(ICogTool.Run)。 public static void Run(object tool) => ((ICogTool)tool).Run(); /// /// 按属性路径取值,支持 索引 与 0参方法:如 "Results.Count"、"Results[0].Score"、 /// "Results[0].GetPose.TranslationX"(GetPose 为 0 参方法时自动调用)。任一环节失败返回 null。 /// public static object GetPath(object root, string path) { try { object cur = root; foreach (var seg in path.Split('.')) { if (cur == null) return null; var m = System.Text.RegularExpressions.Regex.Match(seg, @"^(?[^\[]+)(\[(?\d+)\])?$"); // 方法段带 "()"(如 GetBlobs())——成员查找用裸名(GetBlobs) var name = m.Groups["name"].Value.Replace("()", ""); cur = GetMember(cur, name); if (cur == null) return null; if (m.Groups["idx"].Success && int.TryParse(m.Groups["idx"].Value, out var idx)) cur = GetIndexed(cur, idx); } return cur; } catch { return null; } } private static object GetMember(object obj, string name) { if (obj == null || string.IsNullOrEmpty(name)) return null; var type = obj.GetType(); var prop = type.GetProperty(name); if (prop != null) return prop.GetValue(obj); var field = type.GetField(name); if (field != null) return field.GetValue(obj); // GetXxx 属性不存在时尝试同名 GetXxx() 方法(如 Blob:GetBlobs/GetBlobMeasure) var method = type.GetMethod(name, Type.EmptyTypes) ?? (name.StartsWith("Get") ? null : type.GetMethod("Get" + name, Type.EmptyTypes)); if (method != null) { try { return method.Invoke(obj, null); } catch { return null; } } // 集合默认项:名字为空/First 时取第 0 项 if (name == "First") return GetIndexed(obj, 0); return null; } private static object GetIndexed(object obj, int index) { try { if (obj is System.Collections.IList list) return index < list.Count ? list[index] : null; var indexer = obj?.GetType().GetProperties() .FirstOrDefault(p => p.GetIndexParameters().Length == 1 && p.GetIndexParameters()[0].ParameterType == typeof(int)); if (indexer != null && CountOf(obj) > index) return indexer.GetValue(obj, new object[] { index }); } catch { } return null; } private static int CountOf(object obj) { try { var count = obj?.GetType().GetProperty("Count")?.GetValue(obj); return count is int i ? i : 0; } catch { return 0; } } /// /// 打开工具的原生编辑器(VisionPro 惯例:CogXxxTool ↔ CogXxxEditV2 控件,Subject 属性挂工具)。 /// 编辑控件可能与工具不在同一程序集(如 CogBlobTool 的 EditV2 在 ToolGroup.Controls), /// 因此在全部已加载程序集中按类名查找。找不到返回 false(节点日志提示改用属性面板)。 /// public static bool OpenNativeEditor(object tool, string title) { if (tool == null) return false; var type = tool.GetType(); var baseName = type.Name; // 如 CogBlobTool var shortName = baseName.Replace("Tool", ""); // VisionPro 编辑控件命名惯例去掉 Tool:CogBlobEditV2 foreach (var suffix in new[] { "EditV2", "Edit", "EditControl" }) { var ctlType = FindControlType(baseName + suffix) ?? FindControlType(shortName + suffix); if (ctlType == null) continue; try { var ctl = (Control)Activator.CreateInstance(ctlType); ctlType.GetProperty("Subject")?.SetValue(ctl, tool); using (var form = new Form { Text = title ?? type.Name, Width = 1280, Height = 860, StartPosition = FormStartPosition.CenterScreen }) { ctl.Dock = DockStyle.Fill; form.Controls.Add(ctl); Views.CogGdiRender.Apply(form); // 编辑器内部显示控件统一切 GDI 渲染(远程环境白屏修复) form.ShowDialog(); } return true; } catch { } } return false; } /// 在全部已加载程序集中按简单类名查找 WinForms 控件类型。 /// 找不到时先主动加载 VisionPro 编辑控件程序集再重扫一次(懒加载问题)。 private static Type FindControlType(string simpleName) { var found = FindControlTypeInLoaded(simpleName); if (found != null) return found; EnsureCognexAssembliesLoaded(); return FindControlTypeInLoaded(simpleName); } private static Type FindControlTypeInLoaded(string simpleName) { foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) { try { if (asm.IsDynamic) continue; foreach (var t in asm.GetTypes()) { if (t.Name == simpleName && typeof(Control).IsAssignableFrom(t) && !t.IsAbstract) return t; } } catch { } } return null; } /// /// 学习工具属性树的可读标量路径(迁移 VisionPro「添加终端」的选树体验): /// 递归展开公共属性与字段(深度≤5、总量封顶 300),记录 基元/字符串/日期 值的路径; /// 集合展开前 3 个元素标 [i](如 Results.GetBlobs[0].CenterOfMassX)。 /// 值为 null 的属性也登记(路径有效,只是此刻无值——Blob 未启用的测量项即此类)。 /// public static List LearnTerminalPaths(object root) { var paths = new List(); if (root == null) return paths; var seen = new HashSet(); const int MaxNodes = 300; void Walk(object obj, string prefix, int depth) { if (obj == null || depth > 5 || paths.Count >= MaxNodes) return; if (!seen.Add(obj)) return; var members = obj.GetType() .GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance) .Select(p => (Name: p.Name, Type: (Type)p.PropertyType, Get: (Func)(() => p.GetValue(obj)))) .Concat(obj.GetType() .GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance) .Select(f => (Name: f.Name, Type: f.FieldType, Get: (Func)(() => f.GetValue(obj))))); foreach (var m in members) { if (paths.Count >= MaxNodes) return; var name = m.Name; if (name == "Name" || name == "RunStatus" || name.StartsWith("Site") || name.StartsWith("Tag")) continue; object v; try { v = m.Get(); } catch { continue; } var path = string.IsNullOrEmpty(prefix) ? name : prefix + "." + name; var vt = m.Type; if (vt.IsPrimitive || v is string || v is decimal || v is DateTime) { if (!paths.Contains(path)) paths.Add(path); continue; } if (v == null) { // 路径有效但此刻无值(如 Blob 未启用的测量项)→ 登记为 null 值路径 if (vt.IsPrimitive || vt == typeof(string) || vt == typeof(decimal) || vt == typeof(DateTime)) if (!paths.Contains(path)) paths.Add(path); continue; } if (v is System.Collections.IEnumerable en && !(v is string)) { // 集合:展开前 3 个元素标 [i](Blob 列表等) var idx = 0; try { foreach (var e in en) { if (idx >= 3 || paths.Count >= MaxNodes) break; if (e == null) { idx++; continue; } var et = e.GetType(); if (et.IsPrimitive || e is string || e is decimal || e is DateTime) { var p2 = path + "[" + idx + "]"; if (!paths.Contains(p2)) paths.Add(p2); } else if (!(et.Namespace ?? "").StartsWith("System")) { Walk(e, path + "[" + idx + "]", depth + 1); } idx++; } } catch { } continue; } if (!(vt.Namespace ?? "").StartsWith("System")) { Walk(v, path, depth + 1); } } } Walk(root, "", 0); // 输出统一为树格式(索引段方法化:GetBlobs[0] → GetBlobs()[0]),与弹窗勾选回显一一对应 return paths.Select(NormalizePath).Distinct().ToList(); } /// /// 严格路径取值:exists=false 表示路径中途断裂(属性/字段不存在),value 为 null 仅表示"此刻无值"。 /// 供预设终端预勾选时实测验证——只保留真实存在的路径。 /// public static object GetPathStrict(object root, string path, out bool exists) { exists = false; if (root == null || string.IsNullOrWhiteSpace(path)) return null; object cur = root; foreach (var seg in path.Split('.')) { var m = System.Text.RegularExpressions.Regex.Match(seg, @"^(?[^\[]+)(\[(?\d+)\])?$"); if (!m.Success) return null; var name = m.Groups["name"].Value; var hasMember = MemberExists(cur, name); if (!hasMember) return null; // 属性/字段不存在 → 路径无效 cur = GetMember(cur, name); if (m.Groups["idx"].Success) { if (cur == null) { exists = true; return null; } // 集合为 null:路径结构存在 if (!int.TryParse(m.Groups["idx"].Value, out var idx)) return null; var before = cur; cur = GetIndexed(cur, idx); if (cur == null && before != null && CountOf(before) == 0) { exists = true; return null; } // 空集合:路径结构存在(本次运行无 Blob) if (cur == null) { exists = true; return null; } // 索引越界:结构存在 } } exists = true; return cur; } /// 对象上是否存在指定名称的公共属性或字段(name 兼容带 "()" 的方法段)。 private static bool MemberExists(object obj, string name) { if (obj == null || string.IsNullOrEmpty(name)) return false; name = name.Replace("()", ""); var t = obj.GetType(); if (t.GetProperty(name) != null) return true; if (t.GetField(name) != null) return true; if (t.GetMethod(name, Type.EmptyTypes) != null) return true; return false; } /// /// 旧格式路径 → 树格式规范化:索引加括号(GetBlobs[0] → GetBlobs()[0],兼容属性式旧流程数据)。 /// 规则:"[n]" 前的段若不是以 ")" 结尾(即不是方法调用),在段名与 [n] 之间插入 "()"。 /// public static string NormalizePath(string path) { if (string.IsNullOrWhiteSpace(path)) return path; var segs = path.Split('.'); for (int i = 0; i < segs.Length; i++) { var m = System.Text.RegularExpressions.Regex.Match(segs[i], @"^(?[^\[]+)\[(?\d+)\](?.*)$"); if (m.Success && !m.Groups["name"].Value.EndsWith(")")) { segs[i] = m.Groups["name"].Value + "()" + "[" + m.Groups["idx"].Value + "]" + m.Groups["rest"].Value; } } return string.Join(".", segs); } /// 终端路径的输出名 = 路径末段(如 ...CenterOfMassX → CenterOfMassX)。 public static string TerminalLeafName(string path) { if (string.IsNullOrWhiteSpace(path)) return path; var segs = path.Split('.'); return segs[segs.Length - 1]; } /// 终端输出名(末段 + 与现有名单去重:重名追加 _2/_3)。 public static string TerminalOutputName(string path, ICollection existing) { var n = TerminalLeafName(path); if (existing == null || !existing.Contains(n)) return n; var i = 2; while (existing.Contains(n + "_" + i)) i++; return n + "_" + i; } /// /// Blob 官方测量项清单(CogBlobMeasureConstants 全量,docs.cognex.com)。 /// CogBlobResult 不暴露这些属性 —— 只能经 GetMeasure(枚举) 查询(未启用的测量抛异常)。 /// public static readonly string[] BlobMeasures = { "Label", "Area", "BoundaryPixelLength", "Perimeter", "NumUnfilteredChildren", "CenterMassX", "CenterMassY", "InertiaX", "InertiaY", "InertiaMin", "InertiaMax", "Elongation", "Angle", "Acircularity", "AcircularityRms", "BoundingBoxPixelAlignedNoExcludeCenterX", "BoundingBoxPixelAlignedNoExcludeCenterY", "BoundingBoxPixelAlignedNoExcludeMinX", "BoundingBoxPixelAlignedNoExcludeMaxX", "BoundingBoxPixelAlignedNoExcludeMinY", "BoundingBoxPixelAlignedNoExcludeMaxY", "BoundingBoxPixelAlignedNoExcludeWidth", "BoundingBoxPixelAlignedNoExcludeHeight", "BoundingBoxPixelAlignedNoExcludeAspect", "MedianExtremaAngleX", "MedianExtremaAngleY", "BoundingBoxExtremaAngleCenterX", "BoundingBoxExtremaAngleCenterY", "BoundingBoxExtremaAngleMinX", "BoundingBoxExtremaAngleMaxX", "BoundingBoxExtremaAngleMinY", "BoundingBoxExtremaAngleMaxY", "BoundingBoxExtremaAngleWidth", "BoundingBoxExtremaAngleHeight", "BoundingBoxExtremaAngleAspect", "BoundingBoxPrincipalAxisMinX", "BoundingBoxPrincipalAxisMaxX", "BoundingBoxPrincipalAxisMinY", "BoundingBoxPrincipalAxisMaxY", "BoundingBoxPrincipalAxisWidth", "BoundingBoxPrincipalAxisHeight", "BoundingBoxPrincipalAxisAspect", "NotClipped", }; /// /// GetMeasure 终端:路径形如 "...@Measure:Area",经 CogBlobResult.GetMeasure(枚举) 取值。 /// 路径前半段定位到 CogBlobResult 对象(如 Results.GetBlobs[0])。 /// /// 解析测量终端路径 → (对象路径, 测量名)。非测量路径返回 false。 public static bool TryParseMeasurePath(string path, out string objectPath, out string measureName) { objectPath = null; measureName = null; if (string.IsNullOrWhiteSpace(path)) return false; var i = path.IndexOf(VpPresetTerminal.MeasureMarker, StringComparison.Ordinal); if (i < 0) return false; objectPath = path.Substring(0, i); measureName = path.Substring(i + VpPresetTerminal.MeasureMarker.Length); return !string.IsNullOrEmpty(measureName); } /// 取测量值(对象路径 + 测量名)。对象缺失/未启用该测量 → null(不抛错)。 public static object GetMeasureValue(object root, string objectPath, string measureName) { var target = string.IsNullOrEmpty(objectPath) ? root : GetPath(root, objectPath); if (target == null) return null; try { var enumType = target.GetType().Assembly.GetType(target.GetType().FullName.Replace(target.GetType().Name, "CogBlobMeasureConstants"), false) ?? FindMeasureEnumType(target.GetType()); if (enumType == null) return null; var value = Enum.Parse(enumType, measureName); var mi = target.GetType().GetMethod("GetMeasure", new[] { enumType }); return mi?.Invoke(target, new[] { value }); } catch { return null; } } /// 在对象程序集里找 CogBlobMeasureConstants 枚举类型。 private static Type FindMeasureEnumType(Type resultType) { try { return resultType.Assembly.GetType(resultType.Namespace + ".CogBlobMeasureConstants", false); } catch { return null; } } } /// /// VP 算子节点模型基类:持有原生工具实例(随流程文件序列化,配置不丢)+ 输入图像绑定 + 高级编辑按钮。 /// [Serializable] public abstract class VpToolModelBase : BasePluginModel, IVpPresetSource { [Browsable(false)] public object Tool; /// 工具简单类名(如 "CogBlobTool")——由对应插件基类写入,用于懒创建默认工具。 [Browsable(false)] public string ToolTypeName; [Category("I.输入")] [DisplayName("1.输入图像")] [Description("上游图像(GenImage/CogImage)。通常绑定容器输入 &{输入.图像} 或前序节点输出")] [FormulaEditor(typeof(PluginDataProvider))] public FormulaBound ImageSource { get; set; } = new FormulaBound(); /// /// 输出图层列表(多选):勾选哪些记录图层,哪些就作为节点输出往下游传递。 /// 可选项来自最近一次运行的图层学习(LastRecordKeys,随流程持久化)。 /// [Category("II.输出")] [DisplayName("1.输出图层列表")] [Description("多选要往下游传递的图层(运行一次后勾选列表会列出全部可选项)")] [MultiSelect(typeof(VpOutputLayerMultiProvider))] public List OutputLayers { get; set; } = new List(); /// 最近一次运行学习到的图层名清单(「输出图层列表」可选项;随流程持久化)。 [Browsable(false)] public List LastRecordKeys { get; set; } = new List(); /// /// 输出终端(已勾选的属性路径清单):由「编辑输出终端」弹窗树勾选管理,下游按路径末段名绑定。 /// 首次使用时自动预勾选该工具的预设结果项(面积1/分数2 等已在树中呈勾选态)。 /// [Browsable(false)] public List OutputTerminals { get; set; } = new List(); /// 最近一次运行学习到的工具属性路径清单(编辑弹窗树的数据补充;随流程持久化)。 [Browsable(false)] public List LastTerminalPaths { get; set; } = new List(); [Category("II.输出")] [DisplayName("3.编辑输出终端")] [Description("弹出工具属性/结果树,勾选任意项作为输出终端(迁移 VisionPro「添加终端」)")] [Button("编辑输出终端", nameof(OpenTerminalEditor))] public object TerminalEditorButton { get; set; } public void OpenTerminalEditor() { Views.VpTerminalEditorWindow.Show(this); } /// 预设终端路径(来自对应插件类的 PresetTerminals;经 PluginLoader 按工具名取)。 /// 路径统一为【树生成格式】(方法带括号:Results.GetBlobs()[0]@Measure:Area),与弹窗勾选回显一一对应。 public List GetPresetPaths() { try { var plugin = TeamAAS.FlowEngine.PluginLoader.Instance.CreateInstance(ToolName); var prop = plugin?.GetType().GetProperty("PresetTerminals"); if (prop?.GetValue(plugin) is IEnumerable presets) return presets.Where(p => !string.IsNullOrWhiteSpace(p?.Path)) .Select(p => VpToolSupport.NormalizePath(p.Path)).ToList(); } catch { } return new List(); } // ── 高级编辑(属性面板按钮)── [NonSerialized] public Func NativeEditorOpener = VpToolSupport.OpenNativeEditor; [Category("III.高级编辑")] [DisplayName("1.编辑工具")] [Description("打开该 VisionPro 工具的原生编辑界面(区域/参数/调试全部在原生界面里改)")] [Button("高级编辑", nameof(OpenAdvancedEditor))] public object AdvancedEditButton { get; set; } public void OpenAdvancedEditor() { EnsureTool(); // 注意:NativeEditorOpener 是 [NonSerialized] 字段,流程反序列化后字段初始化器不会执行 // (BinaryFormatter 不走构造器)→ 此时为 null,必须兜底回默认打开器,否则"高级编辑进不去" var opener = NativeEditorOpener ?? VpToolSupport.OpenNativeEditor; var ok = opener.Invoke(Tool, ToolName ?? "VisionPro 工具"); if (!ok) { TeamAAS.AppLogger.Warning($"未找到 {Tool?.GetType().Name} 的原生编辑控件,请改用属性面板配置", "Vp算子"); } } internal void EnsureTool() { if (Tool == null && !string.IsNullOrEmpty(ToolTypeName)) Tool = VpToolSupport.CreateTool(ToolTypeName); } } /// /// 「输出图层列表」多选可选项来源:该工具最近一次运行学习到的图层名。 /// (PropertyGridLib 不一定把模型本体传进上下文 —— 回退到当前正在编辑的节点取模型。) /// public class VpOutputLayerMultiProvider : IMultiSelectProvider { public List GetAvailableItems(PropertyItem propertyItem) { var model = TeamAAS.FlowEngine.PluginLoader.Instance.SelectFlow?.PluginModel?.GetModel as VpToolModelBase; return model?.LastRecordKeys ?? new List(); } } /// /// 「输出终端」多选可选项来源:该工具最近一次运行学习到的属性路径树。 /// public class VpTerminalPathProvider : IMultiSelectProvider { public List GetAvailableItems(PropertyItem propertyItem) { var model = TeamAAS.FlowEngine.PluginLoader.Instance.SelectFlow?.PluginModel?.GetModel as VpToolModelBase; return model?.LastTerminalPaths ?? new List(); } } /// 预设输出终端:名称 + 工具属性路径(如 Results.GetBlobs[0].Area)。 public sealed class VpPresetTerminal { /// 测量终端标记:路径形如 "Results.GetBlobs[0]@Measure:Area"(经 GetMeasure 取值)。 public const string MeasureMarker = "@Measure:"; public string Name { get; } public string Path { get; } public VpPresetTerminal(string name, string path) { Name = name; Path = path; } } /// 预设终端来源契约:终端编辑弹窗据此在首次打开时预勾选常用结果。 public interface IVpPresetSource { /// 预设终端路径清单。 List GetPresetPaths(); } public static class VpPresetTerminals { public static readonly VpPresetTerminal[] Empty = new VpPresetTerminal[0]; } /// /// VP 算子节点基类:统一的 取图→喂图→运行→提取结果 流程。 /// 子类只需声明 工具类型名 与 结果提取(输出"全一些":数量/分数/位置/尺寸…逐项 try/catch)。 /// [Serializable] public abstract class VpToolPluginBase : BasePlugin where TModel : VpToolModelBase, new() { /// VisionPro 工具简单类名(如 "CogBlobTool")。 protected abstract string ToolTypeName { get; } /// /// 预设输出终端:各工具定义自己的常用返回结果(中文名 + 属性路径)。 /// 打开终端编辑弹窗时自动呈勾选态(预选),并自动声明为输出、每次运行后赋值。 /// public virtual VpPresetTerminal[] PresetTerminals => VpPresetTerminals.Empty; /// 结果提取:子类把工具运行结果写进 results(全部走 VpToolSupport.GetPath 容错取值)。 protected abstract void ExtractResults(object tool, Dictionary results); public override List DeclareOutputs() { var list = new List { new OutputField("结果", typeof(bool)) }; // 类型化结果(数量/分数/位置... 子类定义) foreach (var f in DeclareResultFields()) list.Add(f); // 输出图层列表:勾选的每一层都作为输出往下游传(清单首次运行后学习并随流程持久化) var outs = Model?.OutputLayers; if (outs != null) { foreach (var k in outs) { if (string.IsNullOrWhiteSpace(k)) continue; list.Add(new OutputField(k.Trim(), typeof(object))); } } // 输出终端:自定义属性路径终端(迁移 VisionPro「添加终端」,输出名=路径末段) var terminals = Model?.OutputTerminals; if (terminals != null) { foreach (var p in terminals) { if (string.IsNullOrWhiteSpace(p)) continue; var existing = list.Select(x => x.Key).ToList(); list.Add(new OutputField(VpToolSupport.TerminalOutputName(p.Trim(), existing), typeof(object))); } } // 根记录(默认合成视图)的稳定输出口 list.Add(new OutputField("输出图像", typeof(object))); return list; } /// 子类声明的结果字段(供运行前绑定树索引)。 protected abstract IEnumerable DeclareResultFields(); public override bool InitPlugin() { EnsureTool(); return true; } protected void EnsureTool() { Model.ToolTypeName = ToolTypeName; Model.EnsureTool(); } /// 当前工具实例(EnsureTool 之后可用)。 protected object Tool => Model.Tool; public override NodeRunStatus PluginRun(CancellationToken token, out Dictionary results) { var res = new Dictionary(); results = res; NodeRunStatus Fail(string m) { res["Error"] = m; res["结果"] = false; Log(1, $"{Model.NodeName} 失败: {m}", TeamAAS.LogLevel.Error); return NodeRunStatus.Failed; } try { EnsureTool(); var src = GetResolve(Model.ImageSource); if (src == null) return Fail("未获取到输入图像(请绑定「输入图像」)"); // GenImage 统一转 CogImage(喂图与图层发布共用一份,避免重复转换) object cogImage = src is GenImage gi ? GenImageConverter.ToCogImage(gi) : src; VpToolSupport.SetInputImage(Tool, cogImage); VpToolSupport.Run(Tool); Log(3, $"{ToolTypeName} 运行完成"); res["结果"] = true; ExtractResults(Tool, res); // 编辑器图层显示 + 输出:发布工具【运行记录】的全部图层 —— 镜像 VisionPro 原生下拉 Dictionary layerContents = null; try { var record = (Tool as Cognex.VisionPro.Implementation.CogToolBase)?.CreateLastRunRecord(); if (record != null) { layerContents = VppEditorPreview.ShowRecord(Model, record); // 学习图层清单(「输出图层列表」可选项,随流程持久化) var layerKeys = layerContents.Keys.ToList(); if (layerKeys.Count > 0 && !layerKeys.SequenceEqual(Model.LastRecordKeys ?? new List())) Model.LastRecordKeys = layerKeys; // 输出图层列表:勾选的每一层写入节点结果(下游公式绑定) foreach (var k in Model.OutputLayers ?? new List()) { if (string.IsNullOrWhiteSpace(k)) continue; if (layerContents.TryGetValue(k, out var rec)) res[k.Trim()] = rec; } res["输出图像"] = record; } } catch { } // 输出终端:预设终端未初始化时自动预勾选(首次使用即有常用结果); // 路径统一为树格式(方法带括号);勾选项写入节点结果(输出名=路径末段) try { if (Model.OutputTerminals == null || Model.OutputTerminals.Count == 0) { // 预勾选 = 该工具的预设终端(用户可在弹窗里取消/增选,一旦有勾选就不再覆盖) Model.OutputTerminals = PresetTerminals.Where(p => !string.IsNullOrWhiteSpace(p?.Path)) .Select(p => VpToolSupport.NormalizePath(p.Path)).ToList(); } else { // 旧格式兼容迁移:属性式索引 → 方法式(一次性规范化,保存流程后固定) Model.OutputTerminals = Model.OutputTerminals .Where(p => !string.IsNullOrWhiteSpace(p)) .Select(VpToolSupport.NormalizePath).Distinct().ToList(); } var terminalPaths = VpToolSupport.LearnTerminalPaths(Tool); if (terminalPaths.Count > 0 && !terminalPaths.SequenceEqual(Model.LastTerminalPaths ?? new List())) Model.LastTerminalPaths = terminalPaths; foreach (var p in Model.OutputTerminals ?? new List()) { if (string.IsNullOrWhiteSpace(p)) continue; var name = VpToolSupport.TerminalOutputName(p, res.Keys.ToList()); // 测量终端(...@Measure:Area):经 CogBlobResult.GetMeasure 取值 if (VpToolSupport.TryParseMeasurePath(p, out var objPath, out var measureName)) res[name] = VpToolSupport.GetMeasureValue(Tool, objPath, measureName); else res[name] = VpToolSupport.GetPath(Tool, p); } } catch { } return NodeRunStatus.Success; } catch (Exception ex) { return Fail(ex.Message); } } /// 容错取结果值(路径取不到时输出 null,不抛错)。 protected object Get(object tool, string path) => VpToolSupport.GetPath(tool, path); } }