实现了日志管理,修复了碰撞自身的问题。
This commit is contained in:
parent
7a5aa413bc
commit
099afd3f93
@ -48,7 +48,8 @@
|
||||
"mcp__context7__get-library-docs",
|
||||
"Bash(powershell:*)",
|
||||
"mcp__serena__insert_before_symbol",
|
||||
"Bash(\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\MSBuild\\Current\\Bin\\MSBuild.exe\" NavisworksTransportPlugin.csproj /p:Configuration=Debug /p:Platform=AnyCPU /verbosity:normal)"
|
||||
"Bash(\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\MSBuild\\Current\\Bin\\MSBuild.exe\" NavisworksTransportPlugin.csproj /p:Configuration=Debug /p:Platform=AnyCPU /verbosity:normal)",
|
||||
"Bash(cmd:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"additionalDirectories": [
|
||||
|
||||
Binary file not shown.
@ -173,5 +173,5 @@ public class PathClickToolPlugin : ToolPlugin { }
|
||||
- **Testing environment**: Navisworks Manage 2026 exclusively - 2017 support discontinued
|
||||
- **Plugin deployment**: Build output automatically copies to Navisworks 2026 plugin directory
|
||||
- **Hot reload**: Restart Navisworks required after compilation to load new plugin version
|
||||
- **Debugging**: Use LogManager for centralized logging and `tool\LogViewer.bat` for log analysis
|
||||
- **Debugging**: Use LogManager for centralized logging and the built-in log viewer dialog for log analysis
|
||||
- **2026 Features**: Test advanced animation capabilities, enhanced collision detection, and improved model handling
|
||||
|
||||
@ -190,6 +190,9 @@
|
||||
<Compile Include="src\UI\WPF\Views\AboutDialog.xaml.cs">
|
||||
<DependentUpon>AboutDialog.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="src\UI\WPF\Views\LogViewerDialog.xaml.cs">
|
||||
<DependentUpon>LogViewerDialog.xaml</DependentUpon>
|
||||
</Compile>
|
||||
|
||||
<!-- UI - WPF ViewModels -->
|
||||
<Compile Include="src\UI\WPF\ViewModels\ViewModelBase.cs" />
|
||||
@ -272,6 +275,10 @@
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="src\UI\WPF\Views\LogViewerDialog.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<!-- Shared Resource Dictionary -->
|
||||
<Page Include="src\UI\WPF\Resources\NavisworksStyles.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
|
||||
300
doc/design/2026/PATHFINDING_DESIGN.md
Normal file
300
doc/design/2026/PATHFINDING_DESIGN.md
Normal file
@ -0,0 +1,300 @@
|
||||
# Navisworks 插件自动路径规划设计方案
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
* **背景**: 当前已在Navisworks插件中集成Roy-T.AStar库,实现了基于二维地面投影的自动寻路功能。
|
||||
* **问题**: 该方案无法处理三维空间中的障碍物(如管道、横梁),这些障碍物在地面上的投影区域会导致规划出的路径在实际三维空间中不可行。
|
||||
* **目标**: 改进路径规划算法,使其能够识别并避开真实的三维障碍物,确保规划出的路径在三维空间中是可通行的。
|
||||
|
||||
## 2. 设计方案
|
||||
|
||||
提出三种解决方案,按复杂度和效果递增排列。
|
||||
|
||||
### 方案一:基于Navisworks API的完整3D空间分析与图构建 (推荐长期实施)
|
||||
|
||||
#### 2.1 核心思路
|
||||
|
||||
利用Navisworks强大的API,直接访问模型的三维几何和属性信息,在插件内部或外部预处理程序中,构建一个精确反映“可行驶区域”的拓扑图(Graph),然后在此图上运行A*算法。
|
||||
|
||||
#### 2.2 实施步骤
|
||||
|
||||
1. **数据获取 (Navisworks API)**:
|
||||
* 遍历模型,获取关键构件信息:
|
||||
* **地面/楼板 (Floor/Slab)**: 获取其几何形状(面片或网格),用于定义基础行驶区域。
|
||||
* **障碍物 (Obstacles)**: 获取所有可能阻挡车辆的构件(如管道 `Pipe`、风管 `Duct`、横梁 `Beam`、设备 `Equipment` 等)的**三维包围盒 (Bounding Box)** 或精确网格数据。
|
||||
* **连接构件 (Connections)**: 获取门 `Door`、楼梯 `Stairs`、电梯 `Elevator` 等信息,用于处理特殊通行规则或楼层间移动。
|
||||
* 获取构件的属性(如类别、名称、ID),以便于分类和处理。
|
||||
|
||||
2. **3D空间分析与“可行驶区域”定义**:
|
||||
* **定义车辆参数**: 确定车辆的尺寸(长、宽、高 `H_vehicle`)和最大行驶高度 `H_max`。
|
||||
* **方法A - 2.5D栅格化 (推荐)**:
|
||||
* 在地面平面上建立一个2D规则网格。
|
||||
* 对网格中的每个单元格 `(x, y)`,沿Z轴扫描从地面到 `H_max` 的空间。
|
||||
* 检查此垂直扫描线是否与任何障碍物的几何体相交。
|
||||
* 记录每个 `(x, y)` 单元格内所有“无障碍”的高度区间 `[Z_min, Z_max]`。
|
||||
* 如果只关心固定高度层(如所有通道在同一层),则只需检查该高度层是否有障碍物。
|
||||
* **方法B - 简化2D栅格化**: 结合方案三,作为此步骤的一个简化实现。
|
||||
|
||||
3. **图 (Graph) 构建**:
|
||||
* **节点 (Node)**: 每个“可通行”的网格单元格中心(或角落)作为图的节点,其坐标为 `(x, y, z)`。`z` 可以是地面高度或固定行驶层高度。
|
||||
* **边 (Edge)**: 相邻的可通行节点之间建立连接。
|
||||
* **成本计算**: 边的权重可以是欧几里得距离 `Distance`,也可以结合行驶时间 `Duration`(距离/预设速度)。
|
||||
* **特殊边**:
|
||||
* 门: 连接门两侧节点,成本可动态调整(如门关闭时成本极高)。
|
||||
* 楼梯/电梯: 连接不同楼层的对应节点,成本应反映垂直移动的时间。
|
||||
* **输出**: 生成一个可供Roy-T.AStar库使用的 `Graph` 对象。
|
||||
|
||||
4. **Navisworks插件集成**:
|
||||
* **初始化**: 插件启动或模型更新时,执行上述预处理步骤,生成并加载路径图。
|
||||
* **寻路调用**:
|
||||
* 用户指定起点和终点(3D坐标)。
|
||||
* 将3D点映射到图中最邻近的节点。
|
||||
* 调用 `PathFinder.FindPath(startNode, endNode, graph)` 进行寻路。
|
||||
* **结果可视化**: 将A*返回的路径(节点序列)在Navisworks视图中绘制出来。
|
||||
|
||||
#### 2.3 优点
|
||||
|
||||
* 精度最高,能真实反映三维空间的可通行性。
|
||||
* 为未来扩展(如多层车辆、更复杂的避障规则)奠定坚实基础。
|
||||
* 一次计算,多次高效寻路。
|
||||
|
||||
#### 2.4 缺点
|
||||
|
||||
* 开发复杂度最高,需要深入理解和使用Navisworks API进行空间计算。
|
||||
* 预处理阶段计算量较大。
|
||||
|
||||
### 方案二:后处理校验与迭代修正 (快速验证)
|
||||
|
||||
#### 2.1 核心思路
|
||||
|
||||
保留现有二维投影寻路逻辑,但在得到路径后,增加一个三维碰撞检测步骤来校验路径的可行性,并根据结果进行修正或提示。
|
||||
|
||||
#### 2.2 实施步骤
|
||||
|
||||
1. **执行二维寻路**: 运行当前基于地面投影的A*算法,得到一条二维路径。
|
||||
2. **三维碰撞检测**:
|
||||
* 遍历路径上的关键点或按固定步长取点。
|
||||
* 在每个点上,根据预设的车辆尺寸,在三维空间中生成一个代表车辆的包围盒(AABB或OBB)。
|
||||
* 利用Navisworks API的碰撞检测功能,检查此包围盒是否与模型中的任何构件发生碰撞。
|
||||
3. **处理与反馈**:
|
||||
* **无冲突**: 路径可行,直接输出。
|
||||
* **有冲突**:
|
||||
* **简单处理**: 向用户提示“路径在XX处存在碰撞,可能不可行”。
|
||||
* **复杂处理 (迭代)**:
|
||||
a. 记录所有发生碰撞的点及其在二维投影平面上的区域。
|
||||
b. 将这些冲突区域在原始二维寻路网格中临时标记为“不可通行”。
|
||||
c. 重新调用二维A*算法进行寻路。
|
||||
d. 重复步骤2和3,直到找到一条无冲突路径或达到最大迭代次数。
|
||||
|
||||
#### 2.3 优点
|
||||
|
||||
* 对现有代码改动最小,实现快速。
|
||||
* 可以快速验证思路。
|
||||
|
||||
#### 2.4 缺点
|
||||
|
||||
* 效果有限,可能无法找到真正可行的路径。
|
||||
* “试错-修正”机制效率低,可能需要多次迭代。
|
||||
* 无法保证找到最优解。
|
||||
|
||||
### 方案三:混合方法 - 构建精确的可行驶区域网格 (推荐短期实施)
|
||||
|
||||
#### 3.1 核心思路
|
||||
|
||||
结合现有二维框架和Navisworks API能力。核心在于**不再使用建筑最大包围盒作为寻路依据,而是通过Navisworks API精确构建一个反映真实“可行驶区域”的二维网格**。该网格通过叠加“地面区域”和“障碍物投影区域”图层计算得出。
|
||||
|
||||
#### 3.2 实施步骤
|
||||
|
||||
1. **定义车辆参数**:
|
||||
* 明确车辆的尺寸(长、宽、高 `H_vehicle`)。
|
||||
* 确定车辆的最大行驶高度 `H_max`(例如,车辆底盘离地高度 + H_vehicle)。
|
||||
|
||||
2. **生成基础二维网格**:
|
||||
* **范围**: 基于整个建筑地面或您关心的区域,定义一个二维规则网格。网格的分辨率需要权衡精度和性能(例如,单元格大小可以是0.5m x 0.5m)。
|
||||
* **初始状态**: 此时,网格单元格的状态是**未知**的。
|
||||
|
||||
3. **构建“地面区域图层”**:
|
||||
* **目标**: 确定哪些网格单元格是**理论上**可供车辆**安全停放或通过中心点**的区域(即有地面存在的区域)。
|
||||
* **方法 (利用Navisworks API)**:
|
||||
* 遍历模型,找到所有被分类为“Floor”、“Slab”或类似类别的构件。
|
||||
* 获取它们的几何信息(面片)。
|
||||
* 对于每个“地面”面片,判断其覆盖了哪些网格单元格(例如,单元格中心点是否在面片内,或单元格大部分区域在面片内)。
|
||||
* **结果**: 得到一个“地面图层”网格,其中标记了所有有地面的单元格。这表示车辆**有可能**在这些区域活动。
|
||||
|
||||
4. **构建“障碍物投影图层”**:
|
||||
* **目标**: 识别并标记所有会**阻挡车辆通行**的区域。
|
||||
* **方法 (利用Navisworks API)**:
|
||||
* 遍历模型,找到所有可能构成障碍的构件(Pipe, Duct, Beam, Column, Equipment, Wall等)。
|
||||
* **方法 A: 简单包围盒投影 (快速但粗糙)**:
|
||||
* 获取每个障碍物的三维Axis-Aligned Bounding Box (AABB)。
|
||||
* 计算每个障碍物AABB在地面(或车辆行驶平面 `H_max`)上的二维投影区域。
|
||||
* **方法 B: 精确切片投影 (推荐,精度高)**:
|
||||
* **核心思想**: 在车辆行驶高度 `H_vehicle` 对障碍物进行水平“切片”,得到其在该高度的精确二维截面,再将此截面投影到地面网格。
|
||||
* **步骤**:
|
||||
1. **几何获取**: 使用Navisworks API获取障碍物的精确三角网格 (`Mesh`) 数据。
|
||||
2. **执行切片**: 计算水平平面 `Z = H_vehicle` 与障碍物网格的交集。这通常需要实现一个算法或使用第三方计算几何库(如CGAL)来计算网格与平面的交线,得到一组表示截面轮廓的二维线段。
|
||||
3. **投影**: 由于截面本身就是高度为 `H_vehicle` 的二维形状,将其Z坐标统一设为地面高度即可视为投影。
|
||||
4. **栅格化**: 将这些精确的二维截面线段“绘制”到基础二维网格上,标记被覆盖的单元格。
|
||||
* **方法 C: 视图导出与图像处理 (变通方法,实现相对简单)**:
|
||||
* **核心思想**: 利用Navisworks强大的渲染和剪裁功能,通过图像处理间接获得投影。
|
||||
* **步骤**:
|
||||
1. **设置视图**: 在Navisworks中设置一个俯视的正交视图 (Orthographic View)。
|
||||
2. **设置剪裁**: 应用剪裁平面,只显示模型在高度 `Z = H_vehicle` 到 `Z = H_vehicle + delta` (delta为很小的正值) 之间的“薄片”。
|
||||
3. **隐藏地面**: (可选)隐藏地面构件,使视图只显示障碍物薄片。
|
||||
4. **导出图像**: 使用API将此特定视图导出为二维图像(如PNG)。
|
||||
5. **图像处理**: 在插件或外部程序中,使用图像处理库识别图像中的障碍物像素,并将这些像素区域映射回原始的二维寻路网格,标记对应的单元格。
|
||||
* **选择建议**:
|
||||
* **初期验证**: 可使用 **方法 A** 快速搭建流程。
|
||||
* **中期迭代**: 推荐 **方法 C**,它在实现复杂度和精度之间取得了很好的平衡,能有效利用Navisworks现有功能。
|
||||
* **最终方案**: 如果对精度和性能有极致要求,应采用 **方法 B**。
|
||||
* **标记**: 将计算出的投影区域(无论采用哪种方法)所覆盖的网格单元格标记为“被障碍物投影覆盖”。
|
||||
* **结果**: 得到一个“障碍物投影图层”网格。
|
||||
|
||||
5. **合成最终“可通行区域网格”**:
|
||||
* **核心逻辑**: 一个网格单元格是**真正可通行**的,当且仅当:
|
||||
* 它在“地面图层”中被标记为**属于地面** (是潜在可达区域)。
|
||||
* **并且** 它在“障碍物投影图层”中**未被任何障碍物投影覆盖**。
|
||||
* **操作**: 遍历所有网格单元格,根据上述逻辑计算其最终状态。只有同时满足两个条件的单元格,才在最终用于寻路的网格中被标记为 `Accessible`(或类似表示可通行的状态)。
|
||||
|
||||
6. **使用Roy-T.AStar进行寻路**:
|
||||
* **创建Grid**: 使用上述最终生成的、精确的“可通行区域网格”来创建 `Roy_T.AStar.Grids.Grid` 对象。
|
||||
* 网格大小 (`GridSize`) 对应您的二维网格。
|
||||
* 每个单元格的状态 (`GridCellState`) 来自第5步计算出的最终结果。
|
||||
* 单元格大小 (`Size`) 对应您的网格分辨率。
|
||||
* `traversalVelocity` 可以设为一个默认值,或者根据不同区域类型进行调整。
|
||||
* **调用寻路**: `PathFinder.FindPath(startPosition, endPosition, grid)`。
|
||||
|
||||
#### 3.3 优点
|
||||
|
||||
* 实现相对简单,对现有架构改动较小。
|
||||
* 能有效解决当前最主要的问题(空中障碍物投影导致的误判)。
|
||||
* 开发周期短,可作为从方案二到方案一的过渡。
|
||||
* **核心改进**: 通过构建精确的“可通行区域”网格,真正利用了Roy-T.AStar库的Grid功能,而非简单粗暴地使用建筑包围盒。
|
||||
|
||||
#### 3.4 缺点
|
||||
|
||||
* 精度低于方案一(例如,无法处理非垂直投影的复杂障碍物,或不同高度层的障碍物)。
|
||||
* 仍然是基于二维的近似方法。
|
||||
|
||||
### 方案一:基于Navisworks API的完整3D空间分析与图构建 (推荐长期实施)
|
||||
|
||||
#### 2.1 核心思路
|
||||
|
||||
利用Navisworks强大的API,直接访问模型的三维几何和属性信息,在插件内部或外部预处理程序中,构建一个精确反映“可行驶区域”的拓扑图(Graph),然后在此图上运行A*算法。
|
||||
|
||||
#### 2.2 实施步骤
|
||||
|
||||
1. **数据获取 (Navisworks API)**:
|
||||
* 遍历模型,获取关键构件信息:
|
||||
* **地面/楼板 (Floor/Slab)**: 获取其几何形状(面片或网格),用于定义基础行驶区域。
|
||||
* **障碍物 (Obstacles)**: 获取所有可能阻挡车辆的构件(如管道 `Pipe`、风管 `Duct`、横梁 `Beam`、设备 `Equipment` 等)的**三维包围盒 (Bounding Box)** 或精确网格数据。
|
||||
* **连接构件 (Connections)**: 获取门 `Door`、楼梯 `Stairs`、电梯 `Elevator` 等信息,用于处理特殊通行规则或楼层间移动。
|
||||
* 获取构件的属性(如类别、名称、ID),以便于分类和处理。
|
||||
|
||||
2. **3D空间分析与“可行驶区域”定义**:
|
||||
* **定义车辆参数**: 确定车辆的尺寸(长、宽、高 `H_vehicle`)和最大行驶高度 `H_max`。
|
||||
* **方法A - 2.5D栅格化 (推荐)**:
|
||||
* 在地面平面上建立一个2D规则网格。
|
||||
* 对网格中的每个单元格 `(x, y)`,沿Z轴扫描从地面到 `H_max` 的空间。
|
||||
* 检查此垂直扫描线是否与任何障碍物的几何体相交。
|
||||
* 记录每个 `(x, y)` 单元格内所有“无障碍”的高度区间 `[Z_min, Z_max]`。
|
||||
* 如果只关心固定高度层(如所有通道在同一层),则只需检查该高度层是否有障碍物。
|
||||
* **方法B - 简化2D栅格化**: 结合方案三,作为此步骤的一个简化实现。
|
||||
|
||||
3. **图 (Graph) 构建**:
|
||||
* **节点 (Node)**: 每个“可通行”的网格单元格中心(或角落)作为图的节点,其坐标为 `(x, y, z)`。`z` 可以是地面高度或固定行驶层高度。
|
||||
* **边 (Edge)**: 相邻的可通行节点之间建立连接。
|
||||
* **成本计算**: 边的权重可以是欧几里得距离 `Distance`,也可以结合行驶时间 `Duration`(距离/预设速度)。
|
||||
* **特殊边**:
|
||||
* 门: 连接门两侧节点,成本可动态调整(如门关闭时成本极高)。
|
||||
* 楼梯/电梯: 连接不同楼层的对应节点,成本应反映垂直移动的时间。
|
||||
* **输出**: 生成一个可供Roy-T.AStar库使用的 `Graph` 对象。
|
||||
|
||||
4. **Navisworks插件集成**:
|
||||
* **初始化**: 插件启动或模型更新时,执行上述预处理步骤,生成并加载路径图。
|
||||
* **寻路调用**:
|
||||
* 用户指定起点和终点(3D坐标)。
|
||||
* 将3D点映射到图中最邻近的节点。
|
||||
* 调用 `PathFinder.FindPath(startNode, endNode, graph)` 进行寻路。
|
||||
* **结果可视化**: 将A*返回的路径(节点序列)在Navisworks视图中绘制出来。
|
||||
|
||||
#### 2.3 优点
|
||||
|
||||
* 精度最高,能真实反映三维空间的可通行性。
|
||||
* 为未来扩展(如多层车辆、更复杂的避障规则)奠定坚实基础。
|
||||
* 一次计算,多次高效寻路。
|
||||
|
||||
#### 2.4 缺点
|
||||
|
||||
* 开发复杂度最高,需要深入理解和使用Navisworks API进行空间计算。
|
||||
* 预处理阶段计算量较大。
|
||||
|
||||
### 方案二:后处理校验与迭代修正 (快速验证)
|
||||
|
||||
#### 2.1 核心思路
|
||||
|
||||
保留现有二维投影寻路逻辑,但在得到路径后,增加一个三维碰撞检测步骤来校验路径的可行性,并根据结果进行修正或提示。
|
||||
|
||||
#### 2.2 实施步骤
|
||||
|
||||
1. **执行二维寻路**: 运行当前基于地面投影的A*算法,得到一条二维路径。
|
||||
2. **三维碰撞检测**:
|
||||
* 遍历路径上的关键点或按固定步长取点。
|
||||
* 在每个点上,根据预设的车辆尺寸,在三维空间中生成一个代表车辆的包围盒(AABB或OBB)。
|
||||
* 利用Navisworks API的碰撞检测功能,检查此包围盒是否与模型中的任何构件发生碰撞。
|
||||
3. **处理与反馈**:
|
||||
* **无冲突**: 路径可行,直接输出。
|
||||
* **有冲突**:
|
||||
* **简单处理**: 向用户提示“路径在XX处存在碰撞,可能不可行”。
|
||||
* **复杂处理 (迭代)**:
|
||||
a. 记录所有发生碰撞的点及其在二维投影平面上的区域。
|
||||
b. 将这些冲突区域在原始二维寻路网格中临时标记为“不可通行”。
|
||||
c. 重新调用二维A*算法进行寻路。
|
||||
d. 重复步骤2和3,直到找到一条无冲突路径或达到最大迭代次数。
|
||||
|
||||
#### 2.3 优点
|
||||
|
||||
* 对现有代码改动最小,实现快速。
|
||||
* 可以快速验证思路。
|
||||
|
||||
#### 2.4 缺点
|
||||
|
||||
* 效果有限,可能无法找到真正可行的路径。
|
||||
* “试错-修正”机制效率低,可能需要多次迭代。
|
||||
* 无法保证找到最优解。
|
||||
|
||||
### 方案三:混合方法 - 3D障碍物投影融合 (推荐短期实施)
|
||||
|
||||
#### 3.1 核心思路
|
||||
|
||||
结合现有二维框架和部分Navisworks API能力。在预处理阶段识别三维障碍物,并将其在地面(或行驶平面)的投影区域从可通行区域中排除。
|
||||
|
||||
#### 3.2 实施步骤
|
||||
|
||||
1. **障碍物识别与投影 (Navisworks API 预处理)**:
|
||||
* 遍历模型,识别所有可能构成障碍的构件(管道、横梁等)。
|
||||
* 获取这些构件的三维包围盒。
|
||||
* 计算这些包围盒在地面(或预设行驶平面)上的**二维投影区域**。
|
||||
2. **更新二维寻路网格**:
|
||||
* 在您当前用于二维寻路的网格基础上,增加一个“障碍物投影图层”。
|
||||
* 将上一步计算出的所有投影区域,在二维网格中标记为“不可通行”。
|
||||
3. **执行二维寻路**:
|
||||
* 在寻路时,一个网格单元格被认为是“可通行”的,当且仅当它在原始“地面可达性图”中是可达的,并且在“障碍物投影图层”中未被标记为不可通行。
|
||||
* 调用现有的Roy-T.AStar `Grid` 寻路功能。
|
||||
|
||||
#### 3.3 优点
|
||||
|
||||
* 实现相对简单,对现有架构改动较小。
|
||||
* 能有效解决当前最主要的问题(空中障碍物投影导致的误判)。
|
||||
* 开发周期短,可作为从方案二到方案一的过渡。
|
||||
|
||||
#### 3.4 缺点
|
||||
|
||||
* 精度低于方案一(例如,无法处理非垂直投影的复杂障碍物,或不同高度层的障碍物)。
|
||||
* 仍然是基于二维的近似方法。
|
||||
|
||||
## 3. 推荐与实施计划
|
||||
|
||||
1. **短期 (验证与快速迭代)**: 实施 **方案三**。这能以最小的代价解决当前最紧迫的问题,验证思路。
|
||||
2. **中期 (功能完善)**: 在方案三稳定后,评估其局限性。如果场景复杂度增加,开始规划 **方案一** 的开发。
|
||||
3. **长期 (终极目标)**: 实施 **方案一**,构建完整的3D空间分析能力,为插件提供最强大、最精确的路径规划内核。
|
||||
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Autodesk.Navisworks.Api;
|
||||
using NavisApplication = Autodesk.Navisworks.Api.Application;
|
||||
@ -15,6 +16,12 @@ namespace NavisworksTransport.Core.Animation
|
||||
private readonly Document _document;
|
||||
private readonly Dictionary<string, List<SavedViewpoint>> _pathViewpoints;
|
||||
private readonly Dictionary<string, SavedViewpointAnimation> _animations;
|
||||
|
||||
// 动画对象排除列表缓存管理
|
||||
private ModelItem _currentCachedAnimationObject;
|
||||
private List<ModelItem> _cachedExclusionList;
|
||||
private DateTime _cacheCreatedTime;
|
||||
private readonly TimeSpan _cacheValidDuration = TimeSpan.FromMinutes(30); // 缓存有效期30分钟
|
||||
|
||||
public LogisticsAnimationManager()
|
||||
{
|
||||
@ -25,6 +32,178 @@ namespace NavisworksTransport.Core.Animation
|
||||
LogManager.Info("LogisticsAnimationManager 初始化完成 - 使用视点序列");
|
||||
}
|
||||
|
||||
#region 动画对象排除列表缓存管理
|
||||
|
||||
/// <summary>
|
||||
/// 为动画对象预计算并缓存排除列表
|
||||
/// </summary>
|
||||
/// <param name="animationObject">动画对象</param>
|
||||
/// <returns>是否成功缓存</returns>
|
||||
public bool PrecomputeCollisionExclusions(ModelItem animationObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
LogManager.Info($"[缓存管理] 开始为动画对象预计算排除列表: {animationObject?.DisplayName ?? "NULL"}");
|
||||
|
||||
if (animationObject == null)
|
||||
{
|
||||
LogManager.Warning("[缓存管理] 动画对象为空,清除缓存");
|
||||
ClearExclusionCache();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否需要重新计算
|
||||
if (IsCacheValid(animationObject))
|
||||
{
|
||||
LogManager.Info($"[缓存管理] 缓存仍然有效,跳过重新计算");
|
||||
return true;
|
||||
}
|
||||
|
||||
var startTime = DateTime.Now;
|
||||
|
||||
// 直接在这里进行几何分析和排除列表构建
|
||||
var exclusionList = BuildLogicalObjectExclusionList(animationObject);
|
||||
|
||||
// 更新缓存
|
||||
_currentCachedAnimationObject = animationObject;
|
||||
_cachedExclusionList = exclusionList;
|
||||
_cacheCreatedTime = DateTime.Now;
|
||||
|
||||
var elapsedMs = (DateTime.Now - startTime).TotalMilliseconds;
|
||||
LogManager.Info($"[缓存管理] 预计算完成,耗时 {elapsedMs:F1}ms,缓存 {exclusionList.Count} 个排除对象");
|
||||
|
||||
// 记录缓存的前几个对象用于验证
|
||||
for (int i = 0; i < Math.Min(5, exclusionList.Count); i++)
|
||||
{
|
||||
LogManager.Debug($" 缓存排除对象{i+1}: {exclusionList[i]?.DisplayName ?? ""}");
|
||||
}
|
||||
if (exclusionList.Count > 5)
|
||||
{
|
||||
LogManager.Debug($" ... 以及其他 {exclusionList.Count - 5} 个对象");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Error($"[缓存管理] 预计算排除列表失败: {ex.Message}", ex);
|
||||
ClearExclusionCache();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓存的排除列表
|
||||
/// </summary>
|
||||
/// <param name="animationObject">动画对象</param>
|
||||
/// <returns>排除列表,如果缓存无效则返回null</returns>
|
||||
public List<ModelItem> GetCachedExclusionList(ModelItem animationObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!IsCacheValid(animationObject))
|
||||
{
|
||||
LogManager.Debug($"[缓存管理] 缓存无效,建议重新预计算");
|
||||
return null;
|
||||
}
|
||||
|
||||
LogManager.Debug($"[缓存管理] 返回缓存的排除列表: {_cachedExclusionList?.Count ?? 0} 个对象");
|
||||
return _cachedExclusionList;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Warning($"[缓存管理] 获取缓存失败: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查缓存是否有效
|
||||
/// </summary>
|
||||
/// <param name="animationObject">当前动画对象</param>
|
||||
/// <returns>缓存是否有效</returns>
|
||||
private bool IsCacheValid(ModelItem animationObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查是否有缓存
|
||||
if (_currentCachedAnimationObject == null || _cachedExclusionList == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查对象是否相同
|
||||
if (!_currentCachedAnimationObject.Equals(animationObject))
|
||||
{
|
||||
LogManager.Debug($"[缓存验证] 动画对象已变更: '{_currentCachedAnimationObject.DisplayName}' -> '{animationObject.DisplayName}'");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查缓存时间是否过期
|
||||
if (DateTime.Now - _cacheCreatedTime > _cacheValidDuration)
|
||||
{
|
||||
LogManager.Debug($"[缓存验证] 缓存已过期: {(DateTime.Now - _cacheCreatedTime).TotalMinutes:F1} 分钟");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Warning($"[缓存验证] 验证过程异常: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除排除列表缓存
|
||||
/// </summary>
|
||||
public void ClearExclusionCache()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_currentCachedAnimationObject != null)
|
||||
{
|
||||
LogManager.Debug($"[缓存管理] 清除缓存: {_currentCachedAnimationObject.DisplayName}");
|
||||
}
|
||||
|
||||
_currentCachedAnimationObject = null;
|
||||
_cachedExclusionList = null;
|
||||
_cacheCreatedTime = DateTime.MinValue;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Warning($"[缓存管理] 清除缓存失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓存统计信息
|
||||
/// </summary>
|
||||
/// <returns>缓存统计信息</returns>
|
||||
public string GetCacheStats()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_currentCachedAnimationObject == null)
|
||||
{
|
||||
return "缓存状态: 无缓存";
|
||||
}
|
||||
|
||||
var age = DateTime.Now - _cacheCreatedTime;
|
||||
var objectName = _currentCachedAnimationObject.DisplayName ?? "";
|
||||
var count = _cachedExclusionList?.Count ?? 0;
|
||||
|
||||
return $"缓存状态: 对象='{objectName}', 排除数={count}, 年龄={age.TotalSeconds:F1}秒";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"缓存状态获取失败: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 创建路径跟随视点序列
|
||||
/// </summary>
|
||||
@ -336,6 +515,711 @@ namespace NavisworksTransport.Core.Animation
|
||||
}
|
||||
}
|
||||
|
||||
#region 几何分析功能 (从 ClashDetectiveIntegration 移动而来)
|
||||
|
||||
/// <summary>
|
||||
/// 构建逻辑物理对象的排除列表
|
||||
/// </summary>
|
||||
/// <param name="animatedObject">动画对象</param>
|
||||
/// <returns>应该从碰撞检测中排除的所有对象列表</returns>
|
||||
private List<ModelItem> BuildLogicalObjectExclusionList(ModelItem animatedObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
LogManager.Debug($"[排除列表构建] 开始为动画对象构建排除列表: {animatedObject.DisplayName}");
|
||||
|
||||
var exclusionList = new List<ModelItem>();
|
||||
|
||||
// 1. 首先进行几何边界分析
|
||||
var geometryAnalysis = AnalyzeGeometricBoundary(animatedObject);
|
||||
|
||||
// 2. 根据分析结果确定排除范围
|
||||
switch (geometryAnalysis.LogicalObjectScope)
|
||||
{
|
||||
case LogicalObjectScope.SelectedNodeOnly:
|
||||
exclusionList.Add(animatedObject);
|
||||
LogManager.Debug($"[排除列表构建] 使用仅选中节点策略");
|
||||
break;
|
||||
|
||||
case LogicalObjectScope.NodeAndDescendants:
|
||||
exclusionList.AddRange(animatedObject.DescendantsAndSelf.Where(d => d.HasGeometry));
|
||||
LogManager.Debug($"[排除列表构建] 使用节点及后代策略,排除 {exclusionList.Count} 个对象");
|
||||
break;
|
||||
|
||||
case LogicalObjectScope.ParentAndSiblings:
|
||||
if (animatedObject.Parent != null)
|
||||
{
|
||||
exclusionList.AddRange(animatedObject.Parent.DescendantsAndSelf.Where(d => d.HasGeometry));
|
||||
}
|
||||
else
|
||||
{
|
||||
exclusionList.AddRange(animatedObject.DescendantsAndSelf.Where(d => d.HasGeometry));
|
||||
}
|
||||
LogManager.Debug($"[排除列表构建] 使用父节点及兄弟策略,排除 {exclusionList.Count} 个对象");
|
||||
break;
|
||||
|
||||
case LogicalObjectScope.EntireLogicalComponent:
|
||||
exclusionList.AddRange(geometryAnalysis.RelatedNodes.Where(n => n.HasGeometry));
|
||||
LogManager.Debug($"[排除列表构建] 使用整个逻辑组件策略,排除 {exclusionList.Count} 个对象");
|
||||
break;
|
||||
}
|
||||
|
||||
// 3. 补充几何空间分析的相关对象
|
||||
SupplementSpatiallyRelatedObjects(animatedObject, exclusionList);
|
||||
|
||||
// 4. 去重并验证
|
||||
exclusionList = exclusionList.Distinct().Where(item => IsModelItemValid(item)).ToList();
|
||||
|
||||
LogManager.Info($"[排除列表构建] 最终排除列表包含 {exclusionList.Count} 个对象:");
|
||||
foreach (var item in exclusionList.Take(10)) // 只记录前10个,避免日志过长
|
||||
{
|
||||
LogManager.Debug($" - 排除对象: {item.DisplayName ?? ""}");
|
||||
}
|
||||
if (exclusionList.Count > 10)
|
||||
{
|
||||
LogManager.Debug($" - ... 以及其他 {exclusionList.Count - 10} 个对象");
|
||||
}
|
||||
|
||||
return exclusionList;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Error($"[排除列表构建] 构建失败: {ex.Message}", ex);
|
||||
// 返回最基本的排除列表(仅动画对象本身)
|
||||
return new List<ModelItem> { animatedObject };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分析选中对象的完整几何边界,识别逻辑物理对象的范围
|
||||
/// </summary>
|
||||
/// <param name="selectedObject">用户选中的对象</param>
|
||||
/// <returns>几何边界分析结果</returns>
|
||||
private GeometricBoundaryAnalysis AnalyzeGeometricBoundary(ModelItem selectedObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
LogManager.Debug($"[几何边界分析] 开始分析对象: {selectedObject.DisplayName}");
|
||||
|
||||
var analysis = new GeometricBoundaryAnalysis
|
||||
{
|
||||
SelectedObject = selectedObject,
|
||||
RootBoundingBox = selectedObject.BoundingBox(),
|
||||
RelatedNodes = new List<ModelItem>()
|
||||
};
|
||||
|
||||
// 1. 收集所有相关节点(向上和向下遍历)
|
||||
CollectRelatedNodes(selectedObject, analysis);
|
||||
|
||||
// 2. 分析几何包含关系
|
||||
AnalyzeGeometricContainment(analysis);
|
||||
|
||||
// 3. 建立逻辑物理对象边界
|
||||
DefineLogicalObjectBoundary(analysis);
|
||||
|
||||
LogManager.Debug($"[几何边界分析] 完成,发现 {analysis.RelatedNodes.Count} 个相关节点");
|
||||
LogManager.Debug($"[几何边界分析] 逻辑对象范围: {analysis.LogicalObjectScope}");
|
||||
|
||||
return analysis;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Error($"[几何边界分析] 分析失败: {ex.Message}", ex);
|
||||
return CreateFallbackAnalysis(selectedObject);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查ModelItem是否仍然有效
|
||||
/// </summary>
|
||||
private bool IsModelItemValid(ModelItem item)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (item == null)
|
||||
return false;
|
||||
|
||||
// 尝试访问对象的属性来检查是否有效
|
||||
var displayName = item.DisplayName;
|
||||
var hasGeometry = item.HasGeometry;
|
||||
|
||||
// 额外检查:确保对象没有被释放
|
||||
var boundingBox = item.BoundingBox();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Debug($"ModelItem无效: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 收集与选中对象相关的所有节点
|
||||
/// </summary>
|
||||
private void CollectRelatedNodes(ModelItem selectedObject, GeometricBoundaryAnalysis analysis)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 添加自身
|
||||
analysis.RelatedNodes.Add(selectedObject);
|
||||
|
||||
// 1. 向上收集:父节点、祖父节点等
|
||||
CollectAncestorNodes(selectedObject, analysis);
|
||||
|
||||
// 2. 向下收集:子节点、孙节点等
|
||||
CollectDescendantNodes(selectedObject, analysis);
|
||||
|
||||
// 3. 收集兄弟节点(同级节点)
|
||||
CollectSiblingNodes(selectedObject, analysis);
|
||||
|
||||
// 去重
|
||||
analysis.RelatedNodes = analysis.RelatedNodes.Distinct().ToList();
|
||||
|
||||
LogManager.Debug($"[收集相关节点] 总共收集到 {analysis.RelatedNodes.Count} 个节点");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Warning($"[收集相关节点] 收集过程异常: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 收集祖先节点
|
||||
/// </summary>
|
||||
private void CollectAncestorNodes(ModelItem item, GeometricBoundaryAnalysis analysis)
|
||||
{
|
||||
try
|
||||
{
|
||||
var current = item.Parent;
|
||||
int levels = 0;
|
||||
|
||||
while (current != null && levels < 10) // 最多向上10层,避免无限循环
|
||||
{
|
||||
analysis.RelatedNodes.Add(current);
|
||||
|
||||
// 如果当前节点的包围盒显著大于原始对象,可能已经到了更大的组件
|
||||
var currentBbox = current.BoundingBox();
|
||||
var originalBbox = analysis.RootBoundingBox;
|
||||
|
||||
// 如果包围盒体积增大超过5倍,可能已经超出了逻辑对象范围
|
||||
var currentVolume = CalculateBoundingBoxVolume(currentBbox);
|
||||
var originalVolume = CalculateBoundingBoxVolume(originalBbox);
|
||||
|
||||
if (currentVolume > originalVolume * 5.0)
|
||||
{
|
||||
LogManager.Debug($"[祖先节点收集] 在第{levels}层停止,包围盒体积增大过多");
|
||||
break;
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
levels++;
|
||||
}
|
||||
|
||||
LogManager.Debug($"[祖先节点收集] 向上收集了 {levels} 层节点");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Warning($"[祖先节点收集] 收集异常: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 收集后代节点
|
||||
/// </summary>
|
||||
private void CollectDescendantNodes(ModelItem item, GeometricBoundaryAnalysis analysis)
|
||||
{
|
||||
try
|
||||
{
|
||||
var descendants = item.DescendantsAndSelf.Where(d => d.HasGeometry).ToList();
|
||||
analysis.RelatedNodes.AddRange(descendants);
|
||||
|
||||
LogManager.Debug($"[后代节点收集] 收集了 {descendants.Count} 个有几何体的后代节点");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Warning($"[后代节点收集] 收集异常: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 收集兄弟节点
|
||||
/// </summary>
|
||||
private void CollectSiblingNodes(ModelItem item, GeometricBoundaryAnalysis analysis)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (item.Parent != null)
|
||||
{
|
||||
var siblings = item.Parent.Children.Where(child =>
|
||||
child.HasGeometry && !child.Equals(item)).ToList();
|
||||
|
||||
// 只收集包围盒有重叠或非常接近的兄弟节点
|
||||
var itemBbox = item.BoundingBox();
|
||||
var tolerance = 0.1; // 10cm容差
|
||||
|
||||
foreach (var sibling in siblings)
|
||||
{
|
||||
var siblingBbox = sibling.BoundingBox();
|
||||
if (BoundingBoxesIntersectWithTolerance(itemBbox, siblingBbox, tolerance))
|
||||
{
|
||||
analysis.RelatedNodes.Add(sibling);
|
||||
}
|
||||
}
|
||||
|
||||
LogManager.Debug($"[兄弟节点收集] 收集了 {siblings.Count} 个相关兄弟节点");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Warning($"[兄弟节点收集] 收集异常: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算包围盒体积
|
||||
/// </summary>
|
||||
private double CalculateBoundingBoxVolume(BoundingBox3D bbox)
|
||||
{
|
||||
try
|
||||
{
|
||||
var width = Math.Abs(bbox.Max.X - bbox.Min.X);
|
||||
var height = Math.Abs(bbox.Max.Y - bbox.Min.Y);
|
||||
var depth = Math.Abs(bbox.Max.Z - bbox.Min.Z);
|
||||
return width * height * depth;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分析几何包含关系
|
||||
/// </summary>
|
||||
private void AnalyzeGeometricContainment(GeometricBoundaryAnalysis analysis)
|
||||
{
|
||||
try
|
||||
{
|
||||
analysis.ContainmentRelations = new Dictionary<ModelItem, List<ModelItem>>();
|
||||
|
||||
foreach (var item in analysis.RelatedNodes)
|
||||
{
|
||||
analysis.ContainmentRelations[item] = new List<ModelItem>();
|
||||
var itemBbox = item.BoundingBox();
|
||||
|
||||
foreach (var other in analysis.RelatedNodes)
|
||||
{
|
||||
if (item.Equals(other)) continue;
|
||||
|
||||
var otherBbox = other.BoundingBox();
|
||||
if (BoundingBoxContains(itemBbox, otherBbox))
|
||||
{
|
||||
analysis.ContainmentRelations[item].Add(other);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogManager.Debug($"[几何包含分析] 分析了 {analysis.RelatedNodes.Count} 个节点的包含关系");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Warning($"[几何包含分析] 分析异常: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定义逻辑物理对象边界
|
||||
/// </summary>
|
||||
private void DefineLogicalObjectBoundary(GeometricBoundaryAnalysis analysis)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 策略:找到一个合适的"逻辑根节点"
|
||||
// 优先级:1. 有明确名称的节点 2. 包含关系合理的节点 3. 包围盒大小适中的节点
|
||||
|
||||
var candidates = analysis.RelatedNodes.Where(node =>
|
||||
!string.IsNullOrEmpty(node.DisplayName) &&
|
||||
node.HasGeometry).ToList();
|
||||
|
||||
if (candidates.Any())
|
||||
{
|
||||
// 选择包围盒最接近原始选择对象的有名称节点
|
||||
var originalVolume = CalculateBoundingBoxVolume(analysis.RootBoundingBox);
|
||||
|
||||
var bestCandidate = candidates
|
||||
.OrderBy(c => Math.Abs(CalculateBoundingBoxVolume(c.BoundingBox()) - originalVolume))
|
||||
.First();
|
||||
|
||||
analysis.LogicalObjectScope = LogicalObjectScope.NodeAndDescendants;
|
||||
analysis.LogicalRootNode = bestCandidate;
|
||||
|
||||
LogManager.Debug($"[逻辑边界定义] 选择逻辑根节点: {bestCandidate.DisplayName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 没有合适的候选,使用原始选择
|
||||
analysis.LogicalObjectScope = LogicalObjectScope.SelectedNodeOnly;
|
||||
analysis.LogicalRootNode = analysis.SelectedObject;
|
||||
|
||||
LogManager.Debug($"[逻辑边界定义] 使用原始选择作为逻辑边界");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Warning($"[逻辑边界定义] 定义异常: {ex.Message}");
|
||||
analysis.LogicalObjectScope = LogicalObjectScope.SelectedNodeOnly;
|
||||
analysis.LogicalRootNode = analysis.SelectedObject;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建备用分析结果(当主分析失败时)
|
||||
/// </summary>
|
||||
private GeometricBoundaryAnalysis CreateFallbackAnalysis(ModelItem selectedObject)
|
||||
{
|
||||
return new GeometricBoundaryAnalysis
|
||||
{
|
||||
SelectedObject = selectedObject,
|
||||
RootBoundingBox = selectedObject.BoundingBox(),
|
||||
RelatedNodes = new List<ModelItem> { selectedObject },
|
||||
LogicalObjectScope = LogicalObjectScope.SelectedNodeOnly,
|
||||
LogicalRootNode = selectedObject,
|
||||
ContainmentRelations = new Dictionary<ModelItem, List<ModelItem>>()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 补充空间相关的对象到排除列表
|
||||
/// </summary>
|
||||
/// <param name="animatedObject">动画对象</param>
|
||||
/// <param name="exclusionList">当前排除列表</param>
|
||||
private void SupplementSpatiallyRelatedObjects(ModelItem animatedObject, List<ModelItem> exclusionList)
|
||||
{
|
||||
try
|
||||
{
|
||||
LogManager.Debug($"[空间关联补充] 开始补充空间相关对象");
|
||||
|
||||
var animatedBBox = animatedObject.BoundingBox();
|
||||
var originalCount = exclusionList.Count;
|
||||
|
||||
// 获取所有有几何体的对象进行空间分析
|
||||
var allItems = Application.ActiveDocument.Models.RootItemDescendantsAndSelf
|
||||
.Where(item => item.HasGeometry && !exclusionList.Contains(item))
|
||||
.ToList();
|
||||
|
||||
foreach (var item in allItems)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 使用 BelongsToSameLogicalObject 方法进行综合判断
|
||||
if (BelongsToSameLogicalObject(animatedObject, item))
|
||||
{
|
||||
exclusionList.Add(item);
|
||||
LogManager.Debug($"[空间关联补充] 添加空间相关对象: {item.DisplayName}");
|
||||
}
|
||||
}
|
||||
catch (Exception itemEx)
|
||||
{
|
||||
LogManager.Warning($"[空间关联补充] 分析对象时异常 {item.DisplayName}: {itemEx.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
var addedCount = exclusionList.Count - originalCount;
|
||||
LogManager.Debug($"[空间关联补充] 补充了 {addedCount} 个空间相关对象");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Warning($"[空间关联补充] 补充过程异常: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 综合判断两个ModelItem是否属于同一个逻辑物理对象
|
||||
/// </summary>
|
||||
/// <param name="item1">对象1</param>
|
||||
/// <param name="item2">对象2</param>
|
||||
/// <returns>如果属于同一逻辑对象返回true</returns>
|
||||
private bool BelongsToSameLogicalObject(ModelItem item1, ModelItem item2)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. 引用相等检查
|
||||
if (ReferenceEquals(item1, item2) || item1.Equals(item2))
|
||||
{
|
||||
LogManager.Debug($"[逻辑对象判断] 引用相等: {item1.DisplayName}");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. 获取包围盒
|
||||
var bbox1 = item1.BoundingBox();
|
||||
var bbox2 = item2.BoundingBox();
|
||||
|
||||
// 3. 包含关系检查
|
||||
if (BoundingBoxContains(bbox1, bbox2) || BoundingBoxContains(bbox2, bbox1))
|
||||
{
|
||||
LogManager.Debug($"[逻辑对象判断] 包含关系: {item1.DisplayName} <-> {item2.DisplayName}");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 4. 显著重叠检查
|
||||
if (BoundingBoxesHaveSignificantOverlap(bbox1, bbox2, 0.7)) // 70%重叠阈值
|
||||
{
|
||||
LogManager.Debug($"[逻辑对象判断] 显著重叠: {item1.DisplayName} <-> {item2.DisplayName}");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 5. 空间接近检查(对于小对象)
|
||||
var volume1 = CalculateBoundingBoxVolume(bbox1);
|
||||
var volume2 = CalculateBoundingBoxVolume(bbox2);
|
||||
var maxVolume = Math.Max(volume1, volume2);
|
||||
|
||||
// 如果是相对较小的对象(体积小于1立方米),使用更严格的接近检查
|
||||
if (maxVolume < 1.0 && BoundingBoxesAreVeryClose(bbox1, bbox2, 0.05)) // 5cm阈值
|
||||
{
|
||||
LogManager.Debug($"[逻辑对象判断] 小对象接近: {item1.DisplayName} <-> {item2.DisplayName}");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Warning($"[逻辑对象判断] 判断异常: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查一个包围盒是否完全包含另一个包围盒
|
||||
/// </summary>
|
||||
/// <param name="container">外层包围盒</param>
|
||||
/// <param name="contained">内层包围盒</param>
|
||||
/// <param name="tolerance">容差值,默认1mm</param>
|
||||
/// <returns>如果container完全包含contained返回true</returns>
|
||||
private bool BoundingBoxContains(BoundingBox3D container, BoundingBox3D contained, double tolerance = 0.001)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查六个面是否都在容差范围内包含
|
||||
var containsMinX = container.Min.X <= contained.Min.X + tolerance;
|
||||
var containsMinY = container.Min.Y <= contained.Min.Y + tolerance;
|
||||
var containsMinZ = container.Min.Z <= contained.Min.Z + tolerance;
|
||||
|
||||
var containsMaxX = container.Max.X >= contained.Max.X - tolerance;
|
||||
var containsMaxY = container.Max.Y >= contained.Max.Y - tolerance;
|
||||
var containsMaxZ = container.Max.Z >= contained.Max.Z - tolerance;
|
||||
|
||||
var result = containsMinX && containsMinY && containsMinZ &&
|
||||
containsMaxX && containsMaxY && containsMaxZ;
|
||||
|
||||
if (result)
|
||||
{
|
||||
LogManager.Debug($"[包围盒包含] 检测到包含关系: " +
|
||||
$"外层[{container.Min.X:F2},{container.Min.Y:F2},{container.Min.Z:F2}] - [{container.Max.X:F2},{container.Max.Y:F2},{container.Max.Z:F2}] " +
|
||||
$"包含 内层[{contained.Min.X:F2},{contained.Min.Y:F2},{contained.Min.Z:F2}] - [{contained.Max.X:F2},{contained.Max.Y:F2},{contained.Max.Z:F2}]");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Warning($"[包围盒包含] 检测异常: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查两个包围盒是否有显著重叠(用于判断是否属于同一逻辑对象)
|
||||
/// </summary>
|
||||
/// <param name="bbox1">包围盒1</param>
|
||||
/// <param name="bbox2">包围盒2</param>
|
||||
/// <param name="overlapThreshold">重叠阈值(0.0-1.0),表示重叠体积占较小包围盒体积的比例</param>
|
||||
/// <returns>如果重叠程度超过阈值返回true</returns>
|
||||
private bool BoundingBoxesHaveSignificantOverlap(BoundingBox3D bbox1, BoundingBox3D bbox2, double overlapThreshold = 0.5)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 计算重叠区域
|
||||
var overlapMin = new Point3D(
|
||||
Math.Max(bbox1.Min.X, bbox2.Min.X),
|
||||
Math.Max(bbox1.Min.Y, bbox2.Min.Y),
|
||||
Math.Max(bbox1.Min.Z, bbox2.Min.Z)
|
||||
);
|
||||
|
||||
var overlapMax = new Point3D(
|
||||
Math.Min(bbox1.Max.X, bbox2.Max.X),
|
||||
Math.Min(bbox1.Max.Y, bbox2.Max.Y),
|
||||
Math.Min(bbox1.Max.Z, bbox2.Max.Z)
|
||||
);
|
||||
|
||||
// 检查是否有重叠
|
||||
if (overlapMin.X >= overlapMax.X || overlapMin.Y >= overlapMax.Y || overlapMin.Z >= overlapMax.Z)
|
||||
{
|
||||
return false; // 没有重叠
|
||||
}
|
||||
|
||||
// 计算重叠体积
|
||||
var overlapVolume = (overlapMax.X - overlapMin.X) *
|
||||
(overlapMax.Y - overlapMin.Y) *
|
||||
(overlapMax.Z - overlapMin.Z);
|
||||
|
||||
// 计算两个包围盒的体积
|
||||
var volume1 = CalculateBoundingBoxVolume(bbox1);
|
||||
var volume2 = CalculateBoundingBoxVolume(bbox2);
|
||||
var smallerVolume = Math.Min(volume1, volume2);
|
||||
|
||||
if (smallerVolume <= 0) return false;
|
||||
|
||||
var overlapRatio = overlapVolume / smallerVolume;
|
||||
var hasSignificantOverlap = overlapRatio >= overlapThreshold;
|
||||
|
||||
if (hasSignificantOverlap)
|
||||
{
|
||||
LogManager.Debug($"[包围盒重叠] 检测到显著重叠: 重叠比例 {overlapRatio:F3} >= {overlapThreshold:F3}");
|
||||
}
|
||||
|
||||
return hasSignificantOverlap;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Warning($"[包围盒重叠] 检测异常: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查两个对象是否在空间上非常接近(可能是同一逻辑对象的不同表示)
|
||||
/// </summary>
|
||||
/// <param name="bbox1">包围盒1</param>
|
||||
/// <param name="bbox2">包围盒2</param>
|
||||
/// <param name="proximityThreshold">接近阈值(米),默认10cm</param>
|
||||
/// <returns>如果两个对象非常接近返回true</returns>
|
||||
private bool BoundingBoxesAreVeryClose(BoundingBox3D bbox1, BoundingBox3D bbox2, double proximityThreshold = 0.1)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 计算两个包围盒中心点的距离
|
||||
var center1 = new Point3D(
|
||||
(bbox1.Min.X + bbox1.Max.X) / 2,
|
||||
(bbox1.Min.Y + bbox1.Max.Y) / 2,
|
||||
(bbox1.Min.Z + bbox1.Max.Z) / 2
|
||||
);
|
||||
|
||||
var center2 = new Point3D(
|
||||
(bbox2.Min.X + bbox2.Max.X) / 2,
|
||||
(bbox2.Min.Y + bbox2.Max.Y) / 2,
|
||||
(bbox2.Min.Z + bbox2.Max.Z) / 2
|
||||
);
|
||||
|
||||
var centerDistance = CalculatePointDistance(center1, center2);
|
||||
var areClose = centerDistance <= proximityThreshold;
|
||||
|
||||
if (areClose)
|
||||
{
|
||||
LogManager.Debug($"[包围盒接近] 检测到接近对象: 中心距离 {centerDistance:F3}m <= {proximityThreshold:F3}m");
|
||||
}
|
||||
|
||||
return areClose;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Warning($"[包围盒接近] 检测异常: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算两个3D点之间的距离
|
||||
/// </summary>
|
||||
private double CalculatePointDistance(Point3D point1, Point3D point2)
|
||||
{
|
||||
var dx = point1.X - point2.X;
|
||||
var dy = point1.Y - point2.Y;
|
||||
var dz = point1.Z - point2.Z;
|
||||
return Math.Sqrt(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查两个包围盒是否相交(带容差)
|
||||
/// </summary>
|
||||
private bool BoundingBoxesIntersectWithTolerance(BoundingBox3D box1, BoundingBox3D box2, double tolerance)
|
||||
{
|
||||
return box1.Min.X <= box2.Max.X + tolerance && box1.Max.X >= box2.Min.X - tolerance &&
|
||||
box1.Min.Y <= box2.Max.Y + tolerance && box1.Max.Y >= box2.Min.Y - tolerance &&
|
||||
box1.Min.Z <= box2.Max.Z + tolerance && box1.Max.Z >= box2.Min.Z - tolerance;
|
||||
}
|
||||
|
||||
#region 几何体归属分析数据结构
|
||||
|
||||
/// <summary>
|
||||
/// 几何边界分析结果
|
||||
/// </summary>
|
||||
private class GeometricBoundaryAnalysis
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户选中的原始对象
|
||||
/// </summary>
|
||||
public ModelItem SelectedObject { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 原始对象的包围盒
|
||||
/// </summary>
|
||||
public BoundingBox3D RootBoundingBox { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所有相关节点(父、子、兄弟等)
|
||||
/// </summary>
|
||||
public List<ModelItem> RelatedNodes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 逻辑物理对象的范围定义
|
||||
/// </summary>
|
||||
public LogicalObjectScope LogicalObjectScope { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 逻辑对象的根节点
|
||||
/// </summary>
|
||||
public ModelItem LogicalRootNode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 节点间的包含关系映射
|
||||
/// </summary>
|
||||
public Dictionary<ModelItem, List<ModelItem>> ContainmentRelations { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 逻辑对象范围枚举
|
||||
/// </summary>
|
||||
private enum LogicalObjectScope
|
||||
{
|
||||
/// <summary>
|
||||
/// 仅选中节点本身
|
||||
/// </summary>
|
||||
SelectedNodeOnly,
|
||||
|
||||
/// <summary>
|
||||
/// 选中节点及其所有后代
|
||||
/// </summary>
|
||||
NodeAndDescendants,
|
||||
|
||||
/// <summary>
|
||||
/// 选中节点的父节点及其所有子树
|
||||
/// </summary>
|
||||
ParentAndSiblings,
|
||||
|
||||
/// <summary>
|
||||
/// 整个逻辑组件(自动识别的边界)
|
||||
/// </summary>
|
||||
EntireLogicalComponent
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// 其他辅助方法将在下一步添加...
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 资源清理
|
||||
/// </summary>
|
||||
|
||||
@ -305,19 +305,8 @@ namespace NavisworksTransport.Core.Animation
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_animationTimer != null)
|
||||
{
|
||||
_animationTimer.Stop();
|
||||
_animationTimer.Dispose();
|
||||
_animationTimer = null;
|
||||
}
|
||||
|
||||
if (_collisionTimer != null)
|
||||
{
|
||||
_collisionTimer.Stop();
|
||||
_collisionTimer.Dispose();
|
||||
_collisionTimer = null;
|
||||
}
|
||||
// 清理定时器资源
|
||||
CleanupTimers();
|
||||
|
||||
SetState(AnimationState.Stopped);
|
||||
_pausedProgress = 0.0; // 重置暂停进度
|
||||
@ -338,6 +327,63 @@ namespace NavisworksTransport.Core.Animation
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 完成动画(当动画自然播放结束时调用)
|
||||
/// </summary>
|
||||
private void FinishAnimation()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 清理定时器资源
|
||||
CleanupTimers();
|
||||
|
||||
// 重置暂停进度
|
||||
_pausedProgress = 0.0;
|
||||
|
||||
// 直接设置为完成状态,避免中间状态切换
|
||||
SetState(AnimationState.Finished);
|
||||
|
||||
LogManager.Info("动画播放完成");
|
||||
|
||||
// 触发旧版完成事件(保持兼容性)
|
||||
AnimationCompleted?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
// 更新 TimeLiner 任务状态
|
||||
if (_timeLinerManager != null && !string.IsNullOrEmpty(_currentTaskId))
|
||||
{
|
||||
_timeLinerManager.UpdateTaskProgress(_currentTaskId, 1.0, AnimationState.Finished);
|
||||
}
|
||||
|
||||
// 动画结束后统一创建所有碰撞测试(基于官方示例的批量处理)
|
||||
LogManager.Info("动画播放完成,开始创建最终的碰撞测试汇总...");
|
||||
ClashDetectiveIntegration.Instance.CreateAllAnimationCollisionTests();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Error($"完成动画失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理定时器资源的私有方法
|
||||
/// </summary>
|
||||
private void CleanupTimers()
|
||||
{
|
||||
if (_animationTimer != null)
|
||||
{
|
||||
_animationTimer.Stop();
|
||||
_animationTimer.Dispose();
|
||||
_animationTimer = null;
|
||||
}
|
||||
|
||||
if (_collisionTimer != null)
|
||||
{
|
||||
_collisionTimer.Stop();
|
||||
_collisionTimer.Dispose();
|
||||
_collisionTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 暂停动画
|
||||
/// </summary>
|
||||
@ -470,14 +516,8 @@ namespace NavisworksTransport.Core.Animation
|
||||
|
||||
if (progress >= 1.0)
|
||||
{
|
||||
StopAnimation();
|
||||
SetState(AnimationState.Finished); // 标记为完成
|
||||
AnimationCompleted?.Invoke(this, EventArgs.Empty); // 触发旧版完成事件
|
||||
LogManager.Info("动画播放完成");
|
||||
|
||||
// 动画结束后统一创建所有碰撞测试(基于官方示例的批量处理)
|
||||
LogManager.Info("动画播放完成,开始创建最终的碰撞测试汇总...");
|
||||
ClashDetectiveIntegration.Instance.CreateAllAnimationCollisionTests();
|
||||
// 避免双重状态设置,直接完成动画并清理资源
|
||||
FinishAnimation();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@ -6,6 +6,7 @@ using Autodesk.Navisworks.Api;
|
||||
using Autodesk.Navisworks.Api.Clash;
|
||||
using ComApi = Autodesk.Navisworks.Api.Interop.ComApi;
|
||||
using ComApiBridge = Autodesk.Navisworks.Api.ComApi;
|
||||
using NavisworksTransport.UI.WPF;
|
||||
|
||||
namespace NavisworksTransport
|
||||
{
|
||||
@ -280,15 +281,76 @@ namespace NavisworksTransport
|
||||
{
|
||||
try
|
||||
{
|
||||
// 🔍 添加参数验证的详细诊断
|
||||
LogManager.Debug($"[诊断-CacheCollision] 接收到缓存碰撞请求:");
|
||||
LogManager.Debug($" 动画对象: {animatedObject?.DisplayName ?? "NULL"} (Valid: {IsModelItemValid(animatedObject)})");
|
||||
LogManager.Debug($" 碰撞对象: {collisionObject?.DisplayName ?? "NULL"} (Valid: {IsModelItemValid(collisionObject)})");
|
||||
LogManager.Debug($" 动画位置: ({animatedObjectPosition.X:F2},{animatedObjectPosition.Y:F2},{animatedObjectPosition.Z:F2})");
|
||||
|
||||
if (collisionObjectPosition != null)
|
||||
{
|
||||
LogManager.Debug($" 碰撞位置: ({collisionObjectPosition.X:F2},{collisionObjectPosition.Y:F2},{collisionObjectPosition.Z:F2})");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogManager.Debug($" 碰撞位置: NULL (将自动计算)");
|
||||
}
|
||||
|
||||
// 🔍 检查是否为自碰撞
|
||||
if (animatedObject != null && collisionObject != null)
|
||||
{
|
||||
var isEquals = animatedObject.Equals(collisionObject);
|
||||
var isSameGuid = animatedObject.InstanceGuid == collisionObject.InstanceGuid;
|
||||
var isSameName = animatedObject.DisplayName == collisionObject.DisplayName;
|
||||
|
||||
if (isEquals || isSameGuid || isSameName)
|
||||
{
|
||||
LogManager.Warning($"[诊断-自碰撞警告] CacheCollision检测到可疑自碰撞:");
|
||||
LogManager.Warning($" Equals: {isEquals}");
|
||||
LogManager.Warning($" SameGuid: {isSameGuid}");
|
||||
LogManager.Warning($" SameName: {isSameName}");
|
||||
|
||||
if (isEquals)
|
||||
{
|
||||
LogManager.Error($"[诊断-严重错误] animatedObject.Equals(collisionObject) = true! 这是明确的自碰撞,应该跳过!");
|
||||
return; // 直接返回,不处理自碰撞
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!IsModelItemValid(animatedObject) || !IsModelItemValid(collisionObject))
|
||||
{
|
||||
LogManager.Warning($"[诊断-无效对象] 对象验证失败,退出缓存过程");
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用精确的碰撞检测算法
|
||||
var animatedBoundingBox = animatedObject.BoundingBox();
|
||||
var collisionBoundingBox = collisionObject.BoundingBox();
|
||||
|
||||
LogManager.Debug($"[诊断-包围盒] 动画对象包围盒: Min({animatedBoundingBox.Min.X:F2},{animatedBoundingBox.Min.Y:F2},{animatedBoundingBox.Min.Z:F2}) Max({animatedBoundingBox.Max.X:F2},{animatedBoundingBox.Max.Y:F2},{animatedBoundingBox.Max.Z:F2})");
|
||||
LogManager.Debug($"[诊断-包围盒] 碰撞对象包围盒: Min({collisionBoundingBox.Min.X:F2},{collisionBoundingBox.Min.Y:F2},{collisionBoundingBox.Min.Z:F2}) Max({collisionBoundingBox.Max.X:F2},{collisionBoundingBox.Max.Y:F2},{collisionBoundingBox.Max.Z:F2})");
|
||||
|
||||
if (BoundingBoxesIntersect(animatedBoundingBox, collisionBoundingBox))
|
||||
{
|
||||
// 🔍 计算并验证位置信息
|
||||
var finalCollisionPosition = collisionObjectPosition ?? GetObjectPosition(collisionObject);
|
||||
|
||||
LogManager.Debug($"[诊断-位置计算] 最终使用的碰撞对象位置: ({finalCollisionPosition.X:F2},{finalCollisionPosition.Y:F2},{finalCollisionPosition.Z:F2})");
|
||||
|
||||
// 🔍 检查位置是否相同(可能的自碰撞标志)
|
||||
var positionDistance = Math.Sqrt(
|
||||
Math.Pow(animatedObjectPosition.X - finalCollisionPosition.X, 2) +
|
||||
Math.Pow(animatedObjectPosition.Y - finalCollisionPosition.Y, 2) +
|
||||
Math.Pow(animatedObjectPosition.Z - finalCollisionPosition.Z, 2));
|
||||
|
||||
LogManager.Debug($"[诊断-位置距离] 动画对象与碰撞对象的位置距离: {positionDistance:F4}");
|
||||
|
||||
if (positionDistance < 0.001) // 位置基本相同
|
||||
{
|
||||
LogManager.Warning($"[诊断-位置警告] 两个对象位置几乎相同 (距离: {positionDistance:F6}),这可能是自碰撞的标志!");
|
||||
}
|
||||
|
||||
// 创建精确的碰撞结果
|
||||
var collision = new CollisionResult
|
||||
{
|
||||
@ -301,7 +363,7 @@ namespace NavisworksTransport
|
||||
Distance = CalculateDistance(animatedBoundingBox, collisionBoundingBox),
|
||||
Center = CalculateCenter(animatedBoundingBox, collisionBoundingBox),
|
||||
Item1Position = animatedObjectPosition,
|
||||
Item2Position = collisionObjectPosition ?? GetObjectPosition(collisionObject),
|
||||
Item2Position = finalCollisionPosition,
|
||||
HasPositionInfo = true
|
||||
};
|
||||
|
||||
@ -321,10 +383,15 @@ namespace NavisworksTransport
|
||||
LogManager.Debug($"跳过重复碰撞: {animatedObject.DisplayName} <-> {collisionObject.DisplayName}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LogManager.Debug($"[诊断-包围盒] 包围盒不相交,跳过缓存");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Error($"缓存碰撞失败: {ex.Message}");
|
||||
LogManager.Error($"[诊断-异常堆栈] {ex.StackTrace}");
|
||||
}
|
||||
}
|
||||
|
||||
@ -958,6 +1025,119 @@ namespace NavisworksTransport
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增强的自碰撞检测逻辑 - 多重验证确保不会检测对象与自身的碰撞
|
||||
/// </summary>
|
||||
/// <param name="obj1">动画对象</param>
|
||||
/// <param name="obj2">检测对象</param>
|
||||
/// <returns>如果是同一对象返回true</returns>
|
||||
private bool IsSameObjectAdvanced(ModelItem obj1, ModelItem obj2)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. 首要检查:对象引用相等
|
||||
if (ReferenceEquals(obj1, obj2))
|
||||
{
|
||||
LogManager.Debug($"[自碰撞检测] 引用相等检测: 同一对象 {obj1.DisplayName}");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Equals方法检查(Navisworks标准检查)
|
||||
if (obj1.Equals(obj2))
|
||||
{
|
||||
LogManager.Debug($"[自碰撞检测] Equals方法检测: 同一对象 {obj1.DisplayName}");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. InstanceGuid检查 - 但要考虑无效GUID的情况
|
||||
var guid1 = obj1.InstanceGuid;
|
||||
var guid2 = obj2.InstanceGuid;
|
||||
var isValidGuid1 = guid1 != Guid.Empty;
|
||||
var isValidGuid2 = guid2 != Guid.Empty;
|
||||
|
||||
// 只有当两个GUID都有效且相同时,才认为是同一对象
|
||||
if (isValidGuid1 && isValidGuid2 && guid1 == guid2)
|
||||
{
|
||||
LogManager.Debug($"[自碰撞检测] 有效GUID检测: 同一对象 {obj1.DisplayName} (GUID: {guid1})");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 4. 如果GUID无效,使用DisplayName + 包围盒位置组合检查
|
||||
if (!isValidGuid1 || !isValidGuid2)
|
||||
{
|
||||
var name1 = obj1.DisplayName ?? "";
|
||||
var name2 = obj2.DisplayName ?? "";
|
||||
|
||||
// 名称相同且都有几何体,进一步检查位置
|
||||
if (name1 == name2 && !string.IsNullOrEmpty(name1) && obj1.HasGeometry && obj2.HasGeometry)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bbox1 = obj1.BoundingBox();
|
||||
var bbox2 = obj2.BoundingBox();
|
||||
|
||||
// 检查包围盒是否完全相同(精确到小数点后3位)
|
||||
var centerDistance = CalculatePointDistance(
|
||||
new Point3D((bbox1.Min.X + bbox1.Max.X) / 2, (bbox1.Min.Y + bbox1.Max.Y) / 2, (bbox1.Min.Z + bbox1.Max.Z) / 2),
|
||||
new Point3D((bbox2.Min.X + bbox2.Max.X) / 2, (bbox2.Min.Y + bbox2.Max.Y) / 2, (bbox2.Min.Z + bbox2.Max.Z) / 2)
|
||||
);
|
||||
|
||||
// 如果中心点距离非常小(小于1mm),认为是同一对象
|
||||
if (centerDistance < 0.001) // 1mm以内认为是同一位置
|
||||
{
|
||||
LogManager.Debug($"[自碰撞检测] 位置+名称检测: 同一对象 {name1} (中心距离: {centerDistance:F6})");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Warning($"[自碰撞检测] 包围盒比较失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 最后兜底:检查空DisplayName的情况
|
||||
var displayName1 = obj1.DisplayName ?? "";
|
||||
var displayName2 = obj2.DisplayName ?? "";
|
||||
|
||||
// 如果一个对象有名称,另一个没有,但GUID相同且无效,可能是同一对象的不同表示
|
||||
if ((string.IsNullOrEmpty(displayName1) && !string.IsNullOrEmpty(displayName2)) ||
|
||||
(!string.IsNullOrEmpty(displayName1) && string.IsNullOrEmpty(displayName2)))
|
||||
{
|
||||
if (guid1 == guid2 && guid1 == Guid.Empty)
|
||||
{
|
||||
LogManager.Debug($"[自碰撞检测] 空名称+无效GUID检测: 疑似同一对象 '{displayName1}' vs '{displayName2}'");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Warning($"[自碰撞检测] 检测过程出错: {ex.Message}");
|
||||
return false; // 出错时保守处理,不跳过
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算两个3D点之间的距离
|
||||
/// </summary>
|
||||
private double CalculatePointDistance(Point3D point1, Point3D point2)
|
||||
{
|
||||
var dx = point1.X - point2.X;
|
||||
var dy = point1.Y - point2.Y;
|
||||
var dz = point1.Z - point2.Z;
|
||||
return Math.Sqrt(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
|
||||
#region 智能排除策略逻辑
|
||||
|
||||
// 注意:智能排除策略相关代码已移至LogisticsAnimationManager
|
||||
// ClashDetectiveIntegration现在专注于纯碰撞检测功能
|
||||
|
||||
#endregion
|
||||
|
||||
private DateTime _lastTestCreationTime = DateTime.MinValue;
|
||||
|
||||
/// <summary>
|
||||
@ -969,6 +1149,7 @@ namespace NavisworksTransport
|
||||
return DateTime.Now - _lastTestCreationTime > TimeSpan.FromSeconds(5);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 确保动态测试存在
|
||||
/// </summary>
|
||||
@ -1529,34 +1710,53 @@ namespace NavisworksTransport
|
||||
|
||||
try
|
||||
{
|
||||
LogManager.Debug($"开始简化碰撞检测,动画对象: {animatedObject.DisplayName}");
|
||||
LogManager.Debug($"开始纯碰撞检测,动画对象: {animatedObject.DisplayName}");
|
||||
|
||||
var animatedBoundingBox = animatedObject.BoundingBox();
|
||||
LogManager.Debug($"动画对象包围盒: Min({animatedBoundingBox.Min.X:F2}, {animatedBoundingBox.Min.Y:F2}, {animatedBoundingBox.Min.Z:F2}) Max({animatedBoundingBox.Max.X:F2}, {animatedBoundingBox.Max.Y:F2}, {animatedBoundingBox.Max.Z:F2})");
|
||||
|
||||
// 构建基本排除列表:动画对象本身 + 子对象 + 用户指定排除对象
|
||||
var exclusionList = new List<ModelItem>();
|
||||
|
||||
// 1. 排除动画对象本身及其所有子对象
|
||||
exclusionList.AddRange(animatedObject.DescendantsAndSelf.Where(d => d.HasGeometry));
|
||||
|
||||
// 2. 合并用户指定的排除对象
|
||||
if (excludeObjects != null)
|
||||
{
|
||||
foreach (var item in excludeObjects)
|
||||
{
|
||||
if (!exclusionList.Contains(item))
|
||||
{
|
||||
exclusionList.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var allItems = Application.ActiveDocument.Models.RootItemDescendantsAndSelf
|
||||
.Where(item => item.HasGeometry && !item.Equals(animatedObject));
|
||||
|
||||
if (excludeObjects != null)
|
||||
{
|
||||
allItems = allItems.Where(item => !excludeObjects.Contains(item));
|
||||
}
|
||||
|
||||
// 排除通道物体,避免它们在碰撞检测中变红
|
||||
allItems = allItems.Where(item => !IsChannelObject(item));
|
||||
|
||||
var itemList = allItems.ToList();
|
||||
LogManager.Debug($"检测对象总数: {itemList.Count}");
|
||||
LogManager.Debug($"初始检测对象总数: {itemList.Count}");
|
||||
|
||||
int checkedCount = 0;
|
||||
int excludedCount = 0;
|
||||
|
||||
foreach (var item in itemList)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 使用简化的排除逻辑
|
||||
if (ShouldExcludeFromCollisionDetectionSimple(item, exclusionList))
|
||||
{
|
||||
excludedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var itemBoundingBox = item.BoundingBox();
|
||||
|
||||
// 🔥 使用UI设置的检测间隙作为碰撞容差
|
||||
var tolerance = detectionGap; // 直接使用用户设置的检测间隙
|
||||
// 使用UI设置的检测间隙作为碰撞容差
|
||||
var tolerance = detectionGap;
|
||||
|
||||
// 检查是否在容差范围内接近
|
||||
if (BoundingBoxesIntersectWithTolerance(animatedBoundingBox, itemBoundingBox, tolerance))
|
||||
@ -1564,13 +1764,13 @@ namespace NavisworksTransport
|
||||
// 计算真实距离
|
||||
var distance = CalculateDistance(animatedBoundingBox, itemBoundingBox);
|
||||
|
||||
// 🔥 只有真正相交(距离为0)或在检测间隙内的才算碰撞
|
||||
// 只有真正相交(距离为0)或在检测间隙内的才算碰撞
|
||||
if (distance <= tolerance)
|
||||
{
|
||||
var result = new CollisionResult
|
||||
{
|
||||
ClashGuid = Guid.NewGuid(),
|
||||
DisplayName = $"简化碰撞检测: {animatedObject.DisplayName} <-> {item.DisplayName}",
|
||||
DisplayName = $"纯碰撞检测: {animatedObject.DisplayName} <-> {item.DisplayName}",
|
||||
Status = ClashResultStatus.New,
|
||||
Item1 = animatedObject,
|
||||
Item2 = item,
|
||||
@ -1582,10 +1782,6 @@ namespace NavisworksTransport
|
||||
results.Add(result);
|
||||
LogManager.Info($"检测到碰撞: {animatedObject.DisplayName} <-> {item.DisplayName},距离: {result.Distance:F2}");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogManager.Debug($"对象接近但未碰撞: {animatedObject.DisplayName} <-> {item.DisplayName},距离: {distance:F2}");
|
||||
}
|
||||
}
|
||||
|
||||
checkedCount++;
|
||||
@ -1596,16 +1792,56 @@ namespace NavisworksTransport
|
||||
}
|
||||
}
|
||||
|
||||
LogManager.Debug($"简化碰撞检测完成: 检查了 {checkedCount} 个对象,发现 {results.Count} 个碰撞");
|
||||
LogManager.Info($"纯碰撞检测完成统计:");
|
||||
LogManager.Info($" - 检查对象: {checkedCount} 个");
|
||||
LogManager.Info($" - 排除对象: {excludedCount} 个");
|
||||
LogManager.Info($" - 发现碰撞: {results.Count} 个");
|
||||
LogManager.Info($" - 排除效率: {(excludedCount * 100.0 / (checkedCount + excludedCount)):F1}%");
|
||||
|
||||
if (results.Count == 0)
|
||||
{
|
||||
LogManager.Info($"未发现碰撞");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Error($"简化碰撞检测失败: {ex.Message}");
|
||||
LogManager.Error($"纯碰撞检测失败: {ex.Message}", ex);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 简化的排除判断逻辑(不依赖LogisticsAnimationManager)
|
||||
/// </summary>
|
||||
/// <param name="testObject">要测试的对象</param>
|
||||
/// <param name="exclusionList">排除列表</param>
|
||||
/// <returns>如果应该排除返回true</returns>
|
||||
private bool ShouldExcludeFromCollisionDetectionSimple(ModelItem testObject, List<ModelItem> exclusionList)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. 基本排除列表检查
|
||||
if (exclusionList != null && exclusionList.Contains(testObject))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. 通道对象检查(保留原有逻辑)
|
||||
if (IsChannelObject(testObject))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Warning($"简化排除判断异常: {ex.Message}");
|
||||
return false; // 出错时保守处理,不排除
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查物体是否为通道物体(通道物体在碰撞检测时不应该变红)
|
||||
/// </summary>
|
||||
|
||||
@ -23,6 +23,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
#region 私有字段
|
||||
|
||||
private readonly NavisworksTransport.Core.Animation.PathAnimationManager _pathAnimationManager;
|
||||
private readonly NavisworksTransport.Core.Animation.LogisticsAnimationManager _logisticsAnimationManager;
|
||||
private readonly ClashDetectiveIntegration _clashIntegration;
|
||||
private readonly UIStateManager _uiStateManager;
|
||||
|
||||
@ -289,6 +290,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
SetProperty(ref _selectedAnimatedObject, value);
|
||||
UpdateAnimatedObjectInfo();
|
||||
UpdateCanGenerateAnimation();
|
||||
|
||||
// ✨ 新功能:动画对象选择时预计算排除列表
|
||||
PrecomputeCollisionExclusionsAsync(value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -351,6 +355,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
{
|
||||
// 初始化管理器
|
||||
_pathAnimationManager = new NavisworksTransport.Core.Animation.PathAnimationManager();
|
||||
_logisticsAnimationManager = new NavisworksTransport.Core.Animation.LogisticsAnimationManager();
|
||||
_clashIntegration = ClashDetectiveIntegration.Instance;
|
||||
_uiStateManager = UIStateManager.Instance;
|
||||
|
||||
@ -370,7 +375,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
// 初始化碰撞检测集成
|
||||
InitializeClashIntegration();
|
||||
|
||||
LogManager.Info("AnimationControlViewModel初始化完成");
|
||||
LogManager.Info("AnimationControlViewModel初始化完成(含缓存管理)");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -908,6 +913,57 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步预计算碰撞排除列表
|
||||
/// </summary>
|
||||
/// <param name="animationObject">动画对象</param>
|
||||
private async void PrecomputeCollisionExclusionsAsync(ModelItem animationObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (animationObject == null)
|
||||
{
|
||||
LogManager.Debug("[缓存预计算] 动画对象为空,跳过预计算");
|
||||
return;
|
||||
}
|
||||
|
||||
LogManager.Info($"[缓存预计算] 开始为新选择的动画对象预计算: {animationObject.DisplayName}");
|
||||
GenerationStatus = "正在分析动画对象...";
|
||||
|
||||
// 在后台线程执行预计算
|
||||
var success = await Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return _logisticsAnimationManager.PrecomputeCollisionExclusions(animationObject);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Error($"[缓存预计算] 后台预计算失败: {ex.Message}", ex);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// 回到UI线程更新状态
|
||||
if (success)
|
||||
{
|
||||
var cacheStats = _logisticsAnimationManager.GetCacheStats();
|
||||
GenerationStatus = $"动画对象分析完成 - {cacheStats}";
|
||||
LogManager.Info($"[缓存预计算] 预计算成功 - {cacheStats}");
|
||||
}
|
||||
else
|
||||
{
|
||||
GenerationStatus = "动画对象分析失败,将使用实时计算";
|
||||
LogManager.Warning("[缓存预计算] 预计算失败,碰撞检测将使用实时计算模式");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Error($"[缓存预计算] 异步预计算过程异常: {ex.Message}", ex);
|
||||
GenerationStatus = "动画对象分析异常";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行生成动画命令(同步执行,因为Navisworks API需要STA线程)
|
||||
/// </summary>
|
||||
@ -1092,6 +1148,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
// 清理动画管理器
|
||||
_pathAnimationManager?.Dispose();
|
||||
|
||||
// 清理LogisticsAnimationManager缓存
|
||||
_logisticsAnimationManager?.ClearExclusionCache();
|
||||
_logisticsAnimationManager?.Dispose();
|
||||
|
||||
// 取消碰撞检测事件订阅
|
||||
if (_clashIntegration != null)
|
||||
{
|
||||
|
||||
@ -57,6 +57,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
private bool _isGridSizeManuallyEnabled = false; // 是否启用手动网格大小设置
|
||||
private double _gridSize = 1.0; // 网格大小(米)
|
||||
|
||||
// 自动路径起点和终点的路径对象引用(用于正确的ID管理)
|
||||
private PathRoute _autoPathStartPointRoute = null;
|
||||
private PathRoute _autoPathEndPointRoute = null;
|
||||
|
||||
// 路径文件管理
|
||||
private string _pathFileStatus = "未保存";
|
||||
|
||||
@ -77,14 +81,64 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
{
|
||||
if (SetProperty(ref _selectedPathRoute, value))
|
||||
{
|
||||
// 更新命令状态
|
||||
OnPropertyChanged(nameof(CanExecuteStartEdit));
|
||||
OnPropertyChanged(nameof(CanExecuteEndEdit));
|
||||
OnPropertyChanged(nameof(CanExecuteClearPath));
|
||||
OnPropertyChanged(nameof(CanExecuteExportPath));
|
||||
OnPropertyChanged(nameof(CanExecuteSaveAsPath));
|
||||
|
||||
// 实现路径选择时的可视化切换
|
||||
UpdatePathVisualization();
|
||||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 更新路径可视化显示:清理其他路径,只显示当前选中的路径
|
||||
/// </summary>
|
||||
private async void UpdatePathVisualization()
|
||||
{
|
||||
await SafeExecuteAsync(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (PathPointRenderPlugin.Instance == null)
|
||||
{
|
||||
LogManager.Warning("PathPointRenderPlugin实例为空,无法更新路径可视化");
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 清理所有现有的路径可视化
|
||||
PathPointRenderPlugin.Instance.ClearAllPaths();
|
||||
LogManager.WriteLog("[路径可视化] 已清理所有现有路径显示");
|
||||
|
||||
// 2. 如果有选中的路径,则显示该路径
|
||||
if (_selectedPathRoute != null && _selectedPathRoute.Points.Count > 0)
|
||||
{
|
||||
// 查找对应的Core路径对象
|
||||
var coreRoute = _pathPlanningManager?.Routes?.FirstOrDefault(r => r.Name == _selectedPathRoute.Name);
|
||||
if (coreRoute != null)
|
||||
{
|
||||
// 使用PathPlanningManager的绘制方法来保持一致性
|
||||
_pathPlanningManager.DrawRouteVisualization(coreRoute, isAutoPath: false);
|
||||
LogManager.Info($"[路径可视化] 已显示选中路径: {_selectedPathRoute.Name},包含 {_selectedPathRoute.Points.Count} 个点");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogManager.Warning($"[路径可视化] 未找到对应的Core路径: {_selectedPathRoute.Name}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LogManager.WriteLog("[路径可视化] 没有选中的路径,仅清理显示");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Error($"[路径可视化] 更新路径可视化失败: {ex.Message}", ex);
|
||||
}
|
||||
}, "更新路径可视化");
|
||||
}
|
||||
|
||||
public bool IsPathEditMode
|
||||
{
|
||||
@ -495,6 +549,18 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
return;
|
||||
}
|
||||
|
||||
// 立即清理现有的起点标记(使用正确的路径ID)
|
||||
if (PathPointRenderPlugin.Instance != null && _autoPathStartPointRoute != null)
|
||||
{
|
||||
PathPointRenderPlugin.Instance.RemovePath(_autoPathStartPointRoute.Id);
|
||||
LogManager.WriteLog($"[选择起点] 已清除之前的起点标记,ID: {_autoPathStartPointRoute.Id}");
|
||||
_autoPathStartPointRoute = null; // 清除引用
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
_hasStartPoint = false;
|
||||
AutoPathStartPoint = "未选择";
|
||||
|
||||
LogManager.Info("PathPlanningManager已初始化,开始设置选择状态");
|
||||
IsSelectingStartPoint = true;
|
||||
IsSelectingEndPoint = false;
|
||||
@ -537,6 +603,18 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
return;
|
||||
}
|
||||
|
||||
// 立即清理现有的终点标记(使用正确的路径ID)
|
||||
if (PathPointRenderPlugin.Instance != null && _autoPathEndPointRoute != null)
|
||||
{
|
||||
PathPointRenderPlugin.Instance.RemovePath(_autoPathEndPointRoute.Id);
|
||||
LogManager.WriteLog($"[选择终点] 已清除之前的终点标记,ID: {_autoPathEndPointRoute.Id}");
|
||||
_autoPathEndPointRoute = null; // 清除引用
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
_hasEndPoint = false;
|
||||
AutoPathEndPoint = "未选择";
|
||||
|
||||
LogManager.Info("PathPlanningManager已初始化,开始设置选择状态");
|
||||
IsSelectingStartPoint = false;
|
||||
IsSelectingEndPoint = true;
|
||||
@ -731,6 +809,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
IsSelectingStartPoint = false;
|
||||
IsSelectingEndPoint = false;
|
||||
|
||||
// 清除路径引用
|
||||
_autoPathStartPointRoute = null;
|
||||
_autoPathEndPointRoute = null;
|
||||
|
||||
// 通知Can Execute属性更改
|
||||
OnPropertyChanged(nameof(CanExecuteAutoPlanPath));
|
||||
|
||||
@ -1250,13 +1332,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
{
|
||||
await SafeExecuteAsync(() =>
|
||||
{
|
||||
// 如果已有起点,先移除之前的可视化
|
||||
if (_hasStartPoint && PathPointRenderPlugin.Instance != null)
|
||||
{
|
||||
// 移除之前的起点路径可视化
|
||||
PathPointRenderPlugin.Instance.RemovePath("AutoPathStartPoint");
|
||||
}
|
||||
|
||||
// 直接设置新的起点(清理逻辑已在ExecuteSelectStartPointAsync中处理)
|
||||
_startPoint3D = point;
|
||||
_hasStartPoint = true;
|
||||
AutoPathStartPoint = $"({point.X:F2}, {point.Y:F2}, {point.Z:F2})";
|
||||
@ -1265,11 +1341,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
// 通知Can Execute属性更改
|
||||
OnPropertyChanged(nameof(CanExecuteAutoPlanPath));
|
||||
|
||||
// 使用新的RenderPointOnly API只渲染起点标记
|
||||
// 渲染新的起点标记
|
||||
if (PathPointRenderPlugin.Instance != null)
|
||||
{
|
||||
// 创建临时路径只包含起点
|
||||
var startPointPathRoute = new PathRoute("AutoPathStartPoint")
|
||||
_autoPathStartPointRoute = new PathRoute("AutoPathStartPoint")
|
||||
{
|
||||
Description = "自动路径规划起点"
|
||||
};
|
||||
@ -1279,10 +1355,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
Position = point,
|
||||
Type = PathPointType.StartPoint
|
||||
};
|
||||
startPointPathRoute.AddPoint(startPathPoint);
|
||||
_autoPathStartPointRoute.AddPoint(startPathPoint);
|
||||
|
||||
// 使用新的只渲染点的API,不绘制连线
|
||||
PathPointRenderPlugin.Instance.RenderPointOnly(startPointPathRoute);
|
||||
PathPointRenderPlugin.Instance.RenderPointOnly(_autoPathStartPointRoute);
|
||||
LogManager.Info($"起点路径已创建,ID: {_autoPathStartPointRoute.Id}");
|
||||
}
|
||||
|
||||
if (_hasEndPoint)
|
||||
@ -1305,13 +1382,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
{
|
||||
await SafeExecuteAsync(() =>
|
||||
{
|
||||
// 如果已有终点,先移除之前的可视化
|
||||
if (_hasEndPoint && PathPointRenderPlugin.Instance != null)
|
||||
{
|
||||
// 移除之前的终点路径可视化
|
||||
PathPointRenderPlugin.Instance.RemovePath("AutoPathEndPoint");
|
||||
}
|
||||
|
||||
// 直接设置新的终点(清理逻辑已在ExecuteSelectEndPointAsync中处理)
|
||||
_endPoint3D = point;
|
||||
_hasEndPoint = true;
|
||||
AutoPathEndPoint = $"({point.X:F2}, {point.Y:F2}, {point.Z:F2})";
|
||||
@ -1320,11 +1391,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
// 通知Can Execute属性更改
|
||||
OnPropertyChanged(nameof(CanExecuteAutoPlanPath));
|
||||
|
||||
// 使用新的RenderPointOnly API只渲染终点标记
|
||||
// 渲染新的终点标记
|
||||
if (PathPointRenderPlugin.Instance != null)
|
||||
{
|
||||
// 创建临时路径只包含终点
|
||||
var endPointPathRoute = new PathRoute("AutoPathEndPoint")
|
||||
_autoPathEndPointRoute = new PathRoute("AutoPathEndPoint")
|
||||
{
|
||||
Description = "自动路径规划终点"
|
||||
};
|
||||
@ -1334,10 +1405,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
Position = point,
|
||||
Type = PathPointType.EndPoint
|
||||
};
|
||||
endPointPathRoute.AddPoint(endPathPoint);
|
||||
_autoPathEndPointRoute.AddPoint(endPathPoint);
|
||||
|
||||
// 使用新的只渲染点的API,不绘制连线
|
||||
PathPointRenderPlugin.Instance.RenderPointOnly(endPointPathRoute);
|
||||
PathPointRenderPlugin.Instance.RenderPointOnly(_autoPathEndPointRoute);
|
||||
LogManager.Info($"终点路径已创建,ID: {_autoPathEndPointRoute.Id}");
|
||||
}
|
||||
|
||||
if (_hasStartPoint)
|
||||
@ -1430,10 +1502,23 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
|
||||
if (PathPointRenderPlugin.Instance != null)
|
||||
{
|
||||
// 清除临时的起点和终点标记
|
||||
PathPointRenderPlugin.Instance.RemovePath("AutoPathStartPoint");
|
||||
PathPointRenderPlugin.Instance.RemovePath("AutoPathEndPoint");
|
||||
LogManager.WriteLog("[临时标记清理] 已清除AutoPathStartPoint和AutoPathEndPoint临时标记");
|
||||
// 清除临时的起点标记(使用正确的路径ID)
|
||||
if (_autoPathStartPointRoute != null)
|
||||
{
|
||||
PathPointRenderPlugin.Instance.RemovePath(_autoPathStartPointRoute.Id);
|
||||
LogManager.WriteLog($"[临时标记清理] 已清除起点标记,ID: {_autoPathStartPointRoute.Id}");
|
||||
_autoPathStartPointRoute = null;
|
||||
}
|
||||
|
||||
// 清除临时的终点标记(使用正确的路径ID)
|
||||
if (_autoPathEndPointRoute != null)
|
||||
{
|
||||
PathPointRenderPlugin.Instance.RemovePath(_autoPathEndPointRoute.Id);
|
||||
LogManager.WriteLog($"[临时标记清理] 已清除终点标记,ID: {_autoPathEndPointRoute.Id}");
|
||||
_autoPathEndPointRoute = null;
|
||||
}
|
||||
|
||||
LogManager.WriteLog("[临时标记清理] 自动路径临时标记清理完成");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@ -318,9 +318,55 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
{
|
||||
SafeExecute(() =>
|
||||
{
|
||||
// TODO: 实现日志查看功能
|
||||
LogStatus = "打开日志查看器";
|
||||
LogManager.Info("查看日志");
|
||||
try
|
||||
{
|
||||
// 创建并显示日志查看器对话框
|
||||
var logViewerDialog = new NavisworksTransport.UI.WPF.Views.LogViewerDialog();
|
||||
|
||||
// 尝试设置窗口所有者,使用更安全的方式
|
||||
try
|
||||
{
|
||||
// 查找当前活动的主窗口
|
||||
var mainWindow = System.Windows.Application.Current.MainWindow;
|
||||
if (mainWindow != null && mainWindow.IsLoaded)
|
||||
{
|
||||
logViewerDialog.Owner = mainWindow;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果主窗口不可用,查找当前活动的窗口
|
||||
foreach (System.Windows.Window window in System.Windows.Application.Current.Windows)
|
||||
{
|
||||
if (window.IsActive && window.IsLoaded)
|
||||
{
|
||||
logViewerDialog.Owner = window;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ownerEx)
|
||||
{
|
||||
// 如果设置Owner失败,记录警告但继续显示窗口
|
||||
LogManager.Warning($"设置日志查看器Owner失败: {ownerEx.Message}");
|
||||
}
|
||||
|
||||
// 显示对话框(使用Show而不是ShowDialog以避免阻塞)
|
||||
logViewerDialog.Show();
|
||||
|
||||
LogStatus = "日志查看器已打开";
|
||||
LogManager.Info("通过系统管理打开日志查看器");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogStatus = "打开日志查看器失败";
|
||||
LogManager.Error($"打开日志查看器失败: {ex.Message}", ex);
|
||||
|
||||
// 显示错误消息给用户
|
||||
System.Windows.MessageBox.Show($"打开日志查看器失败: {ex.Message}", "错误",
|
||||
System.Windows.MessageBoxButton.OK,
|
||||
System.Windows.MessageBoxImage.Error);
|
||||
}
|
||||
}, "查看日志");
|
||||
}
|
||||
|
||||
@ -331,9 +377,44 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
{
|
||||
SafeExecute(() =>
|
||||
{
|
||||
// TODO: 实现日志清空功能
|
||||
LogStatus = "日志已清空";
|
||||
LogManager.Info("清空日志");
|
||||
try
|
||||
{
|
||||
// 弹出确认对话框
|
||||
var result = System.Windows.MessageBox.Show(
|
||||
"确定要清空日志吗?此操作不可撤销。",
|
||||
"确认清空日志",
|
||||
System.Windows.MessageBoxButton.YesNo,
|
||||
System.Windows.MessageBoxImage.Question);
|
||||
|
||||
if (result == System.Windows.MessageBoxResult.Yes)
|
||||
{
|
||||
// 调用LogManager清空日志
|
||||
LogManager.ClearLog();
|
||||
|
||||
LogStatus = "日志已清空";
|
||||
LogManager.Info("通过系统管理清空日志");
|
||||
|
||||
// 显示成功消息
|
||||
System.Windows.MessageBox.Show("日志已成功清空。", "操作完成",
|
||||
System.Windows.MessageBoxButton.OK,
|
||||
System.Windows.MessageBoxImage.Information);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogStatus = "取消清空日志";
|
||||
LogManager.Info("用户取消清空日志操作");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogStatus = "清空日志失败";
|
||||
LogManager.Error($"清空日志失败: {ex.Message}", ex);
|
||||
|
||||
// 显示错误消息给用户
|
||||
System.Windows.MessageBox.Show($"清空日志失败: {ex.Message}", "错误",
|
||||
System.Windows.MessageBoxButton.OK,
|
||||
System.Windows.MessageBoxImage.Error);
|
||||
}
|
||||
}, "清空日志");
|
||||
}
|
||||
|
||||
@ -344,9 +425,71 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
{
|
||||
SafeExecute(() =>
|
||||
{
|
||||
// TODO: 实现日志导出功能
|
||||
LogStatus = "日志导出完成";
|
||||
LogManager.Info("导出日志");
|
||||
try
|
||||
{
|
||||
// 创建保存文件对话框
|
||||
var saveFileDialog = new Microsoft.Win32.SaveFileDialog
|
||||
{
|
||||
Title = "导出日志文件",
|
||||
Filter = "日志文件 (*.log)|*.log|文本文件 (*.txt)|*.txt|所有文件 (*.*)|*.*",
|
||||
DefaultExt = "log",
|
||||
FileName = $"NavisworksTransport_Log_{DateTime.Now:yyyyMMdd_HHmmss}.log"
|
||||
};
|
||||
|
||||
if (saveFileDialog.ShowDialog() == true)
|
||||
{
|
||||
var logFilePath = LogManager.LogFilePath;
|
||||
|
||||
if (System.IO.File.Exists(logFilePath))
|
||||
{
|
||||
// 从源日志文件复制到目标位置
|
||||
System.IO.File.Copy(logFilePath, saveFileDialog.FileName, true);
|
||||
|
||||
LogStatus = $"日志已导出到: {System.IO.Path.GetFileName(saveFileDialog.FileName)}";
|
||||
LogManager.Info($"日志已导出到: {saveFileDialog.FileName}");
|
||||
|
||||
// 显示成功消息并询问是否打开文件夹
|
||||
var result = System.Windows.MessageBox.Show(
|
||||
$"日志已成功导出到:\n{saveFileDialog.FileName}\n\n是否打开文件所在文件夹?",
|
||||
"导出完成",
|
||||
System.Windows.MessageBoxButton.YesNo,
|
||||
System.Windows.MessageBoxImage.Information);
|
||||
|
||||
if (result == System.Windows.MessageBoxResult.Yes)
|
||||
{
|
||||
// 打开文件所在文件夹并选中文件
|
||||
System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{saveFileDialog.FileName}\"");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果日志文件不存在,创建一个空的日志文件
|
||||
System.IO.File.WriteAllText(saveFileDialog.FileName, $"NavisworksTransport 日志文件\n导出时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}\n\n日志文件为空或不存在。\n");
|
||||
|
||||
LogStatus = "日志文件不存在,已创建空文件";
|
||||
LogManager.Warning("日志文件不存在,已创建空的导出文件");
|
||||
|
||||
System.Windows.MessageBox.Show("原始日志文件不存在,已创建空的导出文件。", "注意",
|
||||
System.Windows.MessageBoxButton.OK,
|
||||
System.Windows.MessageBoxImage.Warning);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LogStatus = "取消导出日志";
|
||||
LogManager.Info("用户取消导出日志操作");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogStatus = "导出日志失败";
|
||||
LogManager.Error($"导出日志失败: {ex.Message}", ex);
|
||||
|
||||
// 显示错误消息给用户
|
||||
System.Windows.MessageBox.Show($"导出日志失败: {ex.Message}", "错误",
|
||||
System.Windows.MessageBoxButton.OK,
|
||||
System.Windows.MessageBoxImage.Error);
|
||||
}
|
||||
}, "导出日志");
|
||||
}
|
||||
|
||||
|
||||
@ -45,8 +45,8 @@ NavisworksTransport 关于对话框 - 采用与主界面一致的Navisworks 2026
|
||||
<StackPanel Margin="20">
|
||||
|
||||
<!-- 插件标题和版本 -->
|
||||
<Label Content="NavisworksTransport" Style="{StaticResource TitleStyle}"/>
|
||||
<Label Content="物流路径规划插件 v1.0" Style="{StaticResource VersionStyle}"/>
|
||||
<Label Content="NavisworksTransport" Style="{StaticResource DialogTitleStyle}"/>
|
||||
<Label Content="物流路径规划插件 v1.0" Style="{StaticResource DialogVersionStyle}"/>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<Border Height="1"
|
||||
@ -67,28 +67,28 @@ NavisworksTransport 关于对话框 - 采用与主界面一致的Navisworks 2026
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Label Grid.Row="0" Grid.Column="0" Content="适用版本:" Style="{StaticResource InfoLabelStyle}"/>
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" Text="Autodesk Navisworks Manage 2026" Style="{StaticResource InfoTextStyle}"/>
|
||||
<Label Grid.Row="0" Grid.Column="0" Content="适用版本:" Style="{StaticResource DialogInfoLabelStyle}"/>
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" Text="Autodesk Navisworks Manage 2026" Style="{StaticResource DialogInfoTextStyle}"/>
|
||||
|
||||
<Label Grid.Row="1" Grid.Column="0" Content="版本号:" Style="{StaticResource InfoLabelStyle}"/>
|
||||
<TextBlock Grid.Row="1" Grid.Column="1" Text="1.0.0" Style="{StaticResource InfoTextStyle}"/>
|
||||
<Label Grid.Row="1" Grid.Column="0" Content="版本号:" Style="{StaticResource DialogInfoLabelStyle}"/>
|
||||
<TextBlock Grid.Row="1" Grid.Column="1" Text="1.0.0" Style="{StaticResource DialogInfoTextStyle}"/>
|
||||
|
||||
<Label Grid.Row="2" Grid.Column="0" Content="发布日期:" Style="{StaticResource InfoLabelStyle}"/>
|
||||
<TextBlock Grid.Row="2" Grid.Column="1" Text="2024年8月" Style="{StaticResource InfoTextStyle}"/>
|
||||
<Label Grid.Row="2" Grid.Column="0" Content="发布日期:" Style="{StaticResource DialogInfoLabelStyle}"/>
|
||||
<TextBlock Grid.Row="2" Grid.Column="1" Text="2024年8月" Style="{StaticResource DialogInfoTextStyle}"/>
|
||||
|
||||
<Label Grid.Row="3" Grid.Column="0" Content="开发语言:" Style="{StaticResource InfoLabelStyle}"/>
|
||||
<TextBlock Grid.Row="3" Grid.Column="1" Text="C# .NET Framework 4.8" Style="{StaticResource InfoTextStyle}"/>
|
||||
<Label Grid.Row="3" Grid.Column="0" Content="开发语言:" Style="{StaticResource DialogInfoLabelStyle}"/>
|
||||
<TextBlock Grid.Row="3" Grid.Column="1" Text="C# .NET Framework 4.8" Style="{StaticResource DialogInfoTextStyle}"/>
|
||||
|
||||
<Label Grid.Row="4" Grid.Column="0" Content="UI框架:" Style="{StaticResource InfoLabelStyle}"/>
|
||||
<TextBlock Grid.Row="4" Grid.Column="1" Text="WPF + MVVM" Style="{StaticResource InfoTextStyle}"/>
|
||||
<Label Grid.Row="4" Grid.Column="0" Content="UI框架:" Style="{StaticResource DialogInfoLabelStyle}"/>
|
||||
<TextBlock Grid.Row="4" Grid.Column="1" Text="WPF + MVVM" Style="{StaticResource DialogInfoTextStyle}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 功能描述 -->
|
||||
<TextBlock Text="专为Navisworks 2026设计的物流路径规划插件。支持3D路径规划、A*寻路算法、动画生成、碰撞检测等功能。通过直观的用户界面,帮助用户在建筑信息模型中进行高效的物流运输规划和冲突检测。"
|
||||
Style="{StaticResource DescriptionStyle}"/>
|
||||
Style="{StaticResource DialogDescriptionStyle}"/>
|
||||
|
||||
<!-- 主要特性 -->
|
||||
<TextBlock Style="{StaticResource DescriptionStyle}" Margin="20,10">
|
||||
<TextBlock Style="{StaticResource DialogDescriptionStyle}" Margin="20,10">
|
||||
<Run Text="主要特性:" FontWeight="Medium" Foreground="#FF2B579A"/>
|
||||
<LineBreak/>
|
||||
<Run Text="• 智能路径规划与A*算法优化"/>
|
||||
|
||||
@ -294,7 +294,7 @@ NavisworksTransport 检测动画页签视图 - 采用与类别设置和分层管
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Label Grid.Column="0" Content="播放进度:" Width="50" VerticalAlignment="Center"/>
|
||||
<Label Grid.Column="0" Content="播放进度:" Width="80" VerticalAlignment="Center"/>
|
||||
<ProgressBar Grid.Column="1"
|
||||
Value="{Binding AnimationProgress}"
|
||||
Style="{StaticResource ProgressBarStyle}"
|
||||
|
||||
170
src/UI/WPF/Views/LogViewerDialog.xaml
Normal file
170
src/UI/WPF/Views/LogViewerDialog.xaml
Normal file
@ -0,0 +1,170 @@
|
||||
<!--
|
||||
NavisworksTransport 日志查看器对话框 - 采用与主界面一致的Navisworks 2026风格
|
||||
|
||||
功能说明:
|
||||
1. 实时显示和刷新日志内容
|
||||
2. 支持搜索、过滤和导出功能
|
||||
3. 提供清空日志和自动滚动选项
|
||||
4. 与主界面保持一致的视觉风格
|
||||
|
||||
设计原则:使用统一的蓝色主题和样式规范,针对大量文本显示优化
|
||||
-->
|
||||
<Window x:Class="NavisworksTransport.UI.WPF.Views.LogViewerDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
mc:Ignorable="d"
|
||||
Title="日志查看器"
|
||||
Height="600"
|
||||
Width="900"
|
||||
ResizeMode="CanResize"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
MinHeight="400"
|
||||
MinWidth="600">
|
||||
|
||||
<Window.Resources>
|
||||
<!-- 引用共享的Navisworks 2026样式资源 -->
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<!-- 日志查看器特有的样式 -->
|
||||
<Style x:Key="LogTextBoxStyle" TargetType="TextBox">
|
||||
<Setter Property="FontFamily" Value="Consolas, Courier New"/>
|
||||
<Setter Property="FontSize" Value="11"/>
|
||||
<Setter Property="Background" Value="#FFFEFEFE"/>
|
||||
<Setter Property="Foreground" Value="#FF333333"/>
|
||||
<Setter Property="BorderBrush" Value="#FFD4E7FF"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="IsReadOnly" Value="True"/>
|
||||
<Setter Property="VerticalScrollBarVisibility" Value="Auto"/>
|
||||
<Setter Property="HorizontalScrollBarVisibility" Value="Auto"/>
|
||||
<Setter Property="TextWrapping" Value="NoWrap"/>
|
||||
<Setter Property="AcceptsReturn" Value="True"/>
|
||||
<Setter Property="AcceptsTab" Value="True"/>
|
||||
<Setter Property="Padding" Value="8"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="SearchTextBoxStyle" TargetType="TextBox">
|
||||
<Setter Property="FontSize" Value="11"/>
|
||||
<Setter Property="Padding" Value="5,3"/>
|
||||
<Setter Property="BorderBrush" Value="#FFD4E7FF"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 工具栏区域 -->
|
||||
<Border Grid.Row="0"
|
||||
BorderBrush="#FFD4E7FF"
|
||||
BorderThickness="0,0,0,1"
|
||||
Background="#FFF8FBFF"
|
||||
Padding="15,10">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 左侧工具按钮 -->
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal">
|
||||
<Button Content="刷新"
|
||||
Click="RefreshButton_Click"
|
||||
Style="{StaticResource ActionButtonStyle}"
|
||||
Margin="0,0,10,0"/>
|
||||
<Button Content="清空日志"
|
||||
Click="ClearLogButton_Click"
|
||||
Style="{StaticResource SecondaryButtonStyle}"
|
||||
Margin="0,0,10,0"/>
|
||||
<Button Content="导出日志"
|
||||
Click="ExportLogButton_Click"
|
||||
Style="{StaticResource SecondaryButtonStyle}"
|
||||
Margin="0,0,15,0"/>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<Border Width="1"
|
||||
Background="#FFD4E7FF"
|
||||
Margin="0,2"/>
|
||||
|
||||
<!-- 自动滚动选项 -->
|
||||
<CheckBox Content="自动滚动到底部"
|
||||
Name="AutoScrollCheckBox"
|
||||
IsChecked="True"
|
||||
VerticalAlignment="Center"
|
||||
Margin="15,0,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 右侧搜索区域 -->
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
||||
<Label Content="搜索:"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ParameterLabelStyle}"/>
|
||||
<TextBox Name="SearchTextBox"
|
||||
Width="200"
|
||||
Style="{StaticResource SearchTextBoxStyle}"
|
||||
TextChanged="SearchTextBox_TextChanged"
|
||||
Margin="5,0,10,0"/>
|
||||
<Button Content="清除"
|
||||
Click="ClearSearchButton_Click"
|
||||
Style="{StaticResource SecondaryButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 日志内容显示区域 -->
|
||||
<Border Grid.Row="1"
|
||||
BorderBrush="#FFD4E7FF"
|
||||
BorderThickness="1,0,1,0"
|
||||
Background="White"
|
||||
Margin="15,0">
|
||||
<TextBox Name="LogContentTextBox"
|
||||
Style="{StaticResource LogTextBoxStyle}"/>
|
||||
</Border>
|
||||
|
||||
<!-- 底部状态栏和按钮 -->
|
||||
<Border Grid.Row="2"
|
||||
BorderBrush="#FFD4E7FF"
|
||||
BorderThickness="0,1,0,0"
|
||||
Background="#FFF8FBFF"
|
||||
Padding="15,10">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 状态信息 -->
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal">
|
||||
<Label Name="StatusLabel"
|
||||
Content="日志已加载"
|
||||
Style="{StaticResource ParameterLabelStyle}"
|
||||
Foreground="#FF2B8A2B"/>
|
||||
<Label Name="LogSizeLabel"
|
||||
Content="大小: 0 KB"
|
||||
Style="{StaticResource ParameterLabelStyle}"
|
||||
Margin="20,0,0,0"/>
|
||||
<Label Name="LineCountLabel"
|
||||
Content="行数: 0"
|
||||
Style="{StaticResource ParameterLabelStyle}"
|
||||
Margin="20,0,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 右侧按钮 -->
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
||||
<Button Content="关闭"
|
||||
Click="CloseButton_Click"
|
||||
Style="{StaticResource ActionButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
337
src/UI/WPF/Views/LogViewerDialog.xaml.cs
Normal file
337
src/UI/WPF/Views/LogViewerDialog.xaml.cs
Normal file
@ -0,0 +1,337 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Threading;
|
||||
using Microsoft.Win32;
|
||||
using NavisworksTransport.Utils;
|
||||
|
||||
namespace NavisworksTransport.UI.WPF.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// 日志查看器对话框
|
||||
/// </summary>
|
||||
public partial class LogViewerDialog : Window
|
||||
{
|
||||
#region 私有字段
|
||||
|
||||
private readonly DispatcherTimer _refreshTimer;
|
||||
private long _lastFileSize = 0;
|
||||
private string _originalLogContent = string.Empty;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 构造函数
|
||||
|
||||
public LogViewerDialog()
|
||||
{
|
||||
try
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// 初始化刷新定时器
|
||||
_refreshTimer = new DispatcherTimer();
|
||||
_refreshTimer.Interval = TimeSpan.FromSeconds(2);
|
||||
_refreshTimer.Tick += RefreshTimer_Tick;
|
||||
|
||||
// 加载初始日志内容
|
||||
LoadLogContent();
|
||||
|
||||
// 启动自动刷新
|
||||
_refreshTimer.Start();
|
||||
|
||||
LogManager.Info("日志查看器对话框已打开");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Error($"日志查看器初始化失败: {ex.Message}", ex);
|
||||
MessageBox.Show($"日志查看器初始化失败: {ex.Message}", "错误",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 事件处理
|
||||
|
||||
/// <summary>
|
||||
/// 刷新按钮点击事件
|
||||
/// </summary>
|
||||
private void RefreshButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
LoadLogContent();
|
||||
StatusLabel.Content = "日志已刷新";
|
||||
LogManager.Info("手动刷新日志");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Error($"手动刷新日志失败: {ex.Message}", ex);
|
||||
StatusLabel.Content = "刷新失败";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空日志按钮点击事件
|
||||
/// </summary>
|
||||
private void ClearLogButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = MessageBox.Show("确定要清空日志吗?此操作不可撤销。", "确认清空",
|
||||
MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||
|
||||
if (result == MessageBoxResult.Yes)
|
||||
{
|
||||
LogManager.ClearLog();
|
||||
LoadLogContent();
|
||||
StatusLabel.Content = "日志已清空";
|
||||
LogManager.Info("通过日志查看器清空日志");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Error($"清空日志失败: {ex.Message}", ex);
|
||||
StatusLabel.Content = "清空失败";
|
||||
MessageBox.Show($"清空日志失败: {ex.Message}", "错误",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出日志按钮点击事件
|
||||
/// </summary>
|
||||
private void ExportLogButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var saveFileDialog = new SaveFileDialog
|
||||
{
|
||||
Title = "导出日志文件",
|
||||
Filter = "日志文件 (*.log)|*.log|文本文件 (*.txt)|*.txt|所有文件 (*.*)|*.*",
|
||||
DefaultExt = "log",
|
||||
FileName = $"NavisworksTransport_Log_{DateTime.Now:yyyyMMdd_HHmmss}.log"
|
||||
};
|
||||
|
||||
if (saveFileDialog.ShowDialog() == true)
|
||||
{
|
||||
// 从日志文件复制到目标位置
|
||||
var logFilePath = LogManager.LogFilePath;
|
||||
if (File.Exists(logFilePath))
|
||||
{
|
||||
File.Copy(logFilePath, saveFileDialog.FileName, true);
|
||||
StatusLabel.Content = $"日志已导出到: {Path.GetFileName(saveFileDialog.FileName)}";
|
||||
LogManager.Info($"日志已导出到: {saveFileDialog.FileName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
File.WriteAllText(saveFileDialog.FileName, LogContentTextBox.Text);
|
||||
StatusLabel.Content = $"日志已导出到: {Path.GetFileName(saveFileDialog.FileName)}";
|
||||
LogManager.Info($"日志内容已导出到: {saveFileDialog.FileName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Error($"导出日志失败: {ex.Message}", ex);
|
||||
StatusLabel.Content = "导出失败";
|
||||
MessageBox.Show($"导出日志失败: {ex.Message}", "错误",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 搜索文本框内容变化事件
|
||||
/// </summary>
|
||||
private void SearchTextBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
PerformSearch();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Error($"搜索功能异常: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除搜索按钮点击事件
|
||||
/// </summary>
|
||||
private void ClearSearchButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
SearchTextBox.Text = string.Empty;
|
||||
LogContentTextBox.Text = _originalLogContent;
|
||||
StatusLabel.Content = "搜索已清除";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Error($"清除搜索失败: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关闭按钮点击事件
|
||||
/// </summary>
|
||||
private void CloseButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口关闭事件
|
||||
/// </summary>
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
_refreshTimer?.Stop();
|
||||
LogManager.Info("日志查看器对话框已关闭");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Error($"关闭日志查看器时发生错误: {ex.Message}", ex);
|
||||
}
|
||||
|
||||
base.OnClosed(e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 自动刷新定时器事件
|
||||
/// </summary>
|
||||
private void RefreshTimer_Tick(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查日志文件是否有变化
|
||||
var logFilePath = LogManager.LogFilePath;
|
||||
if (File.Exists(logFilePath))
|
||||
{
|
||||
var fileInfo = new FileInfo(logFilePath);
|
||||
if (fileInfo.Length != _lastFileSize)
|
||||
{
|
||||
LoadLogContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Error($"自动刷新日志时发生错误: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 私有方法
|
||||
|
||||
/// <summary>
|
||||
/// 加载日志内容
|
||||
/// </summary>
|
||||
private void LoadLogContent()
|
||||
{
|
||||
try
|
||||
{
|
||||
var logFilePath = LogManager.LogFilePath;
|
||||
|
||||
if (File.Exists(logFilePath))
|
||||
{
|
||||
// 读取日志文件内容
|
||||
_originalLogContent = File.ReadAllText(logFilePath);
|
||||
LogContentTextBox.Text = _originalLogContent;
|
||||
|
||||
// 更新文件信息
|
||||
var fileInfo = new FileInfo(logFilePath);
|
||||
_lastFileSize = fileInfo.Length;
|
||||
|
||||
// 更新状态信息
|
||||
LogSizeLabel.Content = $"大小: {FormatFileSize(fileInfo.Length)}";
|
||||
LineCountLabel.Content = $"行数: {LogContentTextBox.LineCount}";
|
||||
|
||||
// 自动滚动到底部
|
||||
if (AutoScrollCheckBox.IsChecked == true)
|
||||
{
|
||||
LogContentTextBox.ScrollToEnd();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_originalLogContent = "日志文件不存在或为空";
|
||||
LogContentTextBox.Text = _originalLogContent;
|
||||
_lastFileSize = 0;
|
||||
LogSizeLabel.Content = "大小: 0 KB";
|
||||
LineCountLabel.Content = "行数: 0";
|
||||
}
|
||||
|
||||
// 如果有搜索文本,重新执行搜索
|
||||
if (!string.IsNullOrEmpty(SearchTextBox.Text))
|
||||
{
|
||||
PerformSearch();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Error($"加载日志内容失败: {ex.Message}", ex);
|
||||
LogContentTextBox.Text = $"加载日志失败: {ex.Message}";
|
||||
StatusLabel.Content = "加载失败";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行搜索
|
||||
/// </summary>
|
||||
private void PerformSearch()
|
||||
{
|
||||
try
|
||||
{
|
||||
var searchText = SearchTextBox.Text?.Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(searchText))
|
||||
{
|
||||
LogContentTextBox.Text = _originalLogContent;
|
||||
StatusLabel.Content = "日志已加载";
|
||||
return;
|
||||
}
|
||||
|
||||
// 简单的文本搜索 - 显示包含搜索词的行
|
||||
var lines = _originalLogContent.Split('\n');
|
||||
var filteredLines = new System.Collections.Generic.List<string>();
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (line.IndexOf(searchText, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
filteredLines.Add(line);
|
||||
}
|
||||
}
|
||||
|
||||
LogContentTextBox.Text = string.Join("\n", filteredLines);
|
||||
StatusLabel.Content = $"找到 {filteredLines.Count} 行匹配结果";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Error($"搜索执行失败: {ex.Message}", ex);
|
||||
StatusLabel.Content = "搜索失败";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 格式化文件大小
|
||||
/// </summary>
|
||||
private string FormatFileSize(long bytes)
|
||||
{
|
||||
if (bytes < 1024)
|
||||
return $"{bytes} B";
|
||||
else if (bytes < 1024 * 1024)
|
||||
return $"{bytes / 1024.0:F1} KB";
|
||||
else if (bytes < 1024 * 1024 * 1024)
|
||||
return $"{bytes / (1024.0 * 1024.0):F1} MB";
|
||||
else
|
||||
return $"{bytes / (1024.0 * 1024.0 * 1024.0):F1} GB";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user