From 76d277b6c268f959759ddf4a8647d56233ff388d Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Wed, 18 Feb 2026 23:25:39 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=8E=E6=95=B0=E6=8D=AE=E5=BA=93=E4=B8=AD?= =?UTF-8?q?=E6=AF=94=E5=AF=B9=E5=8E=86=E5=8F=B2=E6=A3=80=E6=B5=8B=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=98=AF=E5=90=A6=E9=87=8D=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Core/PathDatabase.cs | 194 ++++++++++++++- .../ViewModels/AnimationControlViewModel.cs | 229 ++++++++++++++---- 2 files changed, 357 insertions(+), 66 deletions(-) diff --git a/src/Core/PathDatabase.cs b/src/Core/PathDatabase.cs index decfcf3..b1757cd 100644 --- a/src/Core/PathDatabase.cs +++ b/src/Core/PathDatabase.cs @@ -193,7 +193,7 @@ namespace NavisworksTransport FrameRate INTEGER NOT NULL DEFAULT 30, DurationSeconds REAL NOT NULL DEFAULT 10.0, -- 碰撞检测参数 - DetectionToleranceMeters REAL NOT NULL DEFAULT 0.05, + DetectionTolerance REAL NOT NULL DEFAULT 0.05, -- 运动物体参数 IsVirtualObject INTEGER NOT NULL DEFAULT 0, AnimatedObjectName TEXT, @@ -292,7 +292,7 @@ namespace NavisworksTransport // 12. 设置数据库版本(SQLite内置user_version) // 版本号格式:主版本*10000 + 次版本*100 + 修订号 // 例如:2.1.3 = 20103 - ExecuteNonQuery("PRAGMA user_version = 20102"); // v2.1.2 - 增加时标配置和事件表 + ExecuteNonQuery("PRAGMA user_version = 20103"); // v2.1.3 - 重命名DetectionToleranceMeters为DetectionTolerance // 创建索引 ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_reports_route ON CollisionReports(RouteId)"); @@ -319,7 +319,7 @@ namespace NavisworksTransport ErrorMessage TEXT, FrameRate INTEGER NOT NULL, DurationSeconds REAL NOT NULL, - DetectionToleranceMeters REAL NOT NULL, + DetectionTolerance REAL NOT NULL, IsVirtualObject INTEGER NOT NULL, VirtualObjectLength REAL, VirtualObjectWidth REAL, @@ -1187,7 +1187,175 @@ namespace NavisworksTransport return results; } + /// + /// 获取检测记录的排除对象列表 + /// + public List GetDetectionExcludedObjects(int detectionRecordId) + { + var results = new List(); + + try + { + var sql = @" + SELECT Id, DetectionRecordId, ModelIndex, PathId, DisplayName, ObjectName, Reason + FROM CollisionDetectionExcludedObjects + WHERE DetectionRecordId = @detectionRecordId + "; + + using (var cmd = new SQLiteCommand(sql, _connection)) + { + cmd.Parameters.AddWithValue("@detectionRecordId", detectionRecordId); + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + results.Add(new CollisionDetectionExcludedObjectRecord + { + Id = Convert.ToInt32(reader["Id"]), + DetectionRecordId = Convert.ToInt32(reader["DetectionRecordId"]), + ModelIndex = reader["ModelIndex"] == DBNull.Value ? 0 : Convert.ToInt32(reader["ModelIndex"]), + PathId = reader["PathId"] == DBNull.Value ? null : reader["PathId"].ToString(), + DisplayName = reader["DisplayName"]?.ToString(), + ObjectName = reader["ObjectName"]?.ToString(), + Reason = reader["Reason"]?.ToString() + }); + } + } + } + } + catch (Exception ex) + { + LogManager.Error($"获取排除对象列表失败: {ex.Message}", ex); + } + + return results; + } + + /// + /// 获取检测记录的手工碰撞目标列表 + /// + public List GetDetectionManualTargets(int detectionRecordId) + { + var results = new List(); + + try + { + var sql = @" + SELECT Id, DetectionRecordId, ModelIndex, PathId, DisplayName, ObjectName + FROM CollisionDetectionManualTargets + WHERE DetectionRecordId = @detectionRecordId + "; + + using (var cmd = new SQLiteCommand(sql, _connection)) + { + cmd.Parameters.AddWithValue("@detectionRecordId", detectionRecordId); + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + results.Add(new CollisionDetectionManualTargetRecord + { + Id = Convert.ToInt32(reader["Id"]), + DetectionRecordId = Convert.ToInt32(reader["DetectionRecordId"]), + ModelIndex = reader["ModelIndex"] == DBNull.Value ? 0 : Convert.ToInt32(reader["ModelIndex"]), + PathId = reader["PathId"] == DBNull.Value ? null : reader["PathId"].ToString(), + DisplayName = reader["DisplayName"]?.ToString(), + ObjectName = reader["ObjectName"]?.ToString() + }); + } + } + } + } + catch (Exception ex) + { + LogManager.Error($"获取手工目标列表失败: {ex.Message}", ex); + } + + return results; + } + #endregion + + /// + /// 通过基础参数查询匹配的检测记录(用于检查配置是否重复) + /// 注意:数值参数使用容差匹配,返回的记录还需要进一步比较排除列表和手工目标 + /// + public List FindDetectionRecordsByBaseParams( + string routeId, + int frameRate, + double durationSeconds, + double detectionTolerance, + bool isVirtualObject, + double? virtualObjectLength, + double? virtualObjectWidth, + double? virtualObjectHeight, + double objectRotationCorrection, + bool isManualMode) + { + var results = new List(); + + try + { + // 使用容差匹配浮动数值参数 + var sql = @" + SELECT cdr.*, pr.Name AS PathName + FROM CollisionDetectionRecords cdr + INNER JOIN PathRoutes pr ON cdr.RouteId = pr.Id + WHERE cdr.RouteId = @routeId + AND cdr.FrameRate = @frameRate + AND ABS(cdr.DurationSeconds - @duration) < 0.01 + AND ABS(cdr.DetectionTolerance - @tolerance) < 0.001 + AND cdr.IsVirtualObject = @isVirtual + AND ABS(cdr.ObjectRotationCorrection - @rotation) < 0.01 + AND cdr.DetectAllObjects = @detectAll + AND cdr.TestName IS NOT NULL + "; + + using (var cmd = new SQLiteCommand(sql, _connection)) + { + cmd.Parameters.AddWithValue("@routeId", routeId); + cmd.Parameters.AddWithValue("@frameRate", frameRate); + cmd.Parameters.AddWithValue("@duration", durationSeconds); + cmd.Parameters.AddWithValue("@tolerance", detectionTolerance); + cmd.Parameters.AddWithValue("@isVirtual", isVirtualObject ? 1 : 0); + cmd.Parameters.AddWithValue("@rotation", objectRotationCorrection); + cmd.Parameters.AddWithValue("@detectAll", isManualMode ? 0 : 1); + + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + var record = ReadCollisionDetectionRecordFromReader(reader); + + // 虚拟物体尺寸比较(如果是虚拟物体) + if (isVirtualObject && virtualObjectLength.HasValue) + { + if (!record.VirtualObjectLength.HasValue || + Math.Abs(record.VirtualObjectLength.Value - virtualObjectLength.Value) > 0.01) + continue; + if (!record.VirtualObjectWidth.HasValue || + Math.Abs(record.VirtualObjectWidth.Value - virtualObjectWidth.Value) > 0.01) + continue; + if (!record.VirtualObjectHeight.HasValue || + Math.Abs(record.VirtualObjectHeight.Value - virtualObjectHeight.Value) > 0.01) + continue; + } + + results.Add(record); + } + } + } + + LogManager.Debug($"[FindByBaseParams] 找到 {results.Count} 条基础参数匹配的记录"); + } + catch (Exception ex) + { + LogManager.Error($"通过基础参数查询检测记录失败: {ex.Message}", ex); + } + + return results; + } + /// /// 保存分析评分结果 /// @@ -2086,14 +2254,14 @@ namespace NavisworksTransport { var sql = @" INSERT INTO CollisionDetectionRecords ( - RouteId, CreatedTime, FrameRate, DurationSeconds, DetectionToleranceMeters, + RouteId, CreatedTime, FrameRate, DurationSeconds, DetectionTolerance, IsVirtualObject, AnimatedObjectName, ObjectModelIndex, ObjectPathId, VirtualObjectLength, VirtualObjectWidth, VirtualObjectHeight, DetectAllObjects, ObjectRotationCorrection, Description, TestName, TestTime, CollisionCount, AnimationCollisionCount ) VALUES ( - @RouteId, @CreatedTime, @FrameRate, @DurationSeconds, @DetectionToleranceMeters, + @RouteId, @CreatedTime, @FrameRate, @DurationSeconds, @DetectionTolerance, @IsVirtualObject, @AnimatedObjectName, @ObjectModelIndex, @ObjectPathId, @VirtualObjectLength, @VirtualObjectWidth, @VirtualObjectHeight, @DetectAllObjects, @ObjectRotationCorrection, @Description, @@ -2109,7 +2277,7 @@ namespace NavisworksTransport cmd.Parameters.AddWithValue("@FrameRate", record.FrameRate); cmd.Parameters.AddWithValue("@DurationSeconds", record.DurationSeconds); // 内部使用模型单位,写入数据库时转换为米单位 - cmd.Parameters.AddWithValue("@DetectionToleranceMeters", record.DetectionTolerance * UnitsConverter.GetUnitsToMetersConversionFactor()); + cmd.Parameters.AddWithValue("@DetectionTolerance", record.DetectionTolerance); // 模型单位,不转换 cmd.Parameters.AddWithValue("@IsVirtualObject", record.IsVirtualObject ? 1 : 0); cmd.Parameters.AddWithValue("@AnimatedObjectName", record.AnimatedObjectName ?? ""); cmd.Parameters.AddWithValue("@ObjectModelIndex", record.ObjectModelIndex ?? (object)DBNull.Value); @@ -2417,7 +2585,7 @@ namespace NavisworksTransport FrameRate = Convert.ToInt32(reader["FrameRate"]), DurationSeconds = Convert.ToDouble(reader["DurationSeconds"]), // 数据库中存储米单位,读取时转换为模型单位 - DetectionTolerance = Convert.ToDouble(reader["DetectionToleranceMeters"]) * UnitsConverter.GetMetersToUnitsConversionFactor(), + DetectionTolerance = Convert.ToDouble(reader["DetectionTolerance"]), // 模型单位,不转换 IsVirtualObject = Convert.ToInt32(reader["IsVirtualObject"]) == 1, AnimatedObjectName = reader["AnimatedObjectName"]?.ToString(), ObjectModelIndex = reader["ObjectModelIndex"] != DBNull.Value ? Convert.ToInt32(reader["ObjectModelIndex"]) : (int?)null, @@ -2581,7 +2749,7 @@ namespace NavisworksTransport cmd.CommandText = @" INSERT INTO BatchQueueItems ( RouteId, Status, CreatedTime, StartTime, EndTime, ErrorMessage, - FrameRate, DurationSeconds, DetectionToleranceMeters, + FrameRate, DurationSeconds, DetectionTolerance, IsVirtualObject, VirtualObjectLength, VirtualObjectWidth, VirtualObjectHeight, DetectAllObjects, ClashDetectiveTestName, CollisionCount, ObjectRotationCorrection, @@ -2589,7 +2757,7 @@ namespace NavisworksTransport ) VALUES ( @RouteId, @Status, @CreatedTime, @StartTime, @EndTime, @ErrorMessage, - @FrameRate, @DurationSeconds, @DetectionToleranceMeters, + @FrameRate, @DurationSeconds, @DetectionTolerance, @IsVirtualObject, @VirtualObjectLength, @VirtualObjectWidth, @VirtualObjectHeight, @DetectAllObjects, @ClashDetectiveTestName, @CollisionCount, @ObjectRotationCorrection, @@ -2606,7 +2774,7 @@ namespace NavisworksTransport cmd.Parameters.AddWithValue("@FrameRate", item.FrameRate); cmd.Parameters.AddWithValue("@DurationSeconds", item.DurationSeconds); // 内部使用模型单位,写入数据库时转换为米单位 - cmd.Parameters.AddWithValue("@DetectionToleranceMeters", item.DetectionTolerance * UnitsConverter.GetUnitsToMetersConversionFactor()); + cmd.Parameters.AddWithValue("@DetectionTolerance", item.DetectionTolerance); // 模型单位,不转换 cmd.Parameters.AddWithValue("@IsVirtualObject", item.IsVirtualObject ? 1 : 0); cmd.Parameters.AddWithValue("@VirtualObjectLength", item.VirtualObjectLength); cmd.Parameters.AddWithValue("@VirtualObjectWidth", item.VirtualObjectWidth); @@ -2651,7 +2819,7 @@ namespace NavisworksTransport FrameRate = Convert.ToInt32(reader["FrameRate"]), DurationSeconds = Convert.ToDouble(reader["DurationSeconds"]), // 数据库中存储米单位,读取时转换为模型单位 - DetectionTolerance = Convert.ToDouble(reader["DetectionToleranceMeters"]) * UnitsConverter.GetMetersToUnitsConversionFactor(), + DetectionTolerance = Convert.ToDouble(reader["DetectionTolerance"]), // 模型单位,不转换 IsVirtualObject = isVirtualObject, VirtualObjectLength = Convert.ToDouble(reader["VirtualObjectLength"]), VirtualObjectWidth = Convert.ToDouble(reader["VirtualObjectWidth"]), @@ -2690,7 +2858,7 @@ namespace NavisworksTransport { var sql = @" SELECT bqi.Id, bqi.RouteId, pr.Name AS PathRouteName, pr.PathType, bqi.Status, bqi.CreatedTime, bqi.StartTime, bqi.EndTime, bqi.ErrorMessage, - bqi.FrameRate, bqi.DurationSeconds, bqi.DetectionToleranceMeters, + bqi.FrameRate, bqi.DurationSeconds, bqi.DetectionTolerance, bqi.IsVirtualObject, bqi.VirtualObjectLength, bqi.VirtualObjectWidth, bqi.VirtualObjectHeight, bqi.DetectAllObjects, bqi.ClashDetectiveTestName, bqi.CollisionCount, bqi.ObjectRotationCorrection, @@ -2750,7 +2918,7 @@ namespace NavisworksTransport { cmd.CommandText = @" SELECT bqi.Id, bqi.RouteId, pr.Name AS PathRouteName, pr.PathType, bqi.Status, bqi.CreatedTime, bqi.StartTime, bqi.EndTime, bqi.ErrorMessage, - bqi.FrameRate, bqi.DurationSeconds, bqi.DetectionToleranceMeters, + bqi.FrameRate, bqi.DurationSeconds, bqi.DetectionTolerance, bqi.IsVirtualObject, bqi.VirtualObjectLength, bqi.VirtualObjectWidth, bqi.VirtualObjectHeight, bqi.DetectAllObjects, bqi.ClashDetectiveTestName, bqi.CollisionCount, bqi.ObjectRotationCorrection, diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs index 5021cdd..b69a560 100644 --- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs +++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs @@ -3255,73 +3255,87 @@ namespace NavisworksTransport.UI.WPF.ViewModels /// /// 检查是否已有相同配置的检测记录 - /// 通过动画配置哈希匹配已完成的检测记录 + /// 策略:1.先检查内存缓存 2.SQL过滤基础参数 3.比较排除列表和手工目标 /// /// (记录ID, 用户选择),不存在或用户取消则返回null private (int id, UserChoice userChoice)? CheckExistingDetectionRecord() { try { - // 计算当前动画配置的哈希 - var animatedObject = UseVirtualObject ? VirtualObjectManager.Instance.CurrentVirtualObject : SelectedAnimatedObject; - var route = CurrentPathRoute?.Route; - - if (animatedObject == null || route == null) + var routeId = CurrentPathRoute?.Id; + if (string.IsNullOrEmpty(routeId)) { return null; } - var currentHash = _pathAnimationManager.ComputeAnimationConfigHashFromRoute(animatedObject, route); - - // 查询PathAnimationManager中是否有相同配置的记录 - var existingRecordId = _pathAnimationManager.GetDetectionRecordIdByHash(currentHash); - - if (existingRecordId.HasValue) + var pathDatabase = _pathPlanningManager?.GetPathDatabase(); + if (pathDatabase == null) { - // 验证数据库中该记录是否仍然存在 - var pathDatabase = _pathPlanningManager?.GetPathDatabase(); - var record = pathDatabase?.GetDetectionResultById(existingRecordId.Value); - - if (record != null) + return null; + } + + var animatedObject = UseVirtualObject ? VirtualObjectManager.Instance.CurrentVirtualObject : SelectedAnimatedObject; + if (animatedObject == null) + { + return null; + } + + // 1. 先检查内存缓存(程序运行期间已处理过的) + var currentHash = _pathAnimationManager.ComputeAnimationConfigHashFromRoute(animatedObject, CurrentPathRoute.Route); + var cachedRecordId = _pathAnimationManager.GetDetectionRecordIdByHash(currentHash); + if (cachedRecordId.HasValue) + { + var cachedRecord = pathDatabase.GetDetectionResultById(cachedRecordId.Value); + if (cachedRecord != null) { - LogManager.Info($"[CheckExistingDetectionRecord] 找到相同配置的检测记录: Hash={currentHash.Substring(0, 8)}..., RecordId={existingRecordId}"); - - // 弹出提示框询问用户 - var message = $"检测配置已存在!\n\n" + - $"找到一条相同配置的历史检测记录:\n" + - $"• 测试名称:{record.TestName ?? "未命名"}\n" + - $"• 检测时间:{record.TestTime:yyyy-MM-dd HH:mm:ss}\n" + - $"• 碰撞数量:{record.CollisionCount} 个\n\n" + - $"您可以:\n" + - $"• 【是】直接使用历史记录(跳过重复检测)\n" + - $"• 【否】重新生成新记录"; - - var result = System.Windows.MessageBox.Show( - message, - "检测配置已存在", - System.Windows.MessageBoxButton.YesNo, - System.Windows.MessageBoxImage.Question); - - if (result == System.Windows.MessageBoxResult.Yes) - { - // 用户选择使用历史记录 - _lastReusedRecordId = existingRecordId.Value; - return (existingRecordId.Value, UserChoice.UseExisting); - } - else - { - // 用户选择重新生成 - _lastReusedRecordId = null; - return (existingRecordId.Value, UserChoice.CreateNew); - } - } - else - { - // 记录已被删除,清除缓存 - _pathAnimationManager.RemoveDetectionRecordHash(currentHash); + LogManager.Info($"[CheckExistingDetectionRecord] 内存缓存命中: RecordId={cachedRecordId}"); + return ShowDuplicateConfigDialog(cachedRecord); } } - + + // 2. SQL过滤基础参数匹配的记录 + // 注意:数据库存储的是模型单位(不带Meters后缀),查询时传模型单位 + var factor = UnitsConverter.GetMetersToUnitsConversionFactor(); + var matchingRecords = pathDatabase.FindDetectionRecordsByBaseParams( + routeId, + _animationFrameRate, + AnimationDuration, + _detectionTolerance * factor, // 米 → 模型单位 + UseVirtualObject, + UseVirtualObject ? VirtualObjectLengthInMeters * factor : (double?)null, // 米 → 模型单位 + UseVirtualObject ? VirtualObjectWidthInMeters * factor : (double?)null, + UseVirtualObject ? VirtualObjectHeightInMeters * factor : (double?)null, + _objectRotationCorrection, + IsManualCollisionTargetEnabled + ); + + if (matchingRecords == null || matchingRecords.Count == 0) + { + LogManager.Info("[CheckExistingDetectionRecord] SQL基础参数过滤无匹配"); + return null; + } + + LogManager.Info($"[CheckExistingDetectionRecord] SQL找到 {matchingRecords.Count} 条记录,开始比较排除列表"); + + // 3. 逐个比较排除列表和手工目标 + var currentExcluded = GetCurrentExcludedObjects(); + var currentTargets = GetCurrentManualTargets(); + + foreach (var record in matchingRecords) + { + if (IsExcludedListMatch(record.Id, currentExcluded, pathDatabase) && + IsManualTargetListMatch(record.Id, currentTargets, pathDatabase)) + { + LogManager.Info($"[CheckExistingDetectionRecord] 找到完全匹配: RecordId={record.Id}"); + + // 注册到内存缓存 + _pathAnimationManager.RegisterAnimationHashToRecordId(currentHash, record.Id); + + return ShowDuplicateConfigDialog(record); + } + } + + LogManager.Info("[CheckExistingDetectionRecord] 未找到完全匹配的记录"); return null; } catch (Exception ex) @@ -3331,6 +3345,115 @@ namespace NavisworksTransport.UI.WPF.ViewModels } } + /// + /// 显示重复配置对话框 + /// + private (int id, UserChoice userChoice)? ShowDuplicateConfigDialog(CollisionDetectionRecord record) + { + var message = $"检测配置已存在!\n\n" + + $"找到一条相同配置的历史检测记录:\n" + + $"• 测试名称:{record.TestName ?? "未命名"}\n" + + $"• 检测时间:{record.TestTime:yyyy-MM-dd HH:mm:ss}\n" + + $"• 碰撞数量:{record.CollisionCount} 个\n\n" + + $"您可以:\n" + + $"• 【是】直接使用历史记录(跳过重复检测)\n" + + $"• 【否】重新生成新记录"; + + var result = System.Windows.MessageBox.Show( + message, + "检测配置已存在", + System.Windows.MessageBoxButton.YesNo, + System.Windows.MessageBoxImage.Question); + + if (result == System.Windows.MessageBoxResult.Yes) + { + _lastReusedRecordId = record.Id; + return (record.Id, UserChoice.UseExisting); + } + else + { + _lastReusedRecordId = null; + return (record.Id, UserChoice.CreateNew); + } + } + + /// + /// 获取当前排除对象列表(ModelIndex + PathId) + /// + private List<(int ModelIndex, string PathId)> GetCurrentExcludedObjects() + { + var result = new List<(int, string)>(); + var excludedObjects = _pathAnimationManager?.GetExcludedObjects(); + + if (excludedObjects != null) + { + foreach (var obj in excludedObjects) + { + if (obj == null) continue; + try + { + var pathId = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.CreatePathId(obj); + result.Add((pathId.ModelIndex, pathId.PathId)); + } + catch { } + } + } + + return result.OrderBy(x => x.Item1).ThenBy(x => x.Item2).ToList(); + } + + /// + /// 获取当前手工碰撞目标列表(ModelIndex + PathId) + /// + private List<(int ModelIndex, string PathId)> GetCurrentManualTargets() + { + var result = new List<(int, string)>(); + + if (IsManualCollisionTargetEnabled && _manualCollisionTargets != null) + { + foreach (var target in _manualCollisionTargets) + { + if (target?.ModelItem == null) continue; + try + { + var pathId = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.CreatePathId(target.ModelItem); + result.Add((pathId.ModelIndex, pathId.PathId)); + } + catch { } + } + } + + return result.OrderBy(x => x.Item1).ThenBy(x => x.Item2).ToList(); + } + + /// + /// 比较排除列表是否匹配 + /// + private bool IsExcludedListMatch(int recordId, List<(int ModelIndex, string PathId)> currentList, PathDatabase db) + { + var dbList = db.GetDetectionExcludedObjects(recordId) + .Select(e => (e.ModelIndex, e.PathId)) + .OrderBy(x => x.ModelIndex) + .ThenBy(x => x.PathId) + .ToList(); + + return dbList.SequenceEqual(currentList); + } + + /// + /// 比较手工目标列表是否匹配 + /// + private bool IsManualTargetListMatch(int recordId, List<(int ModelIndex, string PathId)> currentList, PathDatabase db) + { + var dbList = db.GetDetectionManualTargets(recordId) + .Select(t => (t.ModelIndex, t.PathId)) + .OrderBy(x => x.ModelIndex) + .ThenBy(x => x.PathId) + .ToList(); + + return dbList.SequenceEqual(currentList); + } + private void ExecuteToggleWireframeMode() { try