diff --git a/CHANGELOG.md b/CHANGELOG.md index b21e854..8f9b1bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # NavisworksTransport 变更日志 +## [0.15.5] - 2026-06-02 + +### ✨ 新功能 + +- **快速取面工具 `FaceInferToolPlugin`**:新增独立 ToolPlugin,单点+像素邻域法快速推断点击面法向量。 + - 算法:一次点击内做三次 `PickItemFromPoint`(中心、右侧 δ px、下侧 δ px),叉积求法向量,δ 递进 {2, 5, 10} px 规避共线 + - 朝向统一:法向量自动翻转到指向相机一侧(`viewpoint.Position` 与命中点对比) + - 构件识别:沿 `ModelItemAnalysisHelper.FindNamedParentContainer` 向上查找命名容器 + - 光标:`Cursor.MarkupAutoPoly`(箭头+虚线小方块),与取点工具的 `Measure` 十字光标区分 + - 与 `PathClickToolPlugin` 并列,互斥激活,结构完全平行 +- **`FaceInferResult`**:结果数据类,包含命中点(宿主坐标)、归一化法向量、ModelItem +- **系统管理功能测试按钮**:页签底部新增「🧭 快速取面测试」,点击后激活工具,用户点击模型面 → 弹出结果对话框 + +### 📝 文档 + +- 新增 `doc/design/2026/face-infer-tool-design.md` 设计文档 + ## [0.15.4] - 2026-06-01 ### ✨ 新功能 diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj index daa497d..88dde49 100644 --- a/TransportPlugin.csproj +++ b/TransportPlugin.csproj @@ -106,6 +106,8 @@ + + diff --git a/doc/design/2026/face-infer-tool-design.md b/doc/design/2026/face-infer-tool-design.md new file mode 100644 index 0000000..f3dd1ca --- /dev/null +++ b/doc/design/2026/face-infer-tool-design.md @@ -0,0 +1,111 @@ +# 快速取面工具设计 + +创建时间:2026-06-02 + +## 目标 + +新增一个独立 ToolPlugin,通过单点+像素邻域法快速推断鼠标点击位置所在面的法向量。 +和取点工具 `PathClickToolPlugin` 并列,互斥激活。 + +## 核心算法 + +单点+像素邻域法 —— 一次点击内做三次 `PickItemFromPoint`: + +``` +中心点: PickItemFromPoint(x, y) → p0 +右邻域: PickItemFromPoint(x+δ, y) → p1 +下邻域: PickItemFromPoint(x, y+δ) → p2 + +v1 = p1 - p0 +v2 = p2 - p0 +normal = normalize(cross(v1, v2)) +``` + +偏移量 δ 三级递进:2px → 5px → 10px,小偏移共线时自动退到更大偏移。 + +## 新增文件 + +| 文件 | 职责 | +|---|---| +| `src/Models/FaceInferResult.cs` | 结果数据类 | +| `src/Core/FaceInferToolPlugin.cs` | ToolPlugin 主体 | + +不修改任何现有文件。 + +## 架构 + +``` +FaceInferToolPlugin : ToolPlugin +│ +├── Plugin("FaceInferTool", "NavisworksTransport") +│ +├── Static 成员 +│ ├── event FaceInferred # 外部订阅事件 +│ ├── AssemblyPath # 用于动态加载 +│ └── TriggerFaceInferred() # 允许外部手动触发 +│ +├── GetCursor → Cursor.MarkupQuickPick +│ +├── MouseDown(x, y) +│ ├── centerPick = PickItemFromPoint(x, y) +│ ├── rightPick = PickItemFromPoint(x + δ, y) +│ ├── downPick = PickItemFromPoint(x, y + δ) +│ ├── 同构件校验 (ModelItem 比较) +│ ├── 共线校验 (cross.Length < 0.001 → 退到下一级 δ) +│ ├── 法向量归一化 +│ ├── 朝向统一 (dot with viewDir, 始终指向相机) +│ └── 触发 FaceInferred 事件 +│ +└── MouseMove → 暂不处理(轻量原则) +``` + +## FaceInferResult 数据结构 + +```csharp +public class FaceInferResult +{ + public Point3D HitPoint { get; } // 命中点(宿主坐标系) + public Vector3D Normal { get; } // 面法向量(已归一化,朝向相机) + public ModelItem ModelItem { get; } // 命中的 ModelItem + public bool IsValid { get; } // 推断是否成功 +} +``` + +所有坐标均为**宿主坐标系**(`PickItemFromPoint` 原始输出)。 + +## 激活方式 + +与 `PathClickToolPlugin` 完全相同的流程,仅插件名不同: + +```csharp +var assemblyPath = FaceInferToolPlugin.AssemblyPath; +Application.Plugins.AddPluginAssembly(assemblyPath); +var record = (ToolPluginRecord)Application.Plugins.FindPlugin("FaceInferTool.NavisworksTransport"); +var plugin = record.LoadPlugin(); +Application.MainDocument.Tool.SetCustomToolPlugin(plugin); +``` + +## 光标 + +使用 `Cursor.MarkupQuickPick`。Navisworks `Cursor` 枚举无平行四边形,此为最接近的选项。 + +## 与 PathClickToolPlugin 的关系 + +- 两者都是 `ToolPlugin`,同一时刻只有一个激活 +- 互斥切换:取面时取点失活,取面结束切回取点 +- 切换流程与项目中已有的「导航工具 ↔ ClickTool」切换模式一致 + +## 暂不实现 + +| 项目 | 原因 | +|---|---| +| 透视近大远小动态 δ 调整 | 先验证精度,不够再加 | +| 跨面距离检测 (maxDist) | 同构件判断已覆盖基本场景 | +| 曲面曲率分析 | 不同业务需求 | +| MouseMove 实时预览 | 保持轻量 | + +## 下一步 + +1. 实现两个新文件 +2. 在已知平面上验证法向量精度(误差应在 1° 以内) +3. 后续由调用方(ViewModel)按需集成激活/切换逻辑 diff --git a/src/Core/FaceInferResult.cs b/src/Core/FaceInferResult.cs new file mode 100644 index 0000000..9176d68 --- /dev/null +++ b/src/Core/FaceInferResult.cs @@ -0,0 +1,35 @@ +using Autodesk.Navisworks.Api; + +namespace NavisworksTransport +{ + /// + /// 快速取面结果。 + /// 所有坐标为宿主坐标系。 + /// + public class FaceInferResult + { + /// 点击命中点(宿主坐标系) + public Point3D HitPoint { get; } + + /// 面法向量(已归一化,朝向相机) + public Vector3D Normal { get; } + + /// 命中的 ModelItem + public ModelItem ModelItem { get; } + + /// 推断是否成功 + public bool IsValid { get; } + + public FaceInferResult(Point3D hitPoint, Vector3D normal, ModelItem modelItem) + { + HitPoint = hitPoint; + Normal = normal; + ModelItem = modelItem; + IsValid = modelItem != null; + } + + /// 创建失败结果 + public static FaceInferResult Invalid { get; } = new FaceInferResult( + new Point3D(), new Vector3D(), null); + } +} diff --git a/src/Core/FaceInferToolPlugin.cs b/src/Core/FaceInferToolPlugin.cs new file mode 100644 index 0000000..df3e717 --- /dev/null +++ b/src/Core/FaceInferToolPlugin.cs @@ -0,0 +1,171 @@ +using System; +using Autodesk.Navisworks.Api; +using Autodesk.Navisworks.Api.Plugins; +using NavisworksTransport.Utils; + +namespace NavisworksTransport +{ + /// + /// 快速取面工具。单点+像素邻域法推断点击位置所在面的法向量。 + /// 与 PathClickToolPlugin 并列,互斥激活。 + /// + [Plugin("FaceInferTool", "NavisworksTransport")] + public class FaceInferToolPlugin : ToolPlugin + { + /// 像素偏移递进序列(共线时自动退到下一级) + private static readonly int[] PixelOffsets = { 2, 5, 10 }; + + /// 最小叉积长度阈值(低于此值视为三点共线) + private const double MinCrossLength = 0.001; + + /// + /// 面推断结果事件。命中物体时传递 FaceInferResult,未命中时传递 Invalid。 + /// + public static event EventHandler FaceInferred; + + /// + /// 获取插件的程序集路径(用于动态加载) + /// + public static string AssemblyPath => + System.Reflection.Assembly.GetExecutingAssembly().Location; + + /// + /// 允许外部代码手动触发 FaceInferred 事件 + /// + public static void TriggerFaceInferred(object sender, FaceInferResult result) + { + FaceInferred?.Invoke(sender, result); + } + + /// + /// 插件构造函数 + /// + public FaceInferToolPlugin() + { + LogManager.Debug("[FaceInferTool] FaceInferToolPlugin 实例已创建"); + } + + /// + /// 鼠标按下事件 — 单次点击内做三次 PickItemFromPoint 推断面法向量 + /// + public override bool MouseDown(View view, KeyModifiers modifiers, ushort button, int x, int y, double timeOffset) + { + if (button != 1) return false; // 只响应左键 + + LogManager.Debug("[FaceInferTool] ===== 鼠标点击 ====="); + LogManager.Debug($"[FaceInferTool] 屏幕坐标: ({x}, {y})"); + + var result = InferFace(view, x, y); + + if (result.IsValid) + { + LogManager.Debug($"[FaceInferTool] ✓ 面推断成功"); + LogManager.Debug($"[FaceInferTool] 命中点: ({result.HitPoint.X:F3}, {result.HitPoint.Y:F3}, {result.HitPoint.Z:F3})"); + LogManager.Debug($"[FaceInferTool] 法向量: ({result.Normal.X:F3}, {result.Normal.Y:F3}, {result.Normal.Z:F3})"); + LogManager.Debug($"[FaceInferTool] 构件: {ModelItemAnalysisHelper.GetSafeDisplayName(result.ModelItem)}"); + } + else + { + LogManager.Debug("[FaceInferTool] ✗ 面推断失败(未命中/跨构件/共线)"); + } + + FaceInferred?.Invoke(this, result); + return false; + } + + /// + /// 鼠标移动事件 — 暂不处理(保持轻量) + /// + public override bool MouseMove(View view, KeyModifiers modifiers, int x, int y, double timeOffset) + { + return false; + } + + /// + /// 光标样式 — 使用 MarkupQuickPick 与取点工具的 Measure 形成视觉区分 + /// + public override Cursor GetCursor(View view, KeyModifiers modifier) + { + return Cursor.MarkupAutoPoly; + } + + /// + /// 核心面推断逻辑。 + /// 在 (x, y) 处做三次 PickItemFromPoint(中心+右下邻域), + /// 通过叉积计算法向量,γ 递进规避共线场景。 + /// + private static FaceInferResult InferFace(View view, int x, int y) + { + // 拾取中心点 + var centerPick = view.PickItemFromPoint(x, y); + if (centerPick == null) + return FaceInferResult.Invalid; + + var p0 = centerPick.Point; + + foreach (int offset in PixelOffsets) + { + var rightPick = view.PickItemFromPoint(x + offset, y); + if (rightPick == null) continue; + + var downPick = view.PickItemFromPoint(x, y + offset); + if (downPick == null) continue; + + // 必须命中同一构件 + if (rightPick.ModelItem != centerPick.ModelItem || + downPick.ModelItem != centerPick.ModelItem) + continue; + + var p1 = rightPick.Point; + var p2 = downPick.Point; + + var v1 = new Vector3D(p1.X - p0.X, p1.Y - p0.Y, p1.Z - p0.Z); + var v2 = new Vector3D(p2.X - p0.X, p2.Y - p0.Y, p2.Z - p0.Z); + + var normal = Cross(v1, v2); + double len = Length(normal); + if (len < MinCrossLength) + { + LogManager.Debug($"[FaceInferTool] 偏移{offset}px 三点近似共线,尝试更大偏移"); + continue; // 退到下一级偏移 + } + + // 归一化 + normal = new Vector3D(normal.X / len, normal.Y / len, normal.Z / len); + + // 统一法向量朝向:指向相机一侧 + var camPos = view.Document.CurrentViewpoint.Value.Position; + var toCam = new Vector3D(camPos.X - p0.X, camPos.Y - p0.Y, camPos.Z - p0.Z); + if (Dot(normal, toCam) < 0) + { + normal = new Vector3D(-normal.X, -normal.Y, -normal.Z); + } + + return new FaceInferResult(p0, normal, centerPick.ModelItem); + } + + LogManager.Debug("[FaceInferTool] 所有偏移量均无法推断平面"); + return FaceInferResult.Invalid; + } + + // ---- Vector3D 辅助方法(避免依赖 Navisworks Vector3D 静态方法在不同版本的可用性差异)---- + + private static Vector3D Cross(Vector3D a, Vector3D b) + { + return new Vector3D( + a.Y * b.Z - a.Z * b.Y, + a.Z * b.X - a.X * b.Z, + a.X * b.Y - a.Y * b.X); + } + + private static double Length(Vector3D v) + { + return Math.Sqrt(v.X * v.X + v.Y * v.Y + v.Z * v.Z); + } + + private static double Dot(Vector3D a, Vector3D b) + { + return a.X * b.X + a.Y * b.Y + a.Z * b.Z; + } + } +} diff --git a/src/UI/WPF/ViewModels/SystemManagementViewModel.cs b/src/UI/WPF/ViewModels/SystemManagementViewModel.cs index e873b06..cf624c2 100644 --- a/src/UI/WPF/ViewModels/SystemManagementViewModel.cs +++ b/src/UI/WPF/ViewModels/SystemManagementViewModel.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using System.Windows.Input; using System.Windows.Threading; using Autodesk.Navisworks.Api; +using Autodesk.Navisworks.Api.Plugins; using ComApiBridge = Autodesk.Navisworks.Api.ComApi.ComApiBridge; using NavisworksTransport.UI.WPF.Collections; using NavisworksTransport.Core; @@ -184,6 +185,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels public ICommand CoordinateSystemExplorerCommand { get; private set; } public ICommand CaptureRealObjectTransformCommand { get; private set; } public ICommand DetectFragmentDefaultUpCommand { get; private set; } + public ICommand TestFaceInferCommand { get; private set; } // 坐标系设置 @@ -339,6 +341,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels CoordinateSystemExplorerCommand = new RelayCommand(() => ExecuteCoordinateSystemExplorer()); CaptureRealObjectTransformCommand = new RelayCommand(() => ExecuteCaptureRealObjectTransform()); DetectFragmentDefaultUpCommand = new RelayCommand(() => ExecuteDetectFragmentDefaultUp()); + TestFaceInferCommand = new RelayCommand(() => ExecuteTestFaceInfer()); // 初始化坐标系选项 @@ -2024,6 +2027,102 @@ namespace NavisworksTransport.UI.WPF.ViewModels public Vector3 SampleTranslation { get; set; } } + /// + /// 快速取面测试 — 激活 FaceInferToolPlugin,用户在模型上点击后显示面法向量 + /// + private void ExecuteTestFaceInfer() + { + SafeExecute(() => + { + var doc = Autodesk.Navisworks.Api.Application.ActiveDocument; + if (doc == null || doc.IsClear) + { + System.Windows.MessageBox.Show( + "没有活动的文档!请先打开一个模型。", + "错误", + System.Windows.MessageBoxButton.OK, + System.Windows.MessageBoxImage.Error); + return; + } + + // 加载并激活 FaceInferToolPlugin + var assemblyPath = FaceInferToolPlugin.AssemblyPath; + Application.Plugins.AddPluginAssembly(assemblyPath); + var record = (ToolPluginRecord)Application.Plugins.FindPlugin("FaceInferTool.NavisworksTransport"); + if (record == null) + { + System.Windows.MessageBox.Show( + "无法找到 FaceInferTool 插件!", + "错误", + System.Windows.MessageBoxButton.OK, + System.Windows.MessageBoxImage.Error); + return; + } + var plugin = record.LoadPlugin(); + if (plugin == null) + { + System.Windows.MessageBox.Show( + "FaceInferTool 插件加载失败!", + "错误", + System.Windows.MessageBoxButton.OK, + System.Windows.MessageBoxImage.Error); + return; + } + + UpdateMainStatus("请在模型上点击一个面…"); + + // 一次性事件处理器:取到面后自动取消订阅并恢复工具 + EventHandler handler = null; + handler = (sender, result) => + { + FaceInferToolPlugin.FaceInferred -= handler; + + // 恢复为选择工具 + try + { + Application.MainDocument.Tool.Value = Tool.Select; + } + catch { } + + if (!result.IsValid) + { + UpdateMainStatus("面推断失败:未命中有效面"); + return; + } + + var info = new StringBuilder(); + info.AppendLine("=== 面推断结果 ==="); + info.AppendLine(); + var namedParent = ModelItemAnalysisHelper.FindNamedParentContainer(result.ModelItem); + var displayName = ModelItemAnalysisHelper.GetSafeDisplayName(namedParent); + info.AppendLine($"构件: {displayName}"); + info.AppendLine($"命中点: ({result.HitPoint.X:F3}, {result.HitPoint.Y:F3}, {result.HitPoint.Z:F3})"); + info.AppendLine($"法向量: ({result.Normal.X:F6}, {result.Normal.Y:F6}, {result.Normal.Z:F6})"); + + string message = info.ToString(); + LogManager.Info("[面推断测试]\n" + message); + + System.Windows.Application.Current.Dispatcher.Invoke(() => + { + var dialog = new NavisworksTransport.UI.WPF.Views.CoordinateSystemResultDialog + { + Title = "面推断结果", + ResultText = message + }; + DialogHelper.SetOwnerSafely(dialog); + dialog.ShowDialog(); + }); + + var statusName = ModelItemAnalysisHelper.GetSafeDisplayName(namedParent); + UpdateMainStatus($"面推断完成 — {statusName}"); + }; + + FaceInferToolPlugin.FaceInferred += handler; + + Application.MainDocument.Tool.SetCustomToolPlugin(plugin); + }, "面推断测试"); + } + /// /// 读取选中对象的Transform信息,并测试旋转 /// diff --git a/src/UI/WPF/Views/SystemManagementView.xaml b/src/UI/WPF/Views/SystemManagementView.xaml index 94f3a06..8ca9d15 100644 --- a/src/UI/WPF/Views/SystemManagementView.xaml +++ b/src/UI/WPF/Views/SystemManagementView.xaml @@ -288,6 +288,11 @@ NavisworksTransport 系统管理页签视图 - 采用与其他页签一致的Nav Command="{Binding CaptureRealObjectTransformCommand}" Style="{StaticResource ActionButtonStyle}" ToolTip="读取选中真实物体的原始Transform,并在3D视图中可视化X/Y/Z三轴"/> + +