Improve clipboard copy utilities and dialogs

This commit is contained in:
tian 2026-03-28 22:55:45 +08:00
parent f4735b164e
commit 6214cd4397
7 changed files with 215 additions and 43 deletions

View File

@ -367,6 +367,7 @@
<Compile Include="src\Utils\NwdExportHelper.cs" />
<Compile Include="src\Utils\ModelItemTransformHelper.cs" />
<Compile Include="src\Utils\CachedTriangle3D.cs" />
<Compile Include="src\Utils\ClipboardHelper.cs" />
<Compile Include="src\Utils\PathHelper.cs" />
<Compile Include="src\Utils\RailPathPoseHelper.cs" />
<Compile Include="src\Utils\CollisionSceneHelper.cs" />

View File

@ -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` 代表当前姿态

View File

@ -27,15 +27,12 @@ namespace NavisworksTransport.UI.WPF.Views
/// </summary>
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);
}
/// <summary>

View File

@ -111,6 +111,17 @@
<!-- 按钮栏 -->
<Border Grid.Row="2" Background="#FFF8FBFF" BorderBrush="#FFD4E7FF" BorderThickness="0,1,0,0" Padding="20,12">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="CopyButton"
Click="OnCopyClick"
Style="{StaticResource IconButtonStyle}"
Width="32"
Height="32"
Margin="0,0,10,0"
ToolTip="复制坐标 (格式: X,Y,Z)">
<Path Data="{StaticResource CopyIconGeometry}"
Fill="{StaticResource NavisworksPrimaryBrush}"
Width="16" Height="16" Stretch="Uniform"/>
</Button>
<Button x:Name="PasteButton"
Click="OnPasteClick"
Style="{StaticResource IconButtonStyle}"

View File

@ -46,6 +46,16 @@ namespace NavisworksTransport.UI.WPF.Views
Close();
}
/// <summary>
/// 复制坐标
/// </summary>
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);
}
/// <summary>
/// 粘贴坐标
/// </summary>

View File

@ -192,16 +192,9 @@ namespace NavisworksTransport.UI.WPF.Views
/// </summary>
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);
}
/// <summary>
@ -209,16 +202,9 @@ namespace NavisworksTransport.UI.WPF.Views
/// </summary>
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);
}
/// <summary>
@ -226,33 +212,32 @@ namespace NavisworksTransport.UI.WPF.Views
/// </summary>
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);
}
/// <summary>
/// 显示按钮点击反馈(绿色背景闪烁)
/// 显示按钮点击反馈
/// </summary>
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) =>
{

View File

@ -0,0 +1,68 @@
using System;
using System.Threading;
using System.Windows;
namespace NavisworksTransport
{
/// <summary>
/// 剪贴板工具方法
/// </summary>
public static class ClipboardHelper
{
private const int MaxAttempts = 6;
private const int RetryDelayMilliseconds = 80;
/// <summary>
/// 尝试写入文本到剪贴板
/// </summary>
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;
}
}
}