diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj index fb6e518..a697ca9 100644 --- a/TransportPlugin.csproj +++ b/TransportPlugin.csproj @@ -367,6 +367,7 @@ + diff --git a/doc/design/2026/NavisworksAPI使用方法.md b/doc/design/2026/NavisworksAPI使用方法.md index 8f79a8d..114d809 100644 --- a/doc/design/2026/NavisworksAPI使用方法.md +++ b/doc/design/2026/NavisworksAPI使用方法.md @@ -631,6 +631,106 @@ Transform3D original = geometry != null ? geometry.OriginalTransform : item.Tran - 如果业务尺寸是后续叠加出来的,单纯 reset 会把业务尺寸也一起清掉 - 因此虚拟物体常常需要 reset 后再重放业务尺寸 +### 11.5.1 真实物体 Rail 起点旋转的两个关键经验 + +这是本项目在真实物体沿 `Rail` 路径“移动到起点”与“角度调整”排查中确认的两条硬结论。 + +#### A. 正确理解 Navisworks 的“增量旋转” + +`document.Models.OverridePermanentTransform(items, transform, false)` 施加的是**增量层**,不是“把对象直接设成最终姿态”。 + +对真实物体要区分 3 个量: + +1. `OriginalTransform` + - 原始设计文件里的姿态 +2. `PermanentOverrideTransform` + - 我们写进去的增量/覆盖姿态 +3. `PermanentTransform / ActiveTransform` + - Navisworks 最终真正显示出来的姿态 + +项目实测中,真实物体常见关系应按下面理解: + +```text +最终显示姿态 = OriginalTransform × PermanentOverrideTransform +``` + +因此: + +- 如果你手上拿到的是“最终想看到的目标姿态” +- 不能直接把它当 `PermanentOverrideTransform` 写进去 +- 必须先换算出真正应该写入的 override 姿态 + +否则就会出现: + +- 目标姿态日志看起来是对的 +- `OverridePermanentTransform` 也写进去了 +- 但最终 `ActiveTransform` 仍然被原始模型姿态再转一次 + +这类问题在真实物体上非常常见,尤其是 Revit 导入件。 + +#### B. 正确区分“宿主坐标系语义”和“物体自身坐标系语义” + +UI 里的角度调整,用户理解的一定是**宿主坐标系**: + +- `X/Y/Z` 表示宿主世界 `X/Y/Z` + +但 Navisworks 对真实物体施加旋转时,底层消费的仍然是**物体自身轴语义**。 + +因此正确链路必须是: + +```text +宿主世界轴角度输入 +-> 映射到当前真实物体的本地轴 +-> 再生成本地 correction quaternion +-> 最后交给 OverridePermanentTransform 链 +``` + +不能直接把“宿主世界 X/Y/Z”角度传给一个只接受物体本地轴语义的方法。 + +#### C. 哪种映射是对的 + +真实物体这里至少有两套看起来很像、但不能混用的映射: + +1. **宿主世界轴 -> raw 本地轴** + - 用于 UI 角度调整 + - 例如:宿主世界 `X` 对应物体本地 `+Y` +2. **宿主语义轴 -> raw 本地轴** + - 用于路径姿态解释 + - 例如:`Rail` 的 `forward/up` 选择 + +这两套映射在 `up` 轴上可能碰巧一致,但在水平轴上经常不同。 + +项目这次问题的根因之一,就是把: + +- “路径姿态用的宿主语义映射” + +误当成了: + +- “角度调整用的宿主世界轴映射” + +结果表现为: + +- `Y` 调整看起来正确 +- `X/Z` 调整方向却反了 + +#### D. 项目中的推荐实现 + +对真实物体: + +1. 先从 fragment representative frame 读取 `rawAxisX / rawAxisY / rawAxisZ` +2. 单独解析: + - 宿主世界 `X/Y/Z` 分别对应哪根 raw 本地轴 + - 宿主语义 `forward/up` 对应哪根 raw 本地轴 +3. UI 角度调整只使用“宿主世界轴映射” +4. 路径姿态求解只使用“宿主语义映射” +5. 不要让这两套映射复用同一组字段名后再靠上下文猜 + +一句话记忆: + +- **角度调整看宿主世界轴** +- **路径姿态看业务语义轴** +- **最终落到 Navisworks 时,始终要转回物体自身轴** + ### 11.6 常见误区 #### 11.6.1 误区:`ModelItem.Transform` 代表当前姿态 diff --git a/src/UI/WPF/Views/CoordinateSystemResultDialog.xaml.cs b/src/UI/WPF/Views/CoordinateSystemResultDialog.xaml.cs index 6b3a7ff..9963c70 100644 --- a/src/UI/WPF/Views/CoordinateSystemResultDialog.xaml.cs +++ b/src/UI/WPF/Views/CoordinateSystemResultDialog.xaml.cs @@ -27,15 +27,12 @@ namespace NavisworksTransport.UI.WPF.Views /// private void CopyButton_Click(object sender, RoutedEventArgs e) { - try + if (ClipboardHelper.TrySetText(ResultTextBox.Text, "捕获真实物体位姿")) { - Clipboard.SetText(ResultTextBox.Text); - MessageBox.Show("内容已复制到剪贴板!", "复制成功", MessageBoxButton.OK, MessageBoxImage.Information); - } - catch (System.Exception ex) - { - MessageBox.Show($"复制失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + return; } + + MessageBox.Show("复制到剪贴板失败,请稍后重试。", "错误", MessageBoxButton.OK, MessageBoxImage.Error); } /// diff --git a/src/UI/WPF/Views/EditCoordinatesWindow.xaml b/src/UI/WPF/Views/EditCoordinatesWindow.xaml index d952326..be08209 100644 --- a/src/UI/WPF/Views/EditCoordinatesWindow.xaml +++ b/src/UI/WPF/Views/EditCoordinatesWindow.xaml @@ -111,6 +111,17 @@ + + private void OnCopyClick(object sender, RoutedEventArgs e) + { + string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", X, Y, Z); + bool success = ClipboardHelper.TrySetText(coordinate, "编辑路径点坐标"); + ShowButtonFeedback(CopyButton, success); + } + /// /// 粘贴坐标 /// diff --git a/src/UI/WPF/Views/ModelItemBoundsWindow.xaml.cs b/src/UI/WPF/Views/ModelItemBoundsWindow.xaml.cs index cd83161..f6591a6 100644 --- a/src/UI/WPF/Views/ModelItemBoundsWindow.xaml.cs +++ b/src/UI/WPF/Views/ModelItemBoundsWindow.xaml.cs @@ -192,16 +192,9 @@ namespace NavisworksTransport.UI.WPF.Views /// private void OnCopyCenterClick(object sender, RoutedEventArgs e) { - try - { - string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", CenterX, CenterY, CenterZ); - System.Windows.Clipboard.SetText(coordinate); - ShowButtonFeedback(CopyCenterButton); - } - catch (Exception ex) - { - LogManager.Debug($"[坐标窗口] 复制到剪贴板失败: {ex.Message}"); - } + string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", CenterX, CenterY, CenterZ); + bool success = ClipboardHelper.TrySetText(coordinate, "元素包围盒信息"); + ShowButtonFeedback(CopyCenterButton, success); } /// @@ -209,16 +202,9 @@ namespace NavisworksTransport.UI.WPF.Views /// private void OnCopyTopCenterClick(object sender, RoutedEventArgs e) { - try - { - string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", TopCenterX, TopCenterY, TopCenterZ); - System.Windows.Clipboard.SetText(coordinate); - ShowButtonFeedback(CopyTopButton); - } - catch (Exception ex) - { - LogManager.Debug($"[坐标窗口] 复制到剪贴板失败: {ex.Message}"); - } + string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", TopCenterX, TopCenterY, TopCenterZ); + bool success = ClipboardHelper.TrySetText(coordinate, "元素包围盒信息"); + ShowButtonFeedback(CopyTopButton, success); } /// @@ -226,33 +212,32 @@ namespace NavisworksTransport.UI.WPF.Views /// private void OnCopyBottomCenterClick(object sender, RoutedEventArgs e) { - try - { - string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", BottomCenterX, BottomCenterY, BottomCenterZ); - System.Windows.Clipboard.SetText(coordinate); - ShowButtonFeedback(CopyBottomButton); - } - catch (Exception ex) - { - LogManager.Debug($"[坐标窗口] 复制到剪贴板失败: {ex.Message}"); - } + string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", BottomCenterX, BottomCenterY, BottomCenterZ); + bool success = ClipboardHelper.TrySetText(coordinate, "元素包围盒信息"); + ShowButtonFeedback(CopyBottomButton, success); } /// - /// 显示按钮点击反馈(绿色背景闪烁) + /// 显示按钮点击反馈 /// - private void ShowButtonFeedback(Button button) + private void ShowButtonFeedback(Button button, bool success) { if (button == null) return; var originalBackground = button.Background; var originalBorderBrush = button.BorderBrush; - // 变为绿色表示成功 - button.Background = new SolidColorBrush(System.Windows.Media.Color.FromRgb(76, 175, 80)); - button.BorderBrush = new SolidColorBrush(System.Windows.Media.Color.FromRgb(56, 142, 60)); + if (success) + { + button.Background = new SolidColorBrush(System.Windows.Media.Color.FromRgb(76, 175, 80)); + button.BorderBrush = new SolidColorBrush(System.Windows.Media.Color.FromRgb(56, 142, 60)); + } + else + { + button.Background = new SolidColorBrush(System.Windows.Media.Color.FromRgb(244, 67, 54)); + button.BorderBrush = new SolidColorBrush(System.Windows.Media.Color.FromRgb(211, 47, 47)); + } - // 800毫秒后恢复 var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(800) }; timer.Tick += (s, e) => { diff --git a/src/Utils/ClipboardHelper.cs b/src/Utils/ClipboardHelper.cs new file mode 100644 index 0000000..c6fed38 --- /dev/null +++ b/src/Utils/ClipboardHelper.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading; +using System.Windows; + +namespace NavisworksTransport +{ + /// + /// 剪贴板工具方法 + /// + public static class ClipboardHelper + { + private const int MaxAttempts = 6; + private const int RetryDelayMilliseconds = 80; + + /// + /// 尝试写入文本到剪贴板 + /// + public static bool TrySetText(string text, string context = null) + { + string clipboardText = text ?? string.Empty; + Exception lastException = null; + + for (int attempt = 0; attempt < MaxAttempts; attempt++) + { + if (TrySetTextOnStaThread(clipboardText, out lastException)) + { + return true; + } + + if (attempt < MaxAttempts - 1) + { + Thread.Sleep(RetryDelayMilliseconds); + } + } + + string prefix = string.IsNullOrWhiteSpace(context) ? "[剪贴板]" : $"[{context}]"; + LogManager.Debug($"{prefix} 写入剪贴板失败: {lastException?.Message ?? "未知错误"}"); + return false; + } + + private static bool TrySetTextOnStaThread(string text, out Exception exception) + { + bool success = false; + Exception capturedException = null; + + Thread thread = new Thread(() => + { + try + { + Clipboard.SetDataObject(text, true); + success = true; + } + catch (Exception ex) + { + capturedException = ex; + } + }); + + thread.SetApartmentState(ApartmentState.STA); + thread.IsBackground = true; + thread.Start(); + thread.Join(); + + exception = capturedException; + return success; + } + } +}