| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832 |
- 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
- {
- /// <summary>
- /// VisionPro 原生工具反射支撑(算子级节点的基础设施)。
- /// 设计:不依赖具体工具程序集(CogBlob/PMAlign/Caliper…),按工具类名在已加载程序集中
- /// 反射创建/运行/取结果 —— 新增"某个 VP 工具"的算子节点时零引用成本;
- /// 工具实例挂在模型上(object 承载),配置随流程文件(BinaryFormatter)整体持久化,
- /// 与 VppToolBlockModel.ToolBlock 同一持久化模式。
- /// </summary>
- internal static class VpToolSupport
- {
- /// <summary>
- /// VisionPro 工具/显示程序集清单(简单名)。这些程序集是懒加载的:应用没显式用到之前
- /// 不在 AppDomain 里,反射扫描会"找不到 CogBlobTool"。按类型名查工具前先主动加载一轮,
- /// 加载失败(本机未装 VisionPro)静默跳过。
- /// </summary>
- 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 { /* 未安装/缺失 → 跳过 */ }
- }
- }
- /// <summary>按简单类名(如 "CogBlobTool")在已加载程序集中查找 ICogTool 实现。
- /// 找不到时先主动加载 VisionPro 工具程序集再重扫一次(懒加载问题)。</summary>
- 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;
- }
- /// <summary>创建默认工具实例。失败抛异常(节点报错并提示)。</summary>
- public static object CreateTool(string simpleName)
- {
- var type = FindToolType(simpleName)
- ?? throw new InvalidOperationException($"未找到 VisionPro 工具类型 {simpleName}(检查 VPP 运行库是否已加载)");
- return Activator.CreateInstance(type);
- }
- /// <summary>
- /// 设置输入图像(GenImage → ICogImage 自动转换)。
- /// 兼容 ICogRecord(自动抽取其中的 ICogImage,如绑定到工具块的「输出图层N」);
- /// 类型不符时抛出带实际类型信息的异常(而不是反射的"Object 无法转换 ICogImage")。
- /// </summary>
- 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);
- }
- /// <summary>从工具运行记录里递归找第一张 ICogImage(记录树:Content 或 SubRecords)。</summary>
- 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;
- }
- /// <summary>运行工具(ICogTool.Run)。</summary>
- public static void Run(object tool) => ((ICogTool)tool).Run();
- /// <summary>
- /// 按属性路径取值,支持 索引 与 0参方法:如 "Results.Count"、"Results[0].Score"、
- /// "Results[0].GetPose.TranslationX"(GetPose 为 0 参方法时自动调用)。任一环节失败返回 null。
- /// </summary>
- 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, @"^(?<name>[^\[]+)(\[(?<idx>\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; }
- }
- /// <summary>
- /// 打开工具的原生编辑器(VisionPro 惯例:CogXxxTool ↔ CogXxxEditV2 控件,Subject 属性挂工具)。
- /// 编辑控件可能与工具不在同一程序集(如 CogBlobTool 的 EditV2 在 ToolGroup.Controls),
- /// 因此在全部已加载程序集中按类名查找。找不到返回 false(节点日志提示改用属性面板)。
- /// </summary>
- 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;
- }
- /// <summary>在全部已加载程序集中按简单类名查找 WinForms 控件类型。
- /// 找不到时先主动加载 VisionPro 编辑控件程序集再重扫一次(懒加载问题)。</summary>
- 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;
- }
- /// <summary>
- /// 学习工具属性树的可读标量路径(迁移 VisionPro「添加终端」的选树体验):
- /// 递归展开公共属性与字段(深度≤5、总量封顶 300),记录 基元/字符串/日期 值的路径;
- /// 集合展开前 3 个元素标 [i](如 Results.GetBlobs[0].CenterOfMassX)。
- /// 值为 null 的属性也登记(路径有效,只是此刻无值——Blob 未启用的测量项即此类)。
- /// </summary>
- public static List<string> LearnTerminalPaths(object root)
- {
- var paths = new List<string>();
- if (root == null) return paths;
- var seen = new HashSet<object>();
- 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<object>)(() => 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<object>)(() => 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();
- }
- /// <summary>
- /// 严格路径取值:exists=false 表示路径中途断裂(属性/字段不存在),value 为 null 仅表示"此刻无值"。
- /// 供预设终端预勾选时实测验证——只保留真实存在的路径。
- /// </summary>
- 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, @"^(?<name>[^\[]+)(\[(?<idx>\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;
- }
- /// <summary>对象上是否存在指定名称的公共属性或字段(name 兼容带 "()" 的方法段)。</summary>
- 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;
- }
- /// <summary>
- /// 旧格式路径 → 树格式规范化:索引加括号(GetBlobs[0] → GetBlobs()[0],兼容属性式旧流程数据)。
- /// 规则:"[n]" 前的段若不是以 ")" 结尾(即不是方法调用),在段名与 [n] 之间插入 "()"。
- /// </summary>
- 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], @"^(?<name>[^\[]+)\[(?<idx>\d+)\](?<rest>.*)$");
- 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);
- }
- /// <summary>终端路径的输出名 = 路径末段(如 ...CenterOfMassX → CenterOfMassX)。</summary>
- public static string TerminalLeafName(string path)
- {
- if (string.IsNullOrWhiteSpace(path)) return path;
- var segs = path.Split('.');
- return segs[segs.Length - 1];
- }
- /// <summary>终端输出名(末段 + 与现有名单去重:重名追加 _2/_3)。</summary>
- public static string TerminalOutputName(string path, ICollection<string> 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;
- }
- /// <summary>
- /// Blob 官方测量项清单(CogBlobMeasureConstants 全量,docs.cognex.com)。
- /// CogBlobResult 不暴露这些属性 —— 只能经 GetMeasure(枚举) 查询(未启用的测量抛异常)。
- /// </summary>
- 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",
- };
- /// <summary>
- /// GetMeasure 终端:路径形如 "...@Measure:Area",经 CogBlobResult.GetMeasure(枚举) 取值。
- /// 路径前半段定位到 CogBlobResult 对象(如 Results.GetBlobs[0])。
- /// </summary>
- /// <summary>解析测量终端路径 → (对象路径, 测量名)。非测量路径返回 false。</summary>
- 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);
- }
- /// <summary>取测量值(对象路径 + 测量名)。对象缺失/未启用该测量 → null(不抛错)。</summary>
- 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; }
- }
- /// <summary>在对象程序集里找 CogBlobMeasureConstants 枚举类型。</summary>
- private static Type FindMeasureEnumType(Type resultType)
- {
- try { return resultType.Assembly.GetType(resultType.Namespace + ".CogBlobMeasureConstants", false); }
- catch { return null; }
- }
- }
- /// <summary>
- /// VP 算子节点模型基类:持有原生工具实例(随流程文件序列化,配置不丢)+ 输入图像绑定 + 高级编辑按钮。
- /// </summary>
- [Serializable]
- public abstract class VpToolModelBase : BasePluginModel, IVpPresetSource
- {
- [Browsable(false)]
- public object Tool;
- /// <summary>工具简单类名(如 "CogBlobTool")——由对应插件基类写入,用于懒创建默认工具。</summary>
- [Browsable(false)]
- public string ToolTypeName;
- [Category("I.输入")]
- [DisplayName("1.输入图像")]
- [Description("上游图像(GenImage/CogImage)。通常绑定容器输入 &{输入.图像} 或前序节点输出")]
- [FormulaEditor(typeof(PluginDataProvider))]
- public FormulaBound<object> ImageSource { get; set; } = new FormulaBound<object>();
- /// <summary>
- /// 输出图层列表(多选):勾选哪些记录图层,哪些就作为节点输出往下游传递。
- /// 可选项来自最近一次运行的图层学习(LastRecordKeys,随流程持久化)。
- /// </summary>
- [Category("II.输出")]
- [DisplayName("1.输出图层列表")]
- [Description("多选要往下游传递的图层(运行一次后勾选列表会列出全部可选项)")]
- [MultiSelect(typeof(VpOutputLayerMultiProvider))]
- public List<string> OutputLayers { get; set; } = new List<string>();
- /// <summary>最近一次运行学习到的图层名清单(「输出图层列表」可选项;随流程持久化)。</summary>
- [Browsable(false)]
- public List<string> LastRecordKeys { get; set; } = new List<string>();
- /// <summary>
- /// 输出终端(已勾选的属性路径清单):由「编辑输出终端」弹窗树勾选管理,下游按路径末段名绑定。
- /// 首次使用时自动预勾选该工具的预设结果项(面积1/分数2 等已在树中呈勾选态)。
- /// </summary>
- [Browsable(false)]
- public List<string> OutputTerminals { get; set; } = new List<string>();
- /// <summary>最近一次运行学习到的工具属性路径清单(编辑弹窗树的数据补充;随流程持久化)。</summary>
- [Browsable(false)]
- public List<string> LastTerminalPaths { get; set; } = new List<string>();
- [Category("II.输出")]
- [DisplayName("3.编辑输出终端")]
- [Description("弹出工具属性/结果树,勾选任意项作为输出终端(迁移 VisionPro「添加终端」)")]
- [Button("编辑输出终端", nameof(OpenTerminalEditor))]
- public object TerminalEditorButton { get; set; }
- public void OpenTerminalEditor()
- {
- Views.VpTerminalEditorWindow.Show(this);
- }
- /// <summary>预设终端路径(来自对应插件类的 PresetTerminals;经 PluginLoader 按工具名取)。
- /// 路径统一为【树生成格式】(方法带括号:Results.GetBlobs()[0]@Measure:Area),与弹窗勾选回显一一对应。</summary>
- public List<string> GetPresetPaths()
- {
- try
- {
- var plugin = TeamAAS.FlowEngine.PluginLoader.Instance.CreateInstance(ToolName);
- var prop = plugin?.GetType().GetProperty("PresetTerminals");
- if (prop?.GetValue(plugin) is IEnumerable<VpPresetTerminal> presets)
- return presets.Where(p => !string.IsNullOrWhiteSpace(p?.Path))
- .Select(p => VpToolSupport.NormalizePath(p.Path)).ToList();
- }
- catch { }
- return new List<string>();
- }
- // ── 高级编辑(属性面板按钮)──
- [NonSerialized]
- public Func<object, string, bool> 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);
- }
- }
- /// <summary>
- /// 「输出图层列表」多选可选项来源:该工具最近一次运行学习到的图层名。
- /// (PropertyGridLib 不一定把模型本体传进上下文 —— 回退到当前正在编辑的节点取模型。)
- /// </summary>
- public class VpOutputLayerMultiProvider : IMultiSelectProvider
- {
- public List<string> GetAvailableItems(PropertyItem propertyItem)
- {
- var model = TeamAAS.FlowEngine.PluginLoader.Instance.SelectFlow?.PluginModel?.GetModel as VpToolModelBase;
- return model?.LastRecordKeys ?? new List<string>();
- }
- }
- /// <summary>
- /// 「输出终端」多选可选项来源:该工具最近一次运行学习到的属性路径树。
- /// </summary>
- public class VpTerminalPathProvider : IMultiSelectProvider
- {
- public List<string> GetAvailableItems(PropertyItem propertyItem)
- {
- var model = TeamAAS.FlowEngine.PluginLoader.Instance.SelectFlow?.PluginModel?.GetModel as VpToolModelBase;
- return model?.LastTerminalPaths ?? new List<string>();
- }
- }
- /// <summary>预设输出终端:名称 + 工具属性路径(如 Results.GetBlobs[0].Area)。</summary>
- public sealed class VpPresetTerminal
- {
- /// <summary>测量终端标记:路径形如 "Results.GetBlobs[0]@Measure:Area"(经 GetMeasure 取值)。</summary>
- public const string MeasureMarker = "@Measure:";
- public string Name { get; }
- public string Path { get; }
- public VpPresetTerminal(string name, string path) { Name = name; Path = path; }
- }
- /// <summary>预设终端来源契约:终端编辑弹窗据此在首次打开时预勾选常用结果。</summary>
- public interface IVpPresetSource
- {
- /// <summary>预设终端路径清单。</summary>
- List<string> GetPresetPaths();
- }
- public static class VpPresetTerminals
- {
- public static readonly VpPresetTerminal[] Empty = new VpPresetTerminal[0];
- }
- /// <summary>
- /// VP 算子节点基类:统一的 取图→喂图→运行→提取结果 流程。
- /// 子类只需声明 工具类型名 与 结果提取(输出"全一些":数量/分数/位置/尺寸…逐项 try/catch)。
- /// </summary>
- [Serializable]
- public abstract class VpToolPluginBase<TModel> : BasePlugin<TModel>
- where TModel : VpToolModelBase, new()
- {
- /// <summary>VisionPro 工具简单类名(如 "CogBlobTool")。</summary>
- protected abstract string ToolTypeName { get; }
- /// <summary>
- /// 预设输出终端:各工具定义自己的常用返回结果(中文名 + 属性路径)。
- /// 打开终端编辑弹窗时自动呈勾选态(预选),并自动声明为输出、每次运行后赋值。
- /// </summary>
- public virtual VpPresetTerminal[] PresetTerminals => VpPresetTerminals.Empty;
- /// <summary>结果提取:子类把工具运行结果写进 results(全部走 VpToolSupport.GetPath 容错取值)。</summary>
- protected abstract void ExtractResults(object tool, Dictionary<string, object> results);
- public override List<OutputField> DeclareOutputs()
- {
- var list = new List<OutputField> { 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;
- }
- /// <summary>子类声明的结果字段(供运行前绑定树索引)。</summary>
- protected abstract IEnumerable<OutputField> DeclareResultFields();
- public override bool InitPlugin()
- {
- EnsureTool();
- return true;
- }
- protected void EnsureTool()
- {
- Model.ToolTypeName = ToolTypeName;
- Model.EnsureTool();
- }
- /// <summary>当前工具实例(EnsureTool 之后可用)。</summary>
- protected object Tool => Model.Tool;
- public override NodeRunStatus PluginRun(CancellationToken token, out Dictionary<string, object> results)
- {
- var res = new Dictionary<string, object>();
- 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<string, object> 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<string>()))
- Model.LastRecordKeys = layerKeys;
- // 输出图层列表:勾选的每一层写入节点结果(下游公式绑定)
- foreach (var k in Model.OutputLayers ?? new List<string>())
- {
- 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<string>()))
- Model.LastTerminalPaths = terminalPaths;
- foreach (var p in Model.OutputTerminals ?? new List<string>())
- {
- 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);
- }
- }
- /// <summary>容错取结果值(路径取不到时输出 null,不抛错)。</summary>
- protected object Get(object tool, string path) => VpToolSupport.GetPath(tool, path);
- }
- }
|