Add Navisworks automation integration tests

This commit is contained in:
tian 2026-04-13 02:33:29 +08:00
parent 61e6b79299
commit b849bd9aed
17 changed files with 2067 additions and 26 deletions

View File

@ -67,6 +67,30 @@
不要并行执行编译和部署。否则很容易把旧 DLL 部署到插件目录。
### 并行执行边界
后续会话中的 AI 助手必须把命令分成两类:
- **允许并行的纯读取操作**
- `rg`
- `Get-Content`
- `ls / Get-ChildItem`
- 读取日志
- 查询状态
- **禁止并行的产出型/宿主相关操作**
- `run-unit-tests.bat`
- `compile.bat`
- `deploy-plugin.bat`
- 启动/关闭 Navisworks
- 会占用 DLL、修改 `bin/obj`、写插件部署目录、依赖上一步产物的任何命令
硬约束:
- 只有“纯读取、无副作用、且互不依赖”的命令才允许并行
- 只要命令会生成、覆盖、部署、锁定文件、启动宿主进程,必须串行执行
- 对本仓库,默认把 `run-unit-tests -> compile -> deploy -> start Navisworks` 视为**单通道流水线**
- 不允许把这条流水线放进任何并行工具里,即使只是为了节省时间
### 插件部署目录
- `C:\ProgramData\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin\`

View File

@ -42,6 +42,9 @@
<Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions">
<HintPath>packages\MSTest.TestFramework.3.0.4\lib\net462\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Numerics" />
@ -57,6 +60,8 @@
<Compile Include="UnitTests\Core\PathPlanningManagerHoistingCompletionTests.cs" />
<Compile Include="UnitTests\Core\PathPersistenceTests.cs" />
<Compile Include="UnitTests\Core\PathRouteCloneTests.cs" />
<Compile Include="UnitTests\Integration\NavisworksTestAutomationClient.cs" />
<Compile Include="UnitTests\Integration\VirtualCollisionAutomationTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\HostCoordinateAdapterTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\CanonicalRailPoseBuilderTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\CanonicalPlanarPoseBuilderTests.cs" />

View File

@ -150,6 +150,7 @@
<Compile Include="src\Core\Services\TimeTagService.cs" />
<Compile Include="src\Core\Services\TimeTagExporter.cs" />
<Compile Include="src\Core\Services\TimeTagTimeLinerIntegration.cs" />
<Compile Include="src\Core\Services\TestAutomationHttpService.cs" />
<!-- Commands - Command Pattern Framework (for testing) -->
<Compile Include="src\Commands\IPathPlanningCommand.cs" />
<Compile Include="src\Commands\CommandBase.cs" />

View File

@ -0,0 +1,85 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
namespace NavisworksTransport.UnitTests.Integration
{
internal sealed class NavisworksTestAutomationClient : IDisposable
{
private readonly HttpClient _httpClient;
public NavisworksTestAutomationClient()
{
_httpClient = new HttpClient
{
BaseAddress = new Uri("http://127.0.0.1:18777"),
Timeout = TimeSpan.FromSeconds(20)
};
}
public async Task EnsureServiceReadyAsync(TimeSpan timeout)
{
DateTime deadlineUtc = DateTime.UtcNow.Add(timeout);
Exception lastError = null;
while (DateTime.UtcNow < deadlineUtc)
{
try
{
JObject pingResponse = await GetJsonAsync("/api/test/ping").ConfigureAwait(false);
if (pingResponse.Value<bool?>("ok") == true)
{
return;
}
}
catch (Exception ex)
{
lastError = ex;
}
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
}
Assert.Fail(
"Navisworks 测试服务未就绪。请先用 start-navisworks.bat 启动 Navisworks并确保插件面板已加载。最后错误: {0}",
lastError?.Message ?? "unknown");
}
public async Task<JObject> RunVirtualCollisionTestAsync(string pathType, int timeoutSeconds)
{
string requestUri = string.Format(
"/api/test/run-virtual-collision-test?pathType={0}&timeoutSeconds={1}",
Uri.EscapeDataString(pathType),
timeoutSeconds);
return await PostJsonAsync(requestUri).ConfigureAwait(false);
}
private async Task<JObject> GetJsonAsync(string requestUri)
{
using (HttpResponseMessage response = await _httpClient.GetAsync(requestUri).ConfigureAwait(false))
{
string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return JObject.Parse(content);
}
}
private async Task<JObject> PostJsonAsync(string requestUri)
{
using (HttpResponseMessage response = await _httpClient.PostAsync(requestUri, new StringContent(string.Empty)).ConfigureAwait(false))
{
string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return JObject.Parse(content);
}
}
public void Dispose()
{
_httpClient.Dispose();
}
}
}

View File

@ -0,0 +1,68 @@
using System;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
namespace NavisworksTransport.UnitTests.Integration
{
[TestClass]
[TestCategory("NavisworksIntegration")]
public class VirtualCollisionAutomationTests
{
private const int DefaultCollisionTimeoutSeconds = 240;
[TestMethod]
[Timeout(360000)]
public async Task GroundVirtualCollision_AutoTestRoute_Completes()
{
await AssertVirtualCollisionTestAsync("Ground").ConfigureAwait(false);
}
[TestMethod]
[Timeout(360000)]
public async Task HoistingVirtualCollision_AutoTestRoute_Completes()
{
await AssertVirtualCollisionTestAsync("Hoisting").ConfigureAwait(false);
}
[TestMethod]
[Timeout(360000)]
public async Task RailVirtualCollision_AutoTestRoute_Completes()
{
await AssertVirtualCollisionTestAsync("Rail").ConfigureAwait(false);
}
private static async Task AssertVirtualCollisionTestAsync(string pathType)
{
using (var client = new NavisworksTestAutomationClient())
{
await client.EnsureServiceReadyAsync(TimeSpan.FromSeconds(90)).ConfigureAwait(false);
JObject response = await client.RunVirtualCollisionTestAsync(pathType, DefaultCollisionTimeoutSeconds).ConfigureAwait(false);
Assert.IsTrue(response.Value<bool>("ok"), "测试 HTTP 接口返回失败: " + (string)response["error"]);
JObject data = (JObject)response["data"];
Assert.IsNotNull(data, "缺少测试结果 data");
Assert.AreEqual(pathType, (string)data["requestedPathType"], "请求的路径类型不匹配");
JObject route = (JObject)data["route"];
Assert.IsNotNull(route, "缺少 route");
Assert.AreEqual(pathType, (string)route["pathType"], "返回的路径类型不匹配");
Assert.IsFalse(string.IsNullOrWhiteSpace((string)route["name"]), "路径名为空");
JObject animatedObject = (JObject)data["animatedObject"];
Assert.IsNotNull(animatedObject, "缺少 animatedObject");
Assert.AreEqual("VirtualObject", (string)animatedObject["mode"], "当前集成测试应使用虚拟物体");
JObject animation = (JObject)data["animation"];
Assert.IsNotNull(animation, "缺少 animation");
Assert.AreEqual("Finished", (string)animation["currentState"], "动画未完成");
Assert.IsTrue((int)animation["totalFrames"] > 0, "动画总帧数应大于 0");
Assert.IsTrue(animation["detectionRecordId"] != null && (int)animation["detectionRecordId"] > 0, "检测记录 ID 无效");
Assert.IsNotNull(data["report"], "缺少碰撞报告");
}
}
}
}

View File

@ -0,0 +1,7 @@
@echo off
setlocal
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\run-ground-virtual-collision-test.ps1" %*
if errorlevel 1 exit /b 1
exit /b 0

View File

@ -41,7 +41,7 @@ if errorlevel 1 (
exit /b 1
)
%VSTEST_PATH% "bin\x64\Release\NavisworksTransport.UnitTests.dll" /Platform:x64 /TestAdapterPath:%TEST_ADAPTER_PATH%
%VSTEST_PATH% "bin\x64\Release\NavisworksTransport.UnitTests.dll" /Platform:x64 /TestAdapterPath:%TEST_ADAPTER_PATH% /TestCaseFilter:"TestCategory!=NavisworksIntegration"
if errorlevel 1 (
echo Unit tests failed!
exit /b 1

View File

@ -0,0 +1,154 @@
[CmdletBinding()]
param(
[string]$ModelPath,
[int]$ServiceStartupTimeoutSeconds = 90,
[int]$TestTimeoutSeconds = 240
)
$ErrorActionPreference = 'Stop'
$scriptDirectory = Split-Path -Parent $MyInvocation.MyCommand.Path
$repoRoot = Split-Path -Parent $scriptDirectory
$startScriptPath = Join-Path $scriptDirectory 'start-navisworks.ps1'
$defaultsFilePath = Join-Path $scriptDirectory 'test-automation.defaults.json'
$testApiBaseUrl = 'http://127.0.0.1:18777'
function Resolve-DefaultModelPath {
if (-not (Test-Path $defaultsFilePath)) {
return $null
}
$defaults = Get-Content $defaultsFilePath -Raw | ConvertFrom-Json
$defaultModelPath = [string]$defaults.defaultModelPath
if ([string]::IsNullOrWhiteSpace($defaultModelPath)) {
return $null
}
return [System.IO.Path]::GetFullPath($defaultModelPath)
}
function Wait-TestApiReady {
param(
[string]$BaseUrl,
[int]$TimeoutSeconds
)
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
$lastError = $null
while ((Get-Date) -lt $deadline) {
try {
$pingResponse = Invoke-RestMethod -Uri ($BaseUrl + '/api/test/ping') -TimeoutSec 10
if ($pingResponse.ok) {
return
}
}
catch {
$lastError = $_.Exception.Message
}
Start-Sleep -Seconds 1
}
if ([string]::IsNullOrWhiteSpace($lastError)) {
throw "测试 HTTP 服务未在 $TimeoutSeconds 秒内就绪。"
}
throw "测试 HTTP 服务未在 $TimeoutSeconds 秒内就绪。最后错误: $lastError"
}
function Wait-GroundRouteReady {
param(
[string]$BaseUrl,
[int]$TimeoutSeconds
)
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
$lastError = $null
while ((Get-Date) -lt $deadline) {
try {
$routesResponse = Invoke-RestMethod -Uri ($BaseUrl + '/api/test/routes') -TimeoutSec 10
if ($routesResponse.ok -and $routesResponse.data -and $routesResponse.data.totalRouteCount -gt 0) {
$groundRoute = $routesResponse.data.suggestedAutoTestRoutes.ground
if (-not [string]::IsNullOrWhiteSpace([string]$groundRoute)) {
return [string]$groundRoute
}
}
}
catch {
$lastError = $_.Exception.Message
}
Start-Sleep -Seconds 1
}
if ([string]::IsNullOrWhiteSpace($lastError)) {
throw "默认地面测试路径未在 $TimeoutSeconds 秒内就绪。"
}
throw "默认地面测试路径未在 $TimeoutSeconds 秒内就绪。最后错误: $lastError"
}
function Save-JsonResult {
param(
[object]$Payload
)
$logRoot = Join-Path $env:ProgramData 'Autodesk\Navisworks Manage 2026\plugins\TransportPlugin\logs\test-automation'
New-Item -ItemType Directory -Path $logRoot -Force | Out-Null
$fileName = 'ground-virtual-collision-test-' + (Get-Date -Format 'yyyyMMdd-HHmmss') + '.json'
$outputPath = Join-Path $logRoot $fileName
$json = $Payload | ConvertTo-Json -Depth 16
[System.IO.File]::WriteAllText($outputPath, $json, [System.Text.UTF8Encoding]::new($false))
return $outputPath
}
if ([string]::IsNullOrWhiteSpace($ModelPath)) {
$ModelPath = Resolve-DefaultModelPath
}
if (-not [string]::IsNullOrWhiteSpace($ModelPath)) {
$ModelPath = [System.IO.Path]::GetFullPath($ModelPath)
if (-not (Test-Path $ModelPath)) {
throw "模型文件不存在: $ModelPath"
}
}
Write-Host '启动 Navisworks...'
if ([string]::IsNullOrWhiteSpace($ModelPath)) {
& $startScriptPath
}
else {
& $startScriptPath $ModelPath
}
Write-Host '等待测试 HTTP 服务就绪...'
Wait-TestApiReady -BaseUrl $testApiBaseUrl -TimeoutSeconds $ServiceStartupTimeoutSeconds
Write-Host '等待默认地面测试路径就绪...'
$groundRouteName = Wait-GroundRouteReady -BaseUrl $testApiBaseUrl -TimeoutSeconds $ServiceStartupTimeoutSeconds
Write-Host ('切换到默认地面测试路径: {0}' -f $groundRouteName)
$selectRouteResponse = Invoke-RestMethod -Method Post -Uri ($testApiBaseUrl + '/api/test/select-default-route?pathType=Ground') -TimeoutSec 15
if (-not $selectRouteResponse.ok) {
throw '切换默认地面测试路径失败。'
}
Write-Host '执行地面虚拟物体碰撞测试...'
$requestUrl = $testApiBaseUrl + "/api/test/run-ground-collision-test?useVirtualObject=true&timeoutSeconds=$TestTimeoutSeconds"
$testResult = Invoke-RestMethod -Method Post -Uri $requestUrl -TimeoutSec ($TestTimeoutSeconds + 30)
$outputFilePath = Save-JsonResult -Payload $testResult
Write-Host ('结果已保存: {0}' -f $outputFilePath)
if ($testResult.ok -and $testResult.data) {
Write-Host ('路径: {0}' -f $testResult.data.route.name)
Write-Host ('动画状态: {0}' -f $testResult.data.animation.currentState)
Write-Host ('检测记录ID: {0}' -f $testResult.data.animation.detectionRecordId)
if ($testResult.data.report) {
Write-Host ('碰撞数: {0}' -f $testResult.data.report.totalCollisions)
Write-Host ('截图数: {0}' -f $testResult.data.report.screenshotCount)
}
}

View File

@ -0,0 +1,167 @@
[CmdletBinding()]
param(
[Parameter(Position = 0)]
[string]$ModelPath
)
$ErrorActionPreference = 'Stop'
$scriptDirectory = Split-Path -Parent $MyInvocation.MyCommand.Path
$repoRoot = Split-Path -Parent $scriptDirectory
$defaultsFilePath = Join-Path $scriptDirectory 'test-automation.defaults.json'
function Resolve-RoamerPath {
$candidates = New-Object System.Collections.Generic.List[string]
$appPathKeys = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\Roamer.exe',
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\Roamer.exe'
)
foreach ($key in $appPathKeys) {
try {
$value = (Get-ItemProperty -Path $key -ErrorAction Stop).'(default)'
if (-not [string]::IsNullOrWhiteSpace($value)) {
$candidates.Add($value)
}
}
catch {
}
}
$wellKnownPaths = @(
(Join-Path ${env:ProgramFiles} 'Autodesk\Navisworks Manage 2026\Roamer.exe'),
(Join-Path ${env:ProgramFiles(x86)} 'Autodesk\Navisworks Manage 2026\Roamer.exe'),
(Join-Path ${env:ProgramFiles} 'Autodesk\Navisworks Simulate 2026\Roamer.exe'),
(Join-Path ${env:ProgramFiles(x86)} 'Autodesk\Navisworks Simulate 2026\Roamer.exe')
) | Where-Object { $_ }
foreach ($path in $wellKnownPaths) {
$candidates.Add($path)
}
foreach ($candidate in $candidates | Select-Object -Unique) {
if (-not [string]::IsNullOrWhiteSpace($candidate) -and (Test-Path $candidate)) {
return [System.IO.Path]::GetFullPath($candidate)
}
}
throw '找不到 Roamer.exe。请确认 Navisworks Manage 2026 已安装。'
}
function Resolve-DefaultModelPath {
if (-not (Test-Path $defaultsFilePath)) {
return $null
}
try {
$defaults = Get-Content $defaultsFilePath -Raw | ConvertFrom-Json
$defaultModelPath = [string]$defaults.defaultModelPath
if ([string]::IsNullOrWhiteSpace($defaultModelPath)) {
return $null
}
$resolvedPath = [System.IO.Path]::GetFullPath($defaultModelPath)
if (-not (Test-Path $resolvedPath)) {
throw "默认模型文件不存在: $resolvedPath"
}
return $resolvedPath
}
catch {
throw "读取默认测试配置失败: $($_.Exception.Message)"
}
}
function Dismiss-RecoveryPromptIfPresent {
param(
[int]$TimeoutSeconds = 20
)
Add-Type -AssemblyName System.Windows.Forms | Out-Null
$shell = New-Object -ComObject WScript.Shell
$windowTitles = @(
'是否从自动保存重新载入?',
'是否从自动保存重新载入?'
)
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
while ((Get-Date) -lt $deadline) {
foreach ($windowTitle in $windowTitles) {
try {
if ($shell.AppActivate($windowTitle)) {
Start-Sleep -Milliseconds 300
[System.Windows.Forms.SendKeys]::SendWait('%N')
Write-Host '已自动选择“否”以跳过自动保存恢复窗口。'
return
}
}
catch {
}
}
Start-Sleep -Milliseconds 500
}
}
if ([string]::IsNullOrWhiteSpace($ModelPath)) {
$ModelPath = Resolve-DefaultModelPath
}
if (-not [string]::IsNullOrWhiteSpace($ModelPath)) {
$ModelPath = [System.IO.Path]::GetFullPath($ModelPath)
if (-not (Test-Path $ModelPath)) {
throw "模型文件不存在: $ModelPath"
}
}
$roamerPath = Resolve-RoamerPath
$existingProcess = Get-Process Roamer -ErrorAction SilentlyContinue | Select-Object -First 1
$env:TRANSPORTPLUGIN_TEST_AUTOMATION = '1'
if ($existingProcess) {
Write-Host ("Navisworks 已在运行PID={0}" -f $existingProcess.Id)
if (-not [string]::IsNullOrWhiteSpace($ModelPath)) {
Start-Process -FilePath $roamerPath -ArgumentList @($ModelPath) | Out-Null
Write-Host ("已请求 Navisworks 打开模型: {0}" -f $ModelPath)
}
Dismiss-RecoveryPromptIfPresent
Write-Host '提示: 当前测试 HTTP 服务在插件 DockPane OnLoaded 中启动,首次测试前请确保插件面板已打开。'
exit 0
}
$arguments = @()
if (-not [string]::IsNullOrWhiteSpace($ModelPath)) {
$arguments += $ModelPath
}
$startProcessParams = @{
FilePath = $roamerPath
PassThru = $true
}
if ($arguments.Count -gt 0) {
$startProcessParams.ArgumentList = $arguments
}
$process = Start-Process @startProcessParams
Write-Host ("Navisworks 启动中PID={0}" -f $process.Id)
$deadline = (Get-Date).AddSeconds(30)
do {
Start-Sleep -Milliseconds 500
$running = Get-Process -Id $process.Id -ErrorAction SilentlyContinue
} while (-not $running -and (Get-Date) -lt $deadline)
if (-not $running) {
throw 'Navisworks 启动超时30 秒内未检测到 Roamer.exe 进程。'
}
Dismiss-RecoveryPromptIfPresent
Write-Host ("Navisworks 已启动: {0}" -f $roamerPath)
if (-not [string]::IsNullOrWhiteSpace($ModelPath)) {
Write-Host ("已打开模型参数: {0}" -f $ModelPath)
}
Write-Host '提示: 当前测试 HTTP 服务在插件 DockPane OnLoaded 中启动,首次测试前请确保插件面板已打开。'

View File

@ -0,0 +1,4 @@
{
"defaultModelPath": "C:\\Users\\Tellme\\Documents\\NavisworksTransport\\模型\\Floor2_mobile_yup.nwd",
"defaultRouteNames": []
}

View File

@ -7,6 +7,7 @@ using System.Windows.Forms.Integration;
using NavisworksTransport.Core;
using NavisworksTransport.Core.Animation;
using NavisworksTransport.Core.Config;
using NavisworksTransport.Core.Services;
using NavisworksTransport.Utils;
using NavisworksTransport.Utils.CoordinateSystem;
using NavisApplication = Autodesk.Navisworks.Api.Application;
@ -382,6 +383,9 @@ namespace NavisworksTransport
// 订阅文档事件
SubscribeToDocumentEvents();
// 启动本地测试自动化 HTTP 控制面
TestAutomationHttpService.Instance.Start();
// 检查是否已有活动文档且包含模型
var activeDoc = NavisApplication.ActiveDocument;
if (activeDoc != null && activeDoc.Models != null && activeDoc.Models.Count > 0)
@ -413,6 +417,9 @@ namespace NavisworksTransport
// 清理管理器
CleanupManagers();
// 停止本地测试自动化 HTTP 控制面
TestAutomationHttpService.Instance.Stop();
base.OnUnloading();
}

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,7 @@ using Autodesk.Navisworks.Api.Clash;
using NavisworksTransport.Core;
using NavisworksTransport.Core.Models;
using NavisworksTransport.Core.Config;
using NavisworksTransport.Core.Services;
using NavisworksTransport.Commands;
using NavisworksTransport.UI.WPF.Collections;
using NavisworksTransport.UI.WPF.Views;
@ -488,6 +489,14 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
/// <summary>
/// 最近一次自动生成的碰撞报告。
/// 仅用于测试自动化读取结果,不参与业务流程分支。
/// </summary>
public CollisionReportResult LastGeneratedReport => _lastGeneratedReport;
public bool HasGeneratedCollisionReport => _lastGeneratedReport != null;
/// <summary>
@ -1994,11 +2003,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
if (clashResults == null || clashResults.Count == 0)
{
LogManager.Info("🎉 恭喜!本次动画未检测到任何碰撞,路径规划非常安全!");
System.Windows.MessageBox.Show(
DialogHelper.ShowAutoClosingMessageBox(
"🎉 恭喜!本次动画仿真过程中未发现任何碰撞!\n\n您的物流路径规划非常成功物体可以安全通行。",
"仿真完成 - 无碰撞",
System.Windows.MessageBoxButton.OK,
System.Windows.MessageBoxImage.Information);
System.Windows.MessageBoxImage.Information,
autoCloseMilliseconds: 4000);
}
// 统一流程:刷新历史列表并显示报告(无论有无碰撞)
@ -2928,6 +2937,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
try
{
if (DialogHelper.IsTestAutomationModeEnabled)
{
LogManager.Info("[碰撞分析] 自动测试模式下跳过预计算碰撞分析");
SaveCollisionDetectionRecord();
return;
}
// 1. 获取预计算碰撞结果从PathAnimationManager现在缓存帧中已包含碰撞结果
var allResults = _pathAnimationManager?.AllCollisionResults;
if (allResults == null || allResults.Count == 0)
@ -3159,6 +3175,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
string routeId = CurrentPathRoute?.Id ?? "";
if (!DialogHelper.IsTestAutomationModeEnabled)
{
// 🔥 检查是否已有相同配置的检测记录
var existingRecordResult = CheckExistingDetectionRecord();
if (existingRecordResult.HasValue)
@ -3181,6 +3199,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 用户选择重新生成,继续创建新记录
LogManager.Info($"[检测记录] 用户选择重新生成,创建新记录");
}
}
else
{
LogManager.Info("[检测记录] 自动测试模式下跳过重复检测记录检查,直接创建新记录");
}
int recordId = CreateCollisionDetectionRecordSnapshot("动画生成", bindToAnimationManager: true);
@ -3445,6 +3468,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
/// </summary>
private (int id, UserChoice userChoice)? ShowDuplicateConfigDialog(CollisionDetectionRecord record)
{
if (TestAutomationHttpService.ShouldAutoChooseCreateNewDetectionRecord)
{
_lastReusedRecordId = null;
LogManager.Info($"[检测记录] 测试自动化已接管重复配置对话框,自动选择重新生成新记录 (ExistingId={record.Id})");
return (record.Id, UserChoice.CreateNew);
}
var message = $"检测配置已存在!\n\n" +
$"找到一条相同配置的历史检测记录:\n" +
$"• 测试名称:{record.TestName ?? ""}\n" +

View File

@ -551,11 +551,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_documentRotationHintShown.Add(documentKey);
_documentRotationHintPending.Remove(documentKey);
LogManager.Info($"[模型整体旋转提示] {hintMessage}");
System.Windows.MessageBox.Show(
DialogHelper.ShowAutoClosingMessageBox(
hintMessage,
"坐标系提示",
System.Windows.MessageBoxButton.OK,
System.Windows.MessageBoxImage.Information);
System.Windows.MessageBoxImage.Information,
autoCloseMilliseconds: 4000);
}
catch (Exception ex)
{

View File

@ -3,8 +3,10 @@ using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using System.Windows.Media;
using Autodesk.Navisworks.Api;
using NavisworksTransport.Core.Services;
using NavisworksTransport.UI.WPF.ViewModels;
using NavisworksTransport.Utils;
@ -34,6 +36,7 @@ namespace NavisworksTransport.UI.WPF.Views
{
InitializeComponent();
ExcludedObjects = new List<ModelItem>();
Loaded += CollisionAnalysisDialog_Loaded;
// 更新统计信息
UpdateStatsText(hotspots.Count, totalCollisions);
@ -62,6 +65,27 @@ namespace NavisworksTransport.UI.WPF.Views
LogManager.Info($"[碰撞分析] 自动选中 {autoSelectedCount}/{viewModels.Count} 个高频碰撞物体(>100次或>50%");
}
private void CollisionAnalysisDialog_Loaded(object sender, RoutedEventArgs e)
{
if (!TestAutomationHttpService.ShouldAutoConfirmCollisionAnalysisDialogs)
{
return;
}
Dispatcher.BeginInvoke(new Action(() =>
{
if (!IsLoaded || !IsVisible)
{
return;
}
LogManager.Info("[碰撞分析] 测试自动化已接管分析窗口,自动选择继续生成");
ModelHighlightHelper.ClearCategory(PrecomputedHighlightCategory);
SelectedAction = CollisionAnalysisAction.Continue;
Close();
}), DispatcherPriority.Background);
}
/// <summary>
/// 更新统计文本
/// </summary>

View File

@ -2,6 +2,7 @@ using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
using System.Windows.Threading;
using Autodesk.Navisworks.Api;
namespace NavisworksTransport.Utils
@ -11,6 +12,14 @@ namespace NavisworksTransport.Utils
/// </summary>
public static class DialogHelper
{
private const string TestAutomationEnvironmentVariable = "TRANSPORTPLUGIN_TEST_AUTOMATION";
public static bool IsTestAutomationModeEnabled =>
string.Equals(
Environment.GetEnvironmentVariable(TestAutomationEnvironmentVariable),
"1",
StringComparison.OrdinalIgnoreCase);
/// <summary>
/// 从 UserControl 获取父窗口并设置为对话框 Owner
/// </summary>
@ -162,6 +171,159 @@ namespace NavisworksTransport.Utils
return MessageBox.Show(messageBoxText, caption, button, icon);
}
/// <summary>
/// 显示自动关闭的提示窗口,避免在自动化场景中阻塞流程。
/// </summary>
public static void ShowAutoClosingMessageBox(
string messageBoxText,
string caption,
MessageBoxImage icon = MessageBoxImage.Information,
int autoCloseMilliseconds = 4000)
{
if (IsTestAutomationModeEnabled)
{
LogManager.Info($"[DialogHelper] 自动测试模式下跳过自动关闭提示框: {caption}");
return;
}
var owner = FindOwnerWindow();
var window = new Window
{
Title = caption,
Width = 460,
SizeToContent = SizeToContent.Height,
ResizeMode = ResizeMode.NoResize,
WindowStartupLocation = owner != null ? WindowStartupLocation.CenterOwner : WindowStartupLocation.CenterScreen,
ShowInTaskbar = false,
Topmost = true,
Content = BuildAutoClosingDialogContent(messageBoxText, icon, autoCloseMilliseconds)
};
if (owner != null)
{
try
{
window.Owner = owner;
}
catch (InvalidOperationException ex)
{
LogManager.Warning($"[DialogHelper] 设置自动关闭提示框 Owner 失败: {ex.Message}");
}
}
else
{
SetWin32Owner(window);
}
var closeTimer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(Math.Max(500, autoCloseMilliseconds))
};
closeTimer.Tick += (_, __) =>
{
closeTimer.Stop();
if (window.IsVisible)
{
window.Close();
}
};
window.Closed += (_, __) => closeTimer.Stop();
closeTimer.Start();
window.Show();
}
private static FrameworkElement BuildAutoClosingDialogContent(
string messageBoxText,
MessageBoxImage icon,
int autoCloseMilliseconds)
{
string iconGlyph;
switch (icon)
{
case MessageBoxImage.Warning:
iconGlyph = "!";
break;
case MessageBoxImage.Error:
iconGlyph = "X";
break;
case MessageBoxImage.Question:
iconGlyph = "?";
break;
default:
iconGlyph = "i";
break;
}
var root = new Grid
{
Margin = new Thickness(18)
};
root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
root.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
root.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
var iconText = new TextBlock
{
Text = iconGlyph,
FontSize = 22,
FontWeight = FontWeights.Bold,
Margin = new Thickness(0, 0, 14, 0),
VerticalAlignment = VerticalAlignment.Top
};
Grid.SetColumn(iconText, 0);
Grid.SetRow(iconText, 0);
root.Children.Add(iconText);
var messageText = new TextBlock
{
Text = messageBoxText,
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 2, 0, 0),
MinWidth = 320
};
Grid.SetColumn(messageText, 1);
Grid.SetRow(messageText, 0);
root.Children.Add(messageText);
var footerPanel = new DockPanel
{
Margin = new Thickness(0, 16, 0, 0),
LastChildFill = false
};
Grid.SetColumn(footerPanel, 0);
Grid.SetColumnSpan(footerPanel, 2);
Grid.SetRow(footerPanel, 1);
var autoCloseHint = new TextBlock
{
Text = $"此提示将在 {Math.Max(1, autoCloseMilliseconds / 1000)} 秒后自动关闭。",
VerticalAlignment = VerticalAlignment.Center
};
DockPanel.SetDock(autoCloseHint, Dock.Left);
footerPanel.Children.Add(autoCloseHint);
var closeButton = new Button
{
Content = "确定",
MinWidth = 72,
Padding = new Thickness(14, 4, 14, 4),
Margin = new Thickness(16, 0, 0, 0)
};
closeButton.Click += (_, __) =>
{
Window parentWindow = Window.GetWindow(closeButton);
parentWindow?.Close();
};
DockPanel.SetDock(closeButton, Dock.Right);
footerPanel.Children.Add(closeButton);
root.Children.Add(footerPanel);
return root;
}
/// <summary>
/// 获取 Navisworks 主窗口句柄Win32
/// </summary>

7
start-navisworks.bat Normal file
View File

@ -0,0 +1,7 @@
@echo off
setlocal
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\start-navisworks.ps1" %*
if errorlevel 1 exit /b 1
exit /b 0