新增路径处理工具类,支持截图生成和路径计算功能;更新碰撞报告和导航地图对话框,增加截图预览和重新截图功能

This commit is contained in:
tian 2026-01-22 20:28:37 +08:00
parent f501b40dc4
commit af4b1538ab
8 changed files with 394 additions and 6 deletions

View File

@ -283,6 +283,7 @@
<Compile Include="src\Utils\VisibilityHelper.cs" />
<Compile Include="src\Utils\ModelItemTransformHelper.cs" />
<Compile Include="src\Utils\CachedTriangle3D.cs" />
<Compile Include="src\Utils\PathHelper.cs" />
<!-- Assembly Info -->
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>

View File

@ -2,6 +2,14 @@
## 功能点
### [2026/1/18]
1. [x] 功能实现吊装路径支持3步吊运过程
2. [x] (功能)给移动到起点的虚拟车辆或物体增加调整角度能力
3. [x] (优化)在自动路径规划的几何体获取过程中,过滤隐藏项,提高性能
4. [x] (功能)在碰撞检测报告中,增加碰撞结果截图
5. [x] (功能)实现多条路径碰撞检测的批处理
### [2026/1/13]
1. [x] 优化检查API调用的线程安全问题提高插件的稳定性

View File

@ -86,6 +86,12 @@ namespace NavisworksTransport.Commands
public int FrameRate { get; set; }
public double Duration { get; set; }
public double DetectionGap { get; set; }
// 新增:截图信息
public string ScreenshotPath { get; set; }
public string ScreenshotFormat { get; set; }
public int ScreenshotWidth { get; set; }
public int ScreenshotHeight { get; set; }
}
/// <summary>
@ -228,6 +234,34 @@ namespace NavisworksTransport.Commands
}
}
UpdateProgress(95, "生成碰撞场景截图...");
// 自动生成默认截图1920x1080, JPG格式
try
{
string screenshotPath = PathHelper.GenerateSceneScreenshot(
result.PathName ?? "collision",
1920,
1080,
System.Drawing.Imaging.ImageFormat.Jpeg,
"collision"
);
if (screenshotPath != null)
{
result.ScreenshotPath = screenshotPath;
result.ScreenshotFormat = "JPG";
result.ScreenshotWidth = 1920;
result.ScreenshotHeight = 1080;
LogManager.Info($"碰撞场景截图已自动生成: {screenshotPath}");
}
}
catch (Exception ex)
{
LogManager.Error($"自动生成碰撞场景截图失败: {ex.Message}");
// 截图失败不影响报告生成
}
UpdateProgress(100, "报告生成完成");
// 显示报告UI线程

View File

@ -9,6 +9,8 @@ using System.IO;
using System.Text;
using Microsoft.Win32;
using System.Threading.Tasks;
using System.Windows.Media.Imaging;
using NavisworksTransport.UI.WPF.Views;
namespace NavisworksTransport.UI.WPF.ViewModels
{
@ -80,6 +82,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private int _frameRate;
private double _duration;
private double _detectionGap;
private CollisionReportResult _currentReport;
#endregion
@ -239,6 +242,46 @@ namespace NavisworksTransport.UI.WPF.ViewModels
set => SetProperty(ref _detectionGap, value);
}
/// <summary>
/// 当前报告结果
/// </summary>
public CollisionReportResult CurrentReport
{
get => _currentReport;
set => SetProperty(ref _currentReport, value);
}
/// <summary>
/// 截图预览图像源
/// </summary>
public BitmapImage ScreenshotPreviewSource
{
get
{
if (string.IsNullOrEmpty(CurrentReport?.ScreenshotPath) ||
!File.Exists(CurrentReport.ScreenshotPath))
{
return null;
}
try
{
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.UriSource = new Uri(CurrentReport.ScreenshotPath);
bitmap.EndInit();
bitmap.Freeze(); // 防止跨线程访问问题
return bitmap;
}
catch (Exception ex)
{
LogManager.Error($"加载截图预览失败: {ex.Message}");
return null;
}
}
}
#endregion
#region
@ -253,6 +296,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels
/// </summary>
public ICommand CloseCommand { get; }
/// <summary>
/// 重新截图命令
/// </summary>
public ICommand RetakeScreenshotCommand { get; }
/// <summary>
/// 高亮碰撞对象命令
@ -275,6 +322,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
ExportReportCommand = new RelayCommand(async () => await ExportReportAsync(), () => !IsGenerating && HasCollisions);
CloseCommand = new RelayCommand(CloseWindow);
HighlightCollisionCommand = new RelayCommand<CollisionReportItem>(HighlightCollision, item => item?.CollisionData != null);
RetakeScreenshotCommand = new RelayCommand(ExecuteRetakeScreenshot, CanExecuteRetakeScreenshot);
// 初始化状态
ReportStatus = CollisionReportStatus.Generating;
@ -299,6 +347,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
return;
}
// 保存当前报告结果
CurrentReport = reportResult;
try
{
IsGenerating = true;
@ -572,7 +623,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
await Task.Run(() =>
{
var content = GenerateExportContent(Path.GetExtension(saveFileDialog.FileName).ToLower());
var content = GenerateExportContent(Path.GetExtension(saveFileDialog.FileName).ToLower(), saveFileDialog.FileName);
File.WriteAllText(saveFileDialog.FileName, content, Encoding.UTF8);
});
@ -590,11 +641,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
/// <summary>
/// 生成导出内容
/// </summary>
private string GenerateExportContent(string fileExtension)
private string GenerateExportContent(string fileExtension, string filePath = null)
{
if (fileExtension == ".html")
{
return GenerateHtmlReport();
return GenerateHtmlReport(filePath);
}
else
{
@ -703,7 +754,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
/// <summary>
/// 生成HTML报告
/// </summary>
private string GenerateHtmlReport()
private string GenerateHtmlReport(string htmlFilePath = null)
{
var html = new StringBuilder();
var now = DateTime.Now;
@ -731,6 +782,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels
html.AppendLine(".collision-status { display: inline-block; padding: 3px 8px; border-radius: 12px; font-size: 11px; color: white; margin-bottom: 8px; }");
html.AppendLine(".status-new { background-color: #ff6b6b; } .status-active { background-color: #ffa500; }");
html.AppendLine(".status-reviewed { background-color: #87ceeb; } .status-approved { background-color: #98fb98; } .status-resolved { background-color: #90ee90; }");
html.AppendLine(".screenshot-section { margin: 20px 0; padding: 15px; background-color: #f5f5f5; border-radius: 5px; }");
html.AppendLine(".screenshot-container { text-align: center; }");
html.AppendLine(".screenshot-image { max-width: 100%; height: auto; border: 1px solid #ddd; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }");
html.AppendLine(".screenshot-info { margin-top: 10px; font-size: 12px; color: #666; }");
html.AppendLine("</style>");
html.AppendLine("</head><body>");
@ -776,6 +831,25 @@ namespace NavisworksTransport.UI.WPF.ViewModels
html.AppendLine("</div>");
html.AppendLine("</div>");
// 碰撞场景截图
if (!string.IsNullOrEmpty(CurrentReport?.ScreenshotPath) && File.Exists(CurrentReport.ScreenshotPath))
{
html.AppendLine("<h2>碰撞场景截图</h2>");
html.AppendLine("<div class='screenshot-section'>");
// 使用 PathHelper 计算相对路径
string relativePath = PathHelper.GetRelativePath(htmlFilePath, CurrentReport.ScreenshotPath);
html.AppendLine("<div class='screenshot-container'>");
html.AppendLine($"<img src=\"{relativePath}\" alt=\"碰撞场景截图\" class=\"screenshot-image\"/>");
html.AppendLine("<div class='screenshot-info'>");
html.AppendLine($"<p>分辨率: {CurrentReport.ScreenshotWidth} x {CurrentReport.ScreenshotHeight}</p>");
html.AppendLine($"<p>格式: {CurrentReport.ScreenshotFormat}</p>");
html.AppendLine("</div>");
html.AppendLine("</div>");
html.AppendLine("</div>");
}
// 运动构件信息
if (!string.IsNullOrEmpty(MovingObjectInfo))
{
@ -903,6 +977,65 @@ namespace NavisworksTransport.UI.WPF.ViewModels
LogManager.Info("关闭碰撞报告窗口");
}
/// <summary>
/// 执行重新截图
/// </summary>
private void ExecuteRetakeScreenshot()
{
try
{
// 复用现有的截图配置对话框
var dialog = new GenerateNavigationMapDialog();
dialog.Title = "碰撞场景截图配置";
// 尝试设置 Owner如果主窗口已显示
var mainWindow = System.Windows.Application.Current.MainWindow;
if (mainWindow != null && mainWindow.IsLoaded)
{
dialog.Owner = mainWindow;
}
if (dialog.ShowDialog() == true)
{
// 调用 PathHelper 生成截图
string screenshotPath = PathHelper.GenerateSceneScreenshot(
CurrentReport.PathName ?? "collision",
dialog.ImageWidth,
dialog.ImageHeight,
dialog.ImageFormat,
"collision"
);
if (screenshotPath != null)
{
// 更新报告数据
CurrentReport.ScreenshotPath = screenshotPath;
CurrentReport.ScreenshotFormat = PathHelper.ImageFormatToExtension(dialog.ImageFormat).ToUpper();
CurrentReport.ScreenshotWidth = dialog.ImageWidth;
CurrentReport.ScreenshotHeight = dialog.ImageHeight;
// 通知UI更新
OnPropertyChanged(nameof(CurrentReport));
OnPropertyChanged(nameof(ScreenshotPreviewSource));
LogManager.Info("碰撞场景截图已更新");
}
}
}
catch (Exception ex)
{
LogManager.Error($"重新截图失败: {ex.Message}");
System.Windows.MessageBox.Show($"重新截图失败: {ex.Message}", "错误", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
} }
/// <summary>
/// 检查是否可以执行重新截图
/// </summary>
private bool CanExecuteRetakeScreenshot()
{
return CurrentReport != null;
}
#endregion
}
}

View File

@ -269,6 +269,32 @@ NavisworksTransport 碰撞检测报告对话框 - 采用与主界面一致的Nav
</Border>
</UniformGrid>
<!-- 碰撞场景截图区域 -->
<Label Content="碰撞场景截图" Style="{StaticResource SectionHeaderStyle}"/>
<GroupBox Margin="0,0,0,10" BorderBrush="#FFD4E7FF" BorderThickness="1">
<Grid>
<!-- 截图预览 -->
<Border BorderBrush="Gray"
BorderThickness="1"
Background="Black"
MinHeight="300">
<Image Source="{Binding ScreenshotPreviewSource}"
Stretch="Uniform"
RenderOptions.BitmapScalingMode="HighQuality">
<Image.Style>
<Style TargetType="Image">
<Style.Triggers>
<DataTrigger Binding="{Binding ScreenshotPreviewSource}" Value="{x:Null}">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
</Border>
</Grid>
</GroupBox>
<!-- 运动构件信息 -->
<Border Style="{StaticResource StatisticItemStyle}"
Margin="0,0,0,10"
@ -380,6 +406,11 @@ NavisworksTransport 碰撞检测报告对话框 - 采用与主界面一致的Nav
Margin="0,0,10,0"
Visibility="{Binding HasCollisions, Converter={StaticResource BooleanToVisibilityConverter}}"/>
<Button Content="重新截图"
Command="{Binding RetakeScreenshotCommand}"
Style="{StaticResource SecondaryButtonStyle}"
Margin="0,0,10,0"/>
<Button Content="关闭"
Click="CloseButton_Click"
Style="{StaticResource ActionButtonStyle}"/>

View File

@ -20,7 +20,8 @@ NavisworksTransport 生成导航地图对话框 - 采用与主界面一致的Nav
Width="460"
ResizeMode="NoResize"
WindowStartupLocation="CenterOwner"
ShowInTaskbar="False">
ShowInTaskbar="True"
Topmost="True">
<Window.Resources>
<!-- 引用共享的Navisworks 2026样式资源 -->
@ -85,6 +86,7 @@ NavisworksTransport 生成导航地图对话框 - 采用与主界面一致的Nav
<ComboBox Name="PresetSizeComboBox" Width="180" Height="24" SelectedIndex="0"
SelectionChanged="PresetSizeComboBox_SelectionChanged">
<ComboBoxItem Content="自定义" Tag="Custom"/>
<ComboBoxItem Content="3840 x 2160 (4K)" Tag="3840x2160"/>
<ComboBoxItem Content="1920 x 1080 (Full HD)" Tag="1920x1080"/>
<ComboBoxItem Content="1280 x 720 (HD)" Tag="1280x720"/>
<ComboBoxItem Content="1024 x 768 (4:3)" Tag="1024x768"/>

View File

@ -60,7 +60,7 @@ namespace NavisworksTransport.UI.WPF.Views
// 设置默认选择
FormatComboBox.SelectedIndex = 1; // PNG
PresetSizeComboBox.SelectedIndex = 1; // 1920x1080
PresetSizeComboBox.SelectedIndex = 2; // 1920x1080 (因为添加了4K选项索引变为2)
WidthTextBox.Text = ImageWidth.ToString();
HeightTextBox.Text = ImageHeight.ToString();

179
src/Utils/PathHelper.cs Normal file
View File

@ -0,0 +1,179 @@
using System;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using Autodesk.Navisworks.Api;
namespace NavisworksTransport.Utils
{
/// <summary>
/// 路径处理工具类 - 提供文件路径相关的通用方法
/// </summary>
public static class PathHelper
{
/// <summary>
/// 获取插件目录路径
/// </summary>
public static string GetPluginDirectory()
{
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
"Autodesk",
"Navisworks Manage 2026",
"plugins",
"NavisworksTransportPlugin"
);
}
/// <summary>
/// 获取截图目录路径
/// </summary>
public static string GetScreenshotDirectory()
{
return Path.Combine(GetPluginDirectory(), "screenshots");
}
/// <summary>
/// 确保目录存在,不存在则创建
/// </summary>
public static void EnsureDirectoryExists(string directory)
{
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
}
/// <summary>
/// 清理文件名中的非法字符
/// </summary>
/// <param name="fileName">原始文件名</param>
/// <returns>清理后的文件名</returns>
public static string SanitizeFileName(string fileName)
{
if (string.IsNullOrEmpty(fileName))
{
return "unnamed";
}
var invalidChars = Path.GetInvalidFileNameChars();
var cleanedFileName = new string(fileName.Where(c => !invalidChars.Contains(c)).ToArray());
// 如果清理后为空,返回默认名称
return string.IsNullOrWhiteSpace(cleanedFileName) ? "unnamed" : cleanedFileName;
}
/// <summary>
/// 生成带时间戳的文件名
/// </summary>
/// <param name="prefix">文件名前缀</param>
/// <param name="extension">文件扩展名(不含点号)</param>
/// <returns>带时间戳的文件名</returns>
public static string GenerateTimestampedFileName(string prefix, string extension)
{
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
return $"{prefix}_{timestamp}.{extension}";
}
/// <summary>
/// 计算从HTML文件到目标文件的相对路径
/// </summary>
/// <param name="fromPath">HTML文件路径</param>
/// <param name="toPath">目标文件路径</param>
/// <returns>相对路径(使用正斜杠)</returns>
public static string GetRelativePath(string fromPath, string toPath)
{
try
{
var fromUri = new Uri(fromPath);
var toUri = new Uri(toPath);
var relativeUri = fromUri.MakeRelativeUri(toUri);
return Uri.UnescapeDataString(relativeUri.ToString()).Replace('\\', '/');
}
catch (Exception ex)
{
LogManager.Error($"计算相对路径失败: {ex.Message}");
return toPath; // 失败时返回绝对路径
}
}
/// <summary>
/// 将ImageFormat转换为文件扩展名
/// </summary>
public static string ImageFormatToExtension(ImageFormat format)
{
if (format.Equals(ImageFormat.Jpeg))
return "jpg";
if (format.Equals(ImageFormat.Png))
return "png";
if (format.Equals(ImageFormat.Bmp))
return "bmp";
return "png"; // 默认
}
/// <summary>
/// 将文件扩展名转换为ImageFormat
/// </summary>
public static ImageFormat ExtensionToImageFormat(string extension)
{
if (string.IsNullOrEmpty(extension))
return ImageFormat.Png;
extension = extension.ToLowerInvariant();
if (extension == ".jpg" || extension == ".jpeg")
return ImageFormat.Jpeg;
if (extension == ".png")
return ImageFormat.Png;
if (extension == ".bmp")
return ImageFormat.Bmp;
return ImageFormat.Png; // 默认
}
/// <summary>
/// 生成场景截图(通用方法)
/// </summary>
/// <param name="sceneName">场景名称</param>
/// <param name="width">图像宽度</param>
/// <param name="height">图像高度</param>
/// <param name="format">图像格式</param>
/// <param name="prefix">文件名前缀</param>
/// <returns>截图文件路径失败返回null</returns>
public static string GenerateSceneScreenshot(string sceneName, int width, int height, ImageFormat format, string prefix = "scene")
{
try
{
LogManager.Info($"开始生成场景截图: {prefix}_{sceneName}, 尺寸={width}x{height}, 格式={ImageFormatToExtension(format)}");
// 1. 确保截图目录存在
string screenshotDir = GetScreenshotDirectory();
EnsureDirectoryExists(screenshotDir);
// 2. 生成文件名
string extension = ImageFormatToExtension(format);
string sanitizedName = SanitizeFileName(sceneName);
string filename = GenerateTimestampedFileName($"{prefix}_{sanitizedName}", extension);
string screenshotPath = Path.Combine(screenshotDir, filename);
// 3. 调用截图生成器(复用 NavigationMapGenerator
var generator = new Core.NavigationMapGenerator();
var bitmap = generator.GenerateNavigationMapImage(
ImageGenerationStyle.ScenePlusOverlay,
width,
height,
false
);
// 4. 保存截图
generator.SaveNavigationMapImage(bitmap, screenshotPath, format);
LogManager.Info($"场景截图已生成: {screenshotPath}");
return screenshotPath;
}
catch (Exception ex)
{
LogManager.Error($"生成场景截图失败: {ex.Message}", ex);
return null;
}
}
}
}