From 6214cd43977d6efe455f216faf8641905afa37df Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Sat, 28 Mar 2026 22:55:45 +0800
Subject: [PATCH] Improve clipboard copy utilities and dialogs
---
TransportPlugin.csproj | 1 +
doc/design/2026/NavisworksAPI使用方法.md | 100 ++++++++++++++++++
.../CoordinateSystemResultDialog.xaml.cs | 11 +-
src/UI/WPF/Views/EditCoordinatesWindow.xaml | 11 ++
.../WPF/Views/EditCoordinatesWindow.xaml.cs | 10 ++
.../WPF/Views/ModelItemBoundsWindow.xaml.cs | 57 ++++------
src/Utils/ClipboardHelper.cs | 68 ++++++++++++
7 files changed, 215 insertions(+), 43 deletions(-)
create mode 100644 src/Utils/ClipboardHelper.cs
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;
+ }
+ }
+}