From c4cf718502c44dcb86bba4cb06b14ee963c8e697 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Tue, 13 Jan 2026 18:21:54 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BD=BF=E7=94=A8=E5=A4=8D=E5=90=88=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E6=94=B9=E5=96=84=E6=80=A7=E8=83=BD=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E4=B8=94=E9=80=82=E9=85=8DClashdetective=E8=BE=93=E5=87=BA?= =?UTF-8?q?=E7=9A=84=E7=BB=93=E6=9E=9C=EF=BC=88=E5=8F=AA=E5=8C=85=E5=90=AB?= =?UTF-8?q?=E5=AE=9E=E4=BD=93=E8=8A=82=E7=82=B9=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/design/2026/NavisworksAPI使用方法.md | 249 +++++++++++++++++- src/Core/Animation/PathAnimationManager.cs | 27 +- .../Collision/ClashDetectiveIntegration.cs | 169 ++++++------ .../ViewModels/AnimationControlViewModel.cs | 31 ++- src/Utils/ModelHighlightHelper.cs | 3 +- 5 files changed, 346 insertions(+), 133 deletions(-) diff --git a/doc/design/2026/NavisworksAPI使用方法.md b/doc/design/2026/NavisworksAPI使用方法.md index 58ca0a4..4f4ec9f 100644 --- a/doc/design/2026/NavisworksAPI使用方法.md +++ b/doc/design/2026/NavisworksAPI使用方法.md @@ -2607,6 +2607,7 @@ await System.Windows.Application.Current.Dispatcher.InvokeAsync(() => **官方示例参考**: 官方示例文件中的 Progress API 使用案例: + - `ClashDetective/ClashGrouper/ClashGrouperUtils.cs` - 碰撞分组中的进度条使用 - 展示了在复杂算法中如何正确使用 Progress API 和取消支持 @@ -2631,13 +2632,13 @@ await System.Windows.Application.Current.Dispatcher.InvokeAsync(() => 关键发现 1. 每次文档操作都有两个事件: - - 先清空Models(Count=0) - - 再加载新Models(Count>0) - 2. 状态转换清晰: - - 无模型→无模型:初始清理 - - 无模型→有模型:文档加载完成 ✅ 这是我们需要初始化的时机 - - 有模型→无模型:文档关闭/切换 ✅ 这是我们需要清理的时机 - 3. 时间间隔很短:事件成对出现,间隔仅0.1秒左右 + - 先清空Models(Count=0) + - 再加载新Models(Count>0) + 1. 状态转换清晰: + - 无模型→无模型:初始清理 + - 无模型→有模型:文档加载完成 ✅ 这是我们需要初始化的时机 + - 有模型→无模型:文档关闭/切换 ✅ 这是我们需要清理的时机 + 1. 时间间隔很短:事件成对出现,间隔仅0.1秒左右 ### Navisworks Fragment和Path的API分析 @@ -2659,7 +2660,7 @@ await System.Windows.Application.Current.Dispatcher.InvokeAsync(() => - 用于显示的几何体 - 用于显示的外观 - 2. Path概念(第8页) + 1. Path概念(第8页) A Path is the sequence of nodes from the Partition at the root of the scene graph to a particular node within it that uniquely specifies @@ -2671,7 +2672,7 @@ await System.Windows.Application.Current.Dispatcher.InvokeAsync(() => - Path用来唯一标识特定实例 - 但这里的"实例"指的是场景图中的节点实例,不是几何体的不同部分 - 3. 你的地板问题分析 + 1. 你的地板问题分析 真正的问题: @@ -2693,7 +2694,7 @@ await System.Windows.Application.Current.Dispatcher.InvokeAsync(() => - 多实例去重:应该针对不同ModelItem的相同几何体 - 多片段保留:应该保留同一ModelItem的所有Fragment - 4. 解决方案建议 + 1. 解决方案建议 需要修改去重逻辑,区分: @@ -2715,7 +2716,7 @@ await System.Windows.Application.Current.Dispatcher.InvokeAsync(() => ### Setting “Units and Transform” values via Navisworks API -https://blog.autodesk.io/setting-units-and-transform-values-via-navisworks-api/ + DocumentModels models = doc.Models; //Get the required model from DocumentModels @@ -2961,6 +2962,7 @@ CREATE TABLE ClashDetectiveCollisionObjects ( ``` **关键要点**: + - **最小化存储**:只需要 `ModelIndex` 和 `PathId` 两个字段即可恢复对象 - **避免冗余**:不要存储与 PathId 相同的 ModelItemPath 字段 - **保留显示信息**:DisplayName 和 ObjectName 用于 UI 显示和日志,不影响对象恢复 @@ -3097,6 +3099,7 @@ public ModelItem LoadObject(int? modelIndex, string pathId) - 如果 NWD 文件被重新导出或结构改变,PathId 可能失效 2. **批量恢复优化**: + ```csharp // ✅ 批量恢复时,先收集所有 PathId var pathIds = collisionObjects.Select(obj => new ModelItemPathId @@ -3123,6 +3126,7 @@ public ModelItem LoadObject(int? modelIndex, string pathId) ``` 3. **缓存机制**: + ```csharp // ✅ 使用缓存减少重复查询 private Dictionary _modelItemCache = new Dictionary(); @@ -3148,8 +3152,229 @@ public ModelItem LoadObject(int? modelIndex, string pathId) |------|------|------|---------| | **CreatePathId/ResolvePathId** | 官方API,稳定可靠,跨会话有效 | 需要保存两个字段 | 推荐用于所有持久化场景 | | **ModelItem.GetHashCode()** | 简单,单个值 | 跨会话无效,哈希冲突 | 仅用于运行时去重 | + +## 14. Clash Detective API 使用方法 + +### 14.1 Clash Detective 检测行为的关键洞察 ⚠️ 重要 + +基于实际测试验证的核心发现(2026-01-13)。 + +#### 14.1.1 输入 vs 输出的对象类型差异 + +**⚠️ 核心发现:Clash Detective 返回的对象与输入的对象类型不同** + +```csharp +// 🔍 实际测试验证: +// 输入给 Clash Detective 的对象: +var selectionA = new ModelItemCollection { compositeItem }; // IsComposite=True +var selectionB = new ModelItemCollection { compositeItem2 }; // IsComposite=True + +test.SelectionA.Selection.CopyFrom(selectionA); +test.SelectionB.Selection.CopyFrom(selectionB); + +// Clash Detective 返回的对象: +ClashResult result = test.Children[0] as ClashResult; +// result.Item1.IsComposite = False ← 注意:变成了 False +// result.Item2.IsComposite = False ← 注意:变成了 False +// result.Item1.DisplayName = "" ← 注意:名字是空的 +// result.Item2.DisplayName = "" ← 注意:名字是空的 +``` + +**日志验证**: + +``` +[ClashDetective输入] 候选1: Item1=Chair Sitting Square (IsComposite=True), Item2=Chair-Stacking (IsComposite=True) +[ClashDetective结果] 原始碰撞: Item1= (IsComposite=False), Item2= (IsComposite=False) +[ClashDetective结果] 容器映射: Item1=Chair Sitting Square (IsComposite=True), Item2=Chair-Stacking (IsComposite=True) +``` + +#### 14.1.2 Clash Detective 的内部行为 + +**关键理解**: + +1. **输入对象**:我们给 Clash Detective 的是复合对象(`IsComposite=True`) +2. **检测过程**:Clash Detective 深入到几何体级别进行碰撞检测 +3. **返回对象**:Clash Detective 返回的是实际发生碰撞的几何体子节点(`IsComposite=False`) +4. **命名问题**:几何体子节点通常没有 DisplayName,所以返回的名字是空的 + +**为什么这样设计**: + +- Clash Detective 需要精确到几何体级别进行碰撞检测 +- 复合对象可能包含多个几何体子节点 +- 返回具体的几何体节点可以提供更精确的碰撞位置信息 + +#### 14.1.3 正确的处理方法 + +**解决方案:使用 FindNamedParentContainer 映射回复合对象** + +```csharp +// ✅ 正确方法:容器映射 +foreach (var child in collisionGroup.Children) +{ + if (child is ClashResult clashResult) + { + // 1. Clash Detective 返回的是几何体级别的子节点 + var originalItem1 = clashResult.Item1; // IsComposite=False, DisplayName="" + var originalItem2 = clashResult.Item2; // IsComposite=False, DisplayName="" + + // 2. 向上查找有意义的父级容器 + var compositeItem1 = ModelItemAnalysisHelper.FindNamedParentContainer(clashResult.Item1); + var compositeItem2 = ModelItemAnalysisHelper.FindNamedParentContainer(clashResult.Item2); + + // 3. 使用复合对象创建碰撞结果 + var collisionResult = new CollisionResult + { + ClashGuid = clashResult.Guid, + DisplayName = clashResult.DisplayName, + Status = clashResult.Status, + Item1 = compositeItem1, // IsComposite=True, DisplayName="Chair Sitting Square" + Item2 = compositeItem2, // IsComposite=True, DisplayName="Chair-Stacking" + Center = clashResult.Center, + Distance = clashResult.Distance, + CreatedTime = DateTime.Now + }; + } +} +``` + +#### 14.1.4 FindNamedParentContainer 的实现 + +```csharp +// ✅ 向上查找有意义的父级容器 +public static ModelItem FindNamedParentContainer(ModelItem geometryItem) +{ + if (geometryItem == null) + return null; + + ModelItem current = geometryItem; + + // 向上遍历父节点链 + while (current != null) + { + // 如果当前节点有名字且是复合对象,返回它 + if (!string.IsNullOrEmpty(current.DisplayName) && current.IsComposite) + { + return current; + } + + // 继续向上查找 + current = current.Parent; + } + + // 如果没找到,返回原始对象 + return geometryItem; +} +``` + +#### 14.1.5 容器映射的必要性 + +**为什么必须进行容器映射**: + +1. **用户可读性**:复合对象有有意义的名字(如"Chair-Stacking"),几何体子节点没有名字 +2. **高亮显示**:高亮复合对象比高亮几何体子节点更直观 +3. **碰撞报告**:报告需要显示有意义的对象名称 +4. **数据一致性**:预计算使用复合对象,Clash Detective 结果也应该使用复合对象 + +**不进行容器映射的问题**: + +```csharp +// ❌ 错误:直接使用 Clash Detective 返回的对象 +var collisionResult = new CollisionResult +{ + Item1 = clashResult.Item1, // DisplayName="", IsComposite=False + Item2 = clashResult.Item2 // DisplayName="", IsComposite=False +}; + +// 问题: +// 1. 碰撞报告中显示"未命名对象" +// 2. 高亮显示的是几何体子节点(用户看不到) +// 3. 无法与预计算结果对应(预计算使用复合对象) +``` + +#### 14.1.6 性能考虑 + +**容器映射的性能影响**: + +- 每次碰撞结果都需要向上遍历父节点链 +- 对于大型模型,可能影响性能 +- 建议缓存容器映射结果 + +**优化方案**: + +```csharp +// ✅ 使用缓存优化容器映射 +private Dictionary _containerCache = new Dictionary(); + +public ModelItem FindNamedParentContainerCached(ModelItem geometryItem) +{ + if (geometryItem == null) + return null; + + // 检查缓存 + if (_containerCache.TryGetValue(geometryItem, out ModelItem cachedContainer)) + { + return cachedContainer; + } + + // 执行容器映射 + var container = FindNamedParentContainer(geometryItem); + + // 缓存结果 + _containerCache[geometryItem] = container; + + return container; +} +``` + +#### 14.1.7 与 MergeComposites 选项的关系 + +**MergeComposites 选项的作用**: + +```csharp +var test = new ClashTest +{ + DisplayName = testName, + TestType = ClashTestType.HardConservative, + Tolerance = detectionGap, + MergeComposites = true // 这个选项 +}; +``` + +**MergeComposites = true 的效果**: + +- 将同一个复合对象的多个子节点碰撞合并为一个碰撞 +- 减少重复的碰撞结果 +- 但仍然返回几何体级别的对象(不是复合对象) + +**验证**: + +``` +// 即使设置了 MergeComposites=true +// Clash Detective 返回的仍然是: +// result.Item1.IsComposite = False +// result.Item2.IsComposite = False +// result.Item1.DisplayName = "" +``` + +**结论**:`MergeComposites` 选项只是减少碰撞数量,不会改变返回对象的类型。容器映射仍然是必需的。 + +#### 14.1.8 最佳实践总结 + +| 场景 | 方法 | 注意事项 | +|------|------|---------| +| 碰撞检测输入 | 使用复合对象 | 提高检测效率 | +| 碰撞结果提取 | 使用容器映射 | 获取有意义的对象名称 | +| 碰撞报告显示 | 使用复合对象 | 显示可读的名称 | +| 高亮显示 | 使用复合对象 | 高亮整个对象 | +| 性能优化 | 缓存容器映射 | 减少重复计算 | + +**关键要点**: + +1. **Clash Detective 输入和输出的对象类型不同** +2. **必须使用 FindNamedParentContainer 进行容器映射** +3. **容器映射确保碰撞结果的名称和可读性** +4. **使用缓存优化容器映射的性能** | **索引路径(旧方法)** | 简单易懂 | 不稳定,易失效 | 不推荐使用 | | **DisplayName** | 易于理解 | 不唯一,可能重复 | 仅用于UI显示 | **结论**:`CreatePathId/ResolvePathId` 是 Navisworks API 提供的官方持久化方法,应该作为首选方案。 - diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs index 3ea8994..1e18afe 100644 --- a/src/Core/Animation/PathAnimationManager.cs +++ b/src/Core/Animation/PathAnimationManager.cs @@ -539,14 +539,19 @@ namespace NavisworksTransport.Core.Animation LogManager.Info("=== 构建全局空间索引 ==="); spatialIndexManager = SpatialIndexManager.Instance; - double vehicleWidthMeters = ConfigManager.Instance.Current.PathEditing.VehicleWidthMeters; + // 使用传入的车辆宽度参数,而不是从ConfigManager读取 + double vehicleWidthMeters = _virtualVehicleWidth; double cellSizeInModelUnits = UnitsConverter.ConvertFromMeters(vehicleWidthMeters); LogManager.Info($"[空间索引] 车辆宽度: {vehicleWidthMeters:F2}米 → 格子大小: {cellSizeInModelUnits:F2}模型单位"); - if (!spatialIndexManager.IsInitialized) + // 检查空间索引是否需要重新构建(格子大小变化或未初始化) + bool needRebuild = !spatialIndexManager.IsInitialized || + Math.Abs(spatialIndexManager.CellSize - cellSizeInModelUnits) > 0.0001; + + if (needRebuild) { - LogManager.Info("[空间索引] 空间索引未初始化,开始构建..."); + LogManager.Info("[空间索引] 空间索引未初始化或格子大小已改变,开始重新构建..."); spatialIndexManager.BuildGlobalIndex(cellSizeInModelUnits); } else @@ -2412,13 +2417,10 @@ namespace NavisworksTransport.Core.Animation { foreach (var collision in currentFrame.Collisions) { - var item1 = collision.GetValidItem1(); - var item2 = collision.GetValidItem2(); - - if (item1 != null) - currentCollisionObjects.Add(item1); - if (item2 != null) - currentCollisionObjects.Add(item2); + if (collision.Item1 != null) + currentCollisionObjects.Add(collision.Item1); + if (collision.Item2 != null) + currentCollisionObjects.Add(collision.Item2); } } @@ -2631,8 +2633,9 @@ namespace NavisworksTransport.Core.Animation sb.Append("|"); // 包含虚拟车辆尺寸(确保车辆尺寸改变时重新检测) - var vehicleConfig = ConfigManager.Instance.Current.PathEditing; - sb.Append($"Vehicle:{vehicleConfig.VehicleLengthMeters:F2}x{vehicleConfig.VehicleWidthMeters:F2}x{vehicleConfig.VehicleHeightMeters:F2}"); + // 使用传入的车辆尺寸参数,而不是从ConfigManager读取 + var vehicleSizeString = $"Vehicle:{_virtualVehicleLength:F2}x{_virtualVehicleWidth:F2}x{_virtualVehicleHeight:F2}"; + sb.Append(vehicleSizeString); sb.Append("|"); // 包含手工检测对象列表(确保手工指定模式的目标变化时重新检测) diff --git a/src/Core/Collision/ClashDetectiveIntegration.cs b/src/Core/Collision/ClashDetectiveIntegration.cs index e79b445..f1e3d38 100644 --- a/src/Core/Collision/ClashDetectiveIntegration.cs +++ b/src/Core/Collision/ClashDetectiveIntegration.cs @@ -415,20 +415,20 @@ namespace NavisworksTransport { throw new InvalidOperationException($"[LoadClashDetectiveResultsFromDatabase] 找到的 ModelItem 无效: {obj.ModelIndex},{obj.PathId}"); } - + + // 使用重建对象的 DisplayName,而不是数据库中保存的 DisplayName + var collidedObjectName = ModelItemAnalysisHelper.GetSafeDisplayName(collidedObject); + var collisionResult = new CollisionResult { ClashGuid = Guid.NewGuid(), - DisplayName = $"历史碰撞: {obj.DisplayName}", + DisplayName = $"历史碰撞: {collidedObjectName}", Status = ClashResultStatus.Active, Item1 = vehicleObject, Item2 = collidedObject, Center = collidedObject.BoundingBox().Center, Distance = 0.0, - CreatedTime = DateTime.Now, - OriginalItem1 = vehicleObject, - OriginalItem2 = collidedObject, - HasContainerMapping = true + CreatedTime = DateTime.Now }; results.Add(collisionResult); @@ -444,57 +444,6 @@ namespace NavisworksTransport } } - /// - /// 合并复合对象碰撞结果 - /// 将几何体级别的碰撞结果合并成复合对象级别的碰撞结果(去重) - /// - private List MergeCompositeCollisions(ClashResultGroup collisionGroup) - { - var clashResults = new List(); - var mergedKeys = new HashSet(); - - foreach (var child in collisionGroup.Children) - { - if (child is ClashResult clashResult) - { - // 向上查找有意义的父级容器 - var compositeItem1 = ModelItemAnalysisHelper.FindNamedParentContainer(clashResult.Item1); - var compositeItem2 = ModelItemAnalysisHelper.FindNamedParentContainer(clashResult.Item2); - - // 使用复合对象的HashCode作为key去重 - var key = $"{compositeItem1.GetHashCode()}_{compositeItem2.GetHashCode()}"; - - if (!mergedKeys.Contains(key)) - { - // 创建自定义的CollisionResult对象 - var collisionResult = new CollisionResult - { - ClashGuid = clashResult.Guid, - DisplayName = clashResult.DisplayName, - Status = clashResult.Status, - Item1 = compositeItem1, - Item2 = compositeItem2, - Center = clashResult.Center, - Distance = clashResult.Distance, - CreatedTime = DateTime.Now, - OriginalItem1 = clashResult.Item1, - OriginalItem2 = clashResult.Item2, - HasContainerMapping = true - }; - - clashResults.Add(collisionResult); - mergedKeys.Add(key); - } - } - } - - LogManager.Info($"复合对象处理完成:原始{collisionGroup.Children.Count}个碰撞,合并后{clashResults.Count}个碰撞"); - - return clashResults; - } - - - /// /// 获取对象当前位置 /// @@ -536,11 +485,18 @@ namespace NavisworksTransport /// 虚拟车辆长度(米) /// 虚拟车辆宽度(米) /// 虚拟车辆高度(米) - public void CreateAllAnimationCollisionTests(List precomputedCollisions, double detectionGap, - string pathName = "未知路径", string routeId = null, - ModelItem animatedObject = null, bool isVirtualVehicle = false, - int frameRate = 30, double duration = 10.0, - double virtualVehicleLength = 0, double virtualVehicleWidth = 0, double virtualVehicleHeight = 0) + public void CreateAllAnimationCollisionTests( + List precomputedCollisions, + double detectionGap, + string pathName, + string routeId, + ModelItem animatedObject, + bool isVirtualVehicle, + int frameRate, + double duration, + double virtualVehicleLength, + double virtualVehicleWidth, + double virtualVehicleHeight) { try { @@ -813,7 +769,30 @@ namespace NavisworksTransport LogManager.Info($"[分组测试] ClashDetective检测完成: 确认碰撞 {confirmedCount} 组, 跳过 {skippedCount} 个冗余检测点"); // 第三步:处理碰撞结果 - var clashResults = MergeCompositeCollisions(collisionGroup); + // 🔥 重要:ClashDetective 返回的是几何体级别的碰撞结果,需要向上查找有意义的父级容器 + var clashResults = new List(); + foreach (var child in collisionGroup.Children) + { + if (child is ClashResult clashResult) + { + // 向上查找有意义的父级容器 + var compositeItem1 = ModelItemAnalysisHelper.FindNamedParentContainer(clashResult.Item1); + var compositeItem2 = ModelItemAnalysisHelper.FindNamedParentContainer(clashResult.Item2); + + var collisionResult = new CollisionResult + { + ClashGuid = clashResult.Guid, + DisplayName = clashResult.DisplayName, + Status = clashResult.Status, + Item1 = compositeItem1, + Item2 = compositeItem2, + Center = clashResult.Center, + Distance = clashResult.Distance, + CreatedTime = DateTime.Now + }; + clashResults.Add(collisionResult); + } + } // 缓存结果 lock (_clashResultsCacheLock) @@ -1159,17 +1138,22 @@ namespace NavisworksTransport return; } - // 智能收集通道相关节点(包括父节点、同级节点、子节点等) + // 🔥 优化:只提取复合对象(IsComposite = true) + // 原因:通道对象通常是复合对象(如通道、楼梯、电梯) + // 我们只需要检测复合对象,不需要检测其子节点 foreach (var channelItem in allChannelItems) { try { - var itemRelatedNodes = ModelItemAnalysisHelper.CollectRelatedNodes(channelItem); - - // 将结果添加到缓存中 - foreach (var node in itemRelatedNodes) + // 只添加复合对象 + if (channelItem.IsComposite) { - _channelObjectsCache.Add(node); + _channelObjectsCache.Add(channelItem); + } + else if (channelItem.HasGeometry) + { + // 非复合对象但有几何数据(独立实体节点):也添加 + _channelObjectsCache.Add(channelItem); } } catch (Exception ex) @@ -1256,24 +1240,34 @@ namespace NavisworksTransport if (item.IsHidden) continue; - // 收集:如果是几何体,加入缓存 - // 注意:这里我们假设只有几何体(HasGeometry)才是我们需要碰撞检测的对象 - // 如果父组没有Geometry但子节点有,我们会继续遍历子节点 - if (item.HasGeometry) + // 🔥 优化:只提取复合对象(IsComposite = true) + // 原因:复合对象(如窗户)包含多个实体节点(玻璃、框架、把手) + // 我们只需要检测复合对象,不需要检测其子节点 + // Clash Detective API 会自动处理复合对象的子节点几何体 + if (item.IsComposite) { + // 复合对象:添加到缓存,不继续遍历子节点 _allGeometryItemsCache.Add(item); visibleCount++; } - - // 将子节点压入栈 (倒序压入以保持遍历顺序,虽不 strictly necessary) - foreach (var child in item.Children) + else if (item.HasGeometry) { - stack.Push(child); + // 非复合对象但有几何数据(独立实体节点):添加到缓存 + _allGeometryItemsCache.Add(item); + visibleCount++; + } + else + { + // 空节点:继续遍历子节点 + foreach (var child in item.Children) + { + stack.Push(child); + } } } cacheStopwatch.Stop(); - LogManager.Info($"几何对象列表缓存构建完成 (DFS遍历 + 自动剪枝),耗时: {cacheStopwatch.ElapsedMilliseconds}ms"); + LogManager.Info($"复合对象列表缓存构建完成 (DFS遍历 + 自动剪枝),耗时: {cacheStopwatch.ElapsedMilliseconds}ms"); LogManager.Info($" - 缓存可见对象: {visibleCount}"); LogManager.Info($" - 策略: 深度优先搜索 (父节点隐藏则跳过分支)"); } @@ -1286,14 +1280,14 @@ namespace NavisworksTransport } /// - /// 获取几何对象缓存(供外部使用) + /// 获取复合对象缓存(供外部使用) /// - /// 几何对象集合,如果缓存不存在则返回null + /// 复合对象集合,如果缓存不存在则返回null public static ModelItemCollection GetAllGeometryItemsCache() { lock (_cacheLock) { - return _allGeometryItemsCache; // 直接返回 ModelItemCollection,避免 ToList() 转换 + return _allGeometryItemsCache; } } @@ -1453,19 +1447,6 @@ namespace NavisworksTransport public Point3D Item2Position { get; set; } public bool HasPositionInfo { get; set; } - // 📦 容器映射信息:记录原始几何体对象引用 - public ModelItem OriginalItem1 { get; set; } // 原始几何体对象 - public ModelItem OriginalItem2 { get; set; } // 原始几何体对象 - public bool HasContainerMapping { get; set; } // 标识是否进行了容器映射 - - // 📍 获取最终用于ClashDetective的有效对象 - public ModelItem GetValidItem1() => Item1; // 容器对象,用于ClashDetective选择集 - public ModelItem GetValidItem2() => Item2; // 容器对象,用于ClashDetective选择集 - - // 📍 获取原始几何体对象(用于精确位置信息) - public ModelItem GetOriginalItem1() => OriginalItem1 ?? Item1; - public ModelItem GetOriginalItem2() => OriginalItem2 ?? Item2; - // IEquatable 实现:基于碰撞对象进行去重 public bool Equals(CollisionResult other) { diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs index c355f6e..4d5dd58 100644 --- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs +++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs @@ -2145,22 +2145,17 @@ namespace NavisworksTransport.UI.WPF.ViewModels if (UseVirtualVehicle) { - // 使用虚拟车辆:优先获取已存在的实例 + // 使用虚拟车辆:总是调用 ShowVirtualVehicle 来更新尺寸 + LogManager.Info("[ExecuteGenerateAnimation] 显示并更新虚拟车辆..."); + UpdateMainStatus("正在显示虚拟车辆...", -1, true); + + VirtualVehicleManager.Instance.ShowVirtualVehicle( + VirtualVehicleLength, + VirtualVehicleWidth, + VirtualVehicleHeight + ); animatedObject = VirtualVehicleManager.Instance.CurrentVirtualVehicle; - if (animatedObject == null) - { - LogManager.Info("[ExecuteGenerateAnimation] 虚拟车辆不存在,正在显示..."); - UpdateMainStatus("正在显示虚拟车辆...", -1, true); - - VirtualVehicleManager.Instance.ShowVirtualVehicle( - VirtualVehicleLength, - VirtualVehicleWidth, - VirtualVehicleHeight - ); - animatedObject = VirtualVehicleManager.Instance.CurrentVirtualVehicle; - } - if (animatedObject == null) { LogManager.Error("[动画生成] 虚拟车辆显示失败"); @@ -2168,6 +2163,14 @@ namespace NavisworksTransport.UI.WPF.ViewModels return; } + // 将虚拟车辆移到路径起点 + if (CurrentPathRoute != null && CurrentPathRoute.Points != null) + { + var points = CurrentPathRoute.Points.Select(p => new Point3D(p.X, p.Y, p.Z)).ToList(); + _pathAnimationManager?.MoveVehicleToPathStart(animatedObject, points); + LogManager.Info("[ExecuteGenerateAnimation] 虚拟车辆已移到路径起点"); + } + LogManager.Info($"使用虚拟车辆: {animatedObject.DisplayName}"); } else diff --git a/src/Utils/ModelHighlightHelper.cs b/src/Utils/ModelHighlightHelper.cs index b75f937..bb40cf3 100644 --- a/src/Utils/ModelHighlightHelper.cs +++ b/src/Utils/ModelHighlightHelper.cs @@ -357,7 +357,8 @@ namespace NavisworksTransport.Utils foreach (var clash in clashResults) { // 只高亮被撞物体(Item2),排除车辆对象(Item1) - if (clash.Item2 != null && !highlightItems.Contains(clash.Item2)) + // 🔥 移除去重逻辑:应该显示所有被撞物体,包括重复的 + if (clash.Item2 != null) { highlightItems.Add(clash.Item2); }