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);
}