使用复合节点改善性能,并且适配Clashdetective输出的结果(只包含实体节点)

This commit is contained in:
tian 2026-01-13 18:21:54 +08:00
parent cf2e5a4d2f
commit c4cf718502
5 changed files with 346 additions and 133 deletions

View File

@ -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. 每次文档操作都有两个事件:
- 先清空ModelsCount=0
- 再加载新ModelsCount>0
2. 状态转换清晰:
- 无模型→无模型:初始清理
- 无模型→有模型:文档加载完成 ✅ 这是我们需要初始化的时机
- 有模型→无模型:文档关闭/切换 ✅ 这是我们需要清理的时机
3. 时间间隔很短事件成对出现间隔仅0.1秒左右
- 先清空ModelsCount=0
- 再加载新ModelsCount>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/
<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<string, ModelItem> _modelItemCache = new Dictionary<string, ModelItem>();
@ -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<ModelItem, ModelItem> _containerCache = new Dictionary<ModelItem, ModelItem>();
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 提供的官方持久化方法,应该作为首选方案。

View File

@ -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("|");
// 包含手工检测对象列表(确保手工指定模式的目标变化时重新检测)

View File

@ -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
}
}
/// <summary>
/// 合并复合对象碰撞结果
/// 将几何体级别的碰撞结果合并成复合对象级别的碰撞结果(去重)
/// </summary>
private List<CollisionResult> MergeCompositeCollisions(ClashResultGroup collisionGroup)
{
var clashResults = new List<CollisionResult>();
var mergedKeys = new HashSet<string>();
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;
}
/// <summary>
/// 获取对象当前位置
/// </summary>
@ -536,11 +485,18 @@ namespace NavisworksTransport
/// <param name="virtualVehicleLength">虚拟车辆长度(米)</param>
/// <param name="virtualVehicleWidth">虚拟车辆宽度(米)</param>
/// <param name="virtualVehicleHeight">虚拟车辆高度(米)</param>
public void CreateAllAnimationCollisionTests(List<CollisionResult> 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<CollisionResult> 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<CollisionResult>();
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
}
/// <summary>
/// 获取几何对象缓存(供外部使用)
/// 获取复合对象缓存(供外部使用)
/// </summary>
/// <returns>几何对象集合如果缓存不存在则返回null</returns>
/// <returns>复合对象集合如果缓存不存在则返回null</returns>
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<CollisionResult> 实现:基于碰撞对象进行去重
public bool Equals(CollisionResult other)
{

View File

@ -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

View File

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