From 0de096aed04f7be95c8ae1848e7c00f53de4d932 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Thu, 8 Jan 2026 12:50:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0ClashDetective=E7=BB=93?= =?UTF-8?q?=E6=9E=9C=E4=BF=9D=E5=AD=98=E5=88=B0=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=B9=B6=E5=9C=A8UI=E4=B8=AD?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=E7=A2=B0=E6=92=9E=E6=A3=80=E6=B5=8B=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/requirement/todo_features.md | 1 + .../Collision/ClashDetectiveIntegration.cs | 55 ++++ src/Core/PathDatabase.cs | 235 +++++++++++++++++- .../ViewModels/AnimationControlViewModel.cs | 133 ++++++++++ src/UI/WPF/Views/AnimationControlView.xaml | 121 +++++++-- 5 files changed, 515 insertions(+), 30 deletions(-) diff --git a/doc/requirement/todo_features.md b/doc/requirement/todo_features.md index 809535f..c238af1 100644 --- a/doc/requirement/todo_features.md +++ b/doc/requirement/todo_features.md @@ -7,6 +7,7 @@ 1. [ ] (BUG)虚拟车辆模型每次点击都重建 2. [x] (BUG)碰撞结果高亮,应该显示clashdetective检测的结果 3. [x] (优化)对clashdetective的检测结果,进行向上合并,找到集合对象。 +4. [ ] (功能)碰撞检测结果保存数据库,列表展示 ### [2025/12/25] diff --git a/src/Core/Collision/ClashDetectiveIntegration.cs b/src/Core/Collision/ClashDetectiveIntegration.cs index ff60712..e831dfb 100644 --- a/src/Core/Collision/ClashDetectiveIntegration.cs +++ b/src/Core/Collision/ClashDetectiveIntegration.cs @@ -88,6 +88,11 @@ namespace NavisworksTransport /// public event EventHandler CollisionDetected; + /// + /// ClashDetective结果保存到数据库事件 + /// + public event EventHandler ClashDetectiveResultSaved; + private ClashDetectiveIntegration() { _currentCollisions = new List(); @@ -589,6 +594,39 @@ namespace NavisworksTransport // 更新碰撞计数器 _clashDetectiveCollisionCount = clashResults.Count; + // 保存到数据库 + try + { + var pathDatabase = PathPlanningManager.Instance?.GetPathDatabase(); + if (pathDatabase != null) + { + var record = new ClashDetectiveResultRecord + { + TestName = _currentTestName, + PathName = pathName, + RouteId = routeId ?? "", + TestTime = DateTime.Now, + CollisionCount = clashResults.Count, + AnimationCollisionCount = _animationCollisionCount, + FrameRate = frameRate, + Duration = duration, + DetectionGap = detectionGap, + AnimatedObjectName = animatedObjectName, + CreatedAt = DateTime.Now + }; + pathDatabase.SaveClashDetectiveResult(record); + LogManager.Info($"ClashDetective结果已保存到数据库"); + + // 触发结果保存事件,通知UI刷新列表 + ClashDetectiveResultSaved?.Invoke(this, new ClashDetectiveResultSavedEventArgs(pathName, _currentTestName, clashResults.Count)); + } + } + catch (Exception dbEx) + { + LogManager.Error($"保存ClashDetective结果到数据库失败: {dbEx.Message}"); + // 不影响主流程,继续执行 + } + // 第四步:将分组添加到主测试 if (collisionGroup.Children.Count > 0) { @@ -1666,4 +1704,21 @@ namespace NavisworksTransport CollisionCount = results.Count; } } + + /// + /// ClashDetective结果保存事件参数 + /// + public class ClashDetectiveResultSavedEventArgs : EventArgs + { + public string PathName { get; private set; } + public string TestName { get; private set; } + public int CollisionCount { get; private set; } + + public ClashDetectiveResultSavedEventArgs(string pathName, string testName, int collisionCount) + { + PathName = pathName; + TestName = testName; + CollisionCount = collisionCount; + } + } } \ No newline at end of file diff --git a/src/Core/PathDatabase.cs b/src/Core/PathDatabase.cs index 5d7c028..0639036 100644 --- a/src/Core/PathDatabase.cs +++ b/src/Core/PathDatabase.cs @@ -43,9 +43,13 @@ namespace NavisworksTransport ExecuteNonQuery("PRAGMA journal_mode=WAL"); ExecuteNonQuery("PRAGMA foreign_keys=ON"); + // 无论数据库是否为新数据库,都调用CreateTables() + // 因为使用了CREATE TABLE IF NOT EXISTS语法,所以是安全的 + // 这样可以确保所有表都存在,避免在旧数据库中缺少新表的问题 + CreateTables(); + if (isNewDatabase) { - CreateTables(); LogManager.Info($"创建新数据库: {_dbPath}"); } else @@ -152,10 +156,30 @@ namespace NavisworksTransport ) "); + // 6. ClashDetective结果表 + ExecuteNonQuery(@" + CREATE TABLE IF NOT EXISTS ClashDetectiveResults ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + TestName TEXT NOT NULL UNIQUE, + PathName TEXT NOT NULL, + RouteId TEXT NOT NULL, + TestTime DATETIME NOT NULL, + CollisionCount INTEGER NOT NULL, + AnimationCollisionCount INTEGER NOT NULL, + FrameRate INTEGER, + Duration REAL, + DetectionGap REAL, + AnimatedObjectName TEXT, + CreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP + ) + "); + // 创建索引 ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_reports_route ON CollisionReports(RouteId)"); ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_pathpoints_route ON PathPoints(RouteId)"); ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_pathedges_route ON PathEdges(RouteId)"); + ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_clash_path_name ON ClashDetectiveResults(PathName)"); + ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_clash_test_time ON ClashDetectiveResults(TestTime DESC)"); } /// @@ -342,6 +366,183 @@ namespace NavisworksTransport } } + #region ClashDetective结果管理 + + /// + /// 保存ClashDetective碰撞检测结果 + /// + public void SaveClashDetectiveResult(ClashDetectiveResultRecord record) + { + try + { + // 检查TestName是否已存在 + var checkSql = "SELECT COUNT(*) FROM ClashDetectiveResults WHERE TestName = @testName"; + using (var checkCmd = new SQLiteCommand(checkSql, _connection)) + { + checkCmd.Parameters.AddWithValue("@testName", record.TestName); + var count = Convert.ToInt32(checkCmd.ExecuteScalar()); + if (count > 0) + { + throw new InvalidOperationException($"测试名称 '{record.TestName}' 已存在,无法重复保存"); + } + } + + var sql = @" + INSERT INTO ClashDetectiveResults + (TestName, PathName, RouteId, TestTime, CollisionCount, AnimationCollisionCount, + FrameRate, Duration, DetectionGap, AnimatedObjectName, CreatedAt) + VALUES (@testName, @pathName, @routeId, @testTime, @collisionCount, @animationCollisionCount, + @frameRate, @duration, @detectionGap, @animatedObjectName, @createdAt) + "; + + using (var cmd = new SQLiteCommand(sql, _connection)) + { + cmd.Parameters.AddWithValue("@testName", record.TestName); + cmd.Parameters.AddWithValue("@pathName", record.PathName); + cmd.Parameters.AddWithValue("@routeId", record.RouteId); + cmd.Parameters.AddWithValue("@testTime", record.TestTime); + cmd.Parameters.AddWithValue("@collisionCount", record.CollisionCount); + cmd.Parameters.AddWithValue("@animationCollisionCount", record.AnimationCollisionCount); + cmd.Parameters.AddWithValue("@frameRate", record.FrameRate); + cmd.Parameters.AddWithValue("@duration", record.Duration); + cmd.Parameters.AddWithValue("@detectionGap", record.DetectionGap); + cmd.Parameters.AddWithValue("@animatedObjectName", record.AnimatedObjectName ?? ""); + cmd.Parameters.AddWithValue("@createdAt", record.CreatedAt); + cmd.ExecuteNonQuery(); + } + + LogManager.Info($"ClashDetective结果已保存: 测试={record.TestName}, 碰撞数={record.CollisionCount}"); + } + catch (Exception ex) + { + LogManager.Error($"保存ClashDetective结果失败: {ex.Message}", ex); + throw; + } + } + + /// + /// 获取路径的所有ClashDetective检测结果 + /// + public List GetClashDetectiveResultsByPath(string pathName) + { + var results = new List(); + + try + { + var sql = @" + SELECT Id, TestName, PathName, RouteId, TestTime, CollisionCount, AnimationCollisionCount, + FrameRate, Duration, DetectionGap, AnimatedObjectName, CreatedAt + FROM ClashDetectiveResults + WHERE PathName = @pathName + ORDER BY TestTime DESC + "; + + using (var cmd = new SQLiteCommand(sql, _connection)) + { + cmd.Parameters.AddWithValue("@pathName", pathName); + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + results.Add(new ClashDetectiveResultRecord + { + Id = Convert.ToInt32(reader["Id"]), + TestName = reader["TestName"].ToString(), + PathName = reader["PathName"].ToString(), + RouteId = reader["RouteId"]?.ToString(), + TestTime = Convert.ToDateTime(reader["TestTime"]), + CollisionCount = Convert.ToInt32(reader["CollisionCount"]), + AnimationCollisionCount = Convert.ToInt32(reader["AnimationCollisionCount"]), + FrameRate = reader["FrameRate"] != DBNull.Value ? Convert.ToInt32(reader["FrameRate"]) : 0, + Duration = reader["Duration"] != DBNull.Value ? Convert.ToDouble(reader["Duration"]) : 0.0, + DetectionGap = reader["DetectionGap"] != DBNull.Value ? Convert.ToDouble(reader["DetectionGap"]) : 0.0, + AnimatedObjectName = reader["AnimatedObjectName"]?.ToString(), + CreatedAt = Convert.ToDateTime(reader["CreatedAt"]) + }); + } + } + } + + LogManager.Debug($"获取到 {results.Count} 条ClashDetective结果记录"); + } + catch (Exception ex) + { + LogManager.Error($"获取ClashDetective结果失败: {ex.Message}", ex); + } + + return results; + } + + /// + /// 获取路径最新的ClashDetective检测结果 + /// + public ClashDetectiveResultRecord GetLatestClashDetectiveResult(string pathName) + { + try + { + var sql = @" + SELECT Id, TestName, PathName, RouteId, TestTime, CollisionCount, AnimationCollisionCount, + FrameRate, Duration, DetectionGap, AnimatedObjectName, CreatedAt + FROM ClashDetectiveResults + WHERE PathName = @pathName + ORDER BY TestTime DESC + LIMIT 1 + "; + + using (var cmd = new SQLiteCommand(sql, _connection)) + { + cmd.Parameters.AddWithValue("@pathName", pathName); + using (var reader = cmd.ExecuteReader()) + { + if (reader.Read()) + { + return new ClashDetectiveResultRecord + { + Id = Convert.ToInt32(reader["Id"]), + TestName = reader["TestName"].ToString(), + PathName = reader["PathName"].ToString(), + RouteId = reader["RouteId"]?.ToString(), + TestTime = Convert.ToDateTime(reader["TestTime"]), + CollisionCount = Convert.ToInt32(reader["CollisionCount"]), + AnimationCollisionCount = Convert.ToInt32(reader["AnimationCollisionCount"]), + FrameRate = reader["FrameRate"] != DBNull.Value ? Convert.ToInt32(reader["FrameRate"]) : 0, + Duration = reader["Duration"] != DBNull.Value ? Convert.ToDouble(reader["Duration"]) : 0.0, + DetectionGap = reader["DetectionGap"] != DBNull.Value ? Convert.ToDouble(reader["DetectionGap"]) : 0.0, + AnimatedObjectName = reader["AnimatedObjectName"]?.ToString(), + CreatedAt = Convert.ToDateTime(reader["CreatedAt"]) + }; + } + } + } + } + catch (Exception ex) + { + LogManager.Error($"获取最新ClashDetective结果失败: {ex.Message}", ex); + } + + return null; + } + + /// + /// 删除ClashDetective检测结果 + /// + public void DeleteClashDetectiveResult(int id) + { + try + { + ExecuteNonQuery("DELETE FROM ClashDetectiveResults WHERE Id=@id", + new { id = id }); + LogManager.Info($"已删除ClashDetective结果记录: Id={id}"); + } + catch (Exception ex) + { + LogManager.Error($"删除ClashDetective结果失败: {ex.Message}", ex); + throw; + } + } + + #endregion + /// /// 保存分析评分结果 /// @@ -836,9 +1037,37 @@ namespace NavisworksTransport /// public void Dispose() { - _connection?.Close(); - _connection?.Dispose(); + Dispose(true); + GC.SuppressFinalize(this); } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _connection?.Close(); + _connection?.Dispose(); + } + } + } + + /// + /// ClashDetective碰撞检测结果记录 + /// + public class ClashDetectiveResultRecord + { + public int Id { get; set; } + public string TestName { get; set; } + public string PathName { get; set; } + public string RouteId { get; set; } + public DateTime TestTime { get; set; } + public int CollisionCount { get; set; } + public int AnimationCollisionCount { get; set; } + public int FrameRate { get; set; } + public double Duration { get; set; } + public double DetectionGap { get; set; } + public string AnimatedObjectName { get; set; } + public DateTime CreatedAt { get; set; } } /// diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs index f8576d1..0cfeb6f 100644 --- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs +++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs @@ -77,6 +77,56 @@ namespace NavisworksTransport.UI.WPF.ViewModels public Guid InstanceGuid { get; } } + /// + /// ClashDetective碰撞检测结果视图模型 + /// + public class ClashDetectiveResultViewModel + { + private readonly Action _refreshCallback; + + public ClashDetectiveResultViewModel(ClashDetectiveResultRecord record, Action refreshCallback) + { + Record = record ?? throw new ArgumentNullException(nameof(record)); + _refreshCallback = refreshCallback ?? throw new ArgumentNullException(nameof(refreshCallback)); + ViewCommand = new RelayCommand(() => ExecuteView()); + ReportCommand = new RelayCommand(() => ExecuteReport()); + DeleteCommand = new RelayCommand(() => ExecuteDelete()); + } + + public ClashDetectiveResultRecord Record { get; } + public string TestTimeDisplay => Record.TestTime.ToString("HH:mm:ss"); + public string CollisionCountDisplay => $"{Record.CollisionCount}个"; + public ICommand ViewCommand { get; } + public ICommand ReportCommand { get; } + public ICommand DeleteCommand { get; } + + private void ExecuteView() + { + // 高亮显示该次检测的碰撞对象 + var clashIntegration = ClashDetectiveIntegration.Instance; + clashIntegration?.HighlightClashDetectiveResults(Record.TestName); + } + + private void ExecuteReport() + { + // 打开该次检测的详细报告 + var command = GenerateCollisionReportCommand.CreateComprehensive(autoHighlight: false); + command.ExecuteAsync(); + } + + private void ExecuteDelete() + { + // 删除该次检测记录 + var pathDatabase = PathPlanningManager.Instance?.GetPathDatabase(); + if (pathDatabase != null) + { + pathDatabase.DeleteClashDetectiveResult(Record.Id); + // 通过回调刷新列表 + _refreshCallback?.Invoke(); + } + } + } + /// /// 动画控制视图模型 /// 专门处理路径动画播放和碰撞检测功能 @@ -351,6 +401,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels OnPropertyChanged(nameof(MovementSpeed)); OnPropertyChanged(nameof(PathLength)); UpdateCanGenerateAnimation(); + + // 路径改变时,刷新ClashDetective结果列表 + RefreshClashDetectiveResultsList(); } } } @@ -562,6 +615,16 @@ namespace NavisworksTransport.UI.WPF.ViewModels #endregion + #region ClashDetective结果管理 + + private ObservableCollection _clashDetectiveResults = new ObservableCollection(); + + public ObservableCollection ClashDetectiveResults => _clashDetectiveResults; + + public ICommand RefreshClashDetectiveResultsCommand { get; private set; } + + #endregion + #region 媒体控制属性 /// @@ -706,6 +769,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels // 订阅碰撞检测事件 _clashIntegration.CollisionDetected += OnCollisionDetected; + _clashIntegration.ClashDetectiveResultSaved += OnClashDetectiveResultSaved; // 订阅文档状态事件 DocumentStateManager.Instance.DocumentInvalidated += OnDocumentInvalidated; @@ -786,6 +850,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels ClearCollisionHighlightsCommand = new RelayCommand(ExecuteClearCollisionHighlights, () => HasCollisionResults); HighlightClashDetectiveResultsCommand = new RelayCommand(ExecuteHighlightClashDetectiveResults, () => HasClashDetectiveResults); ClearClashDetectiveHighlightsCommand = new RelayCommand(ExecuteClearClashDetectiveHighlights, () => HasClashDetectiveResults); + RefreshClashDetectiveResultsCommand = new RelayCommand(ExecuteRefreshClashDetectiveResults); PlayForwardCommand = new RelayCommand(ExecutePlayForward, CanExecuteMediaCommands); PlayReverseCommand = new RelayCommand(ExecutePlayReverse, CanExecuteMediaCommands); StepForwardCommand = new RelayCommand(ExecuteStepForward, CanExecuteMediaCommands); @@ -1408,6 +1473,23 @@ namespace NavisworksTransport.UI.WPF.ViewModels } } + /// + /// ClashDetective结果保存事件处理 + /// + private void OnClashDetectiveResultSaved(object sender, ClashDetectiveResultSavedEventArgs e) + { + try + { + // 刷新ClashDetective结果列表 + RefreshClashDetectiveResultsList(); + LogManager.Info($"ClashDetective结果已保存,列表已刷新: 路径={e.PathName}, 测试={e.TestName}, 碰撞数={e.CollisionCount}"); + } + catch (Exception ex) + { + LogManager.Error($"处理ClashDetective结果保存事件失败: {ex.Message}", ex); + } + } + /// /// 保存碰撞报告到数据库 /// @@ -1648,6 +1730,56 @@ namespace NavisworksTransport.UI.WPF.ViewModels UpdateMainStatus("已清除ClashDetective高亮"); } + /// + /// 刷新ClashDetective结果列表 + /// + private void RefreshClashDetectiveResultsList() + { + try + { + var pathName = CurrentPathRoute?.Name; + if (string.IsNullOrEmpty(pathName)) + { + _clashDetectiveResults.Clear(); + HasClashDetectiveResults = false; + return; + } + + var pathDatabase = PathPlanningManager.Instance?.GetPathDatabase(); + if (pathDatabase == null) + { + _clashDetectiveResults.Clear(); + HasClashDetectiveResults = false; + return; + } + + var records = pathDatabase.GetClashDetectiveResultsByPath(pathName); + _clashDetectiveResults.Clear(); + + foreach (var record in records) + { + _clashDetectiveResults.Add(new ClashDetectiveResultViewModel(record, RefreshClashDetectiveResultsList)); + } + + // 更新HasClashDetectiveResults属性 + HasClashDetectiveResults = _clashDetectiveResults.Count > 0; + + UpdateMainStatus($"已刷新ClashDetective结果列表:{_clashDetectiveResults.Count}条记录"); + LogManager.Info($"ClashDetective结果列表已刷新:{_clashDetectiveResults.Count}条记录"); + } + catch (Exception ex) + { + LogManager.Error($"刷新ClashDetective结果列表失败: {ex.Message}"); + _clashDetectiveResults.Clear(); + HasClashDetectiveResults = false; + } + } + + private void ExecuteRefreshClashDetectiveResults() + { + RefreshClashDetectiveResultsList(); + } + #endregion /// @@ -2598,6 +2730,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels try { _clashIntegration.CollisionDetected -= OnCollisionDetected; + _clashIntegration.ClashDetectiveResultSaved -= OnClashDetectiveResultSaved; LogManager.Debug("碰撞检测事件订阅取消完成"); } catch (Exception eventEx) diff --git a/src/UI/WPF/Views/AnimationControlView.xaml b/src/UI/WPF/Views/AnimationControlView.xaml index 63f2cae..4f860e5 100644 --- a/src/UI/WPF/Views/AnimationControlView.xaml +++ b/src/UI/WPF/Views/AnimationControlView.xaml @@ -399,22 +399,105 @@ NavisworksTransport 检测动画页签视图 - 采用与类别设置和分层管 IsEnabled="{Binding HasClashDetectiveResults}" Style="{StaticResource SecondaryButtonStyle}"/> - - - - - + + + + + + + + + +