Compare commits

...

598 Commits

Author SHA1 Message Date
0a318f4dca Update completed todo items 2026-04-26 19:13:40 +08:00
454f1e6584 Fix auto path grid restore and validation 2026-04-26 19:11:03 +08:00
28d0708858 Lock selection clip box after activation 2026-04-15 16:02:12 +08:00
1a9d0972f0 Fix auto path obstacle geometry collection 2026-04-14 09:22:01 +08:00
fdb857af17 Refactor auto path planning coordinate semantics 2026-04-13 10:31:14 +08:00
6f037a4571 Reduce pose diagnostics log noise 2026-04-13 02:51:24 +08:00
b849bd9aed Add Navisworks automation integration tests 2026-04-13 02:33:29 +08:00
61e6b79299 Remove legacy yaw recovery fallbacks 2026-04-12 15:29:08 +08:00
315fa46043 Unify planar collision pose replay 2026-04-12 15:01:33 +08:00
970d3465f4 Add a custom ribbon tab for logistics tools 2026-04-11 23:17:21 +08:00
7e1931fd37 Delay database creation until plugin data is needed 2026-04-11 22:29:40 +08:00
bbd72345c9 Add selection-based clip box controls 2026-04-11 22:10:21 +08:00
18b530ef5f Unify WPF numeric textbox editing behavior 2026-04-11 09:04:03 +08:00
a252dad46a remove pose fallbacks and fix script compatibility 2026-04-10 22:23:46 +08:00
5a03c3aa6c preserve planar collision pose rotations 2026-04-10 22:02:43 +08:00
1a9c9ecd21 fix collision report save timing 2026-04-10 00:16:55 +08:00
da5f8a1ae1 add ground object lift adjustment 2026-04-09 23:53:40 +08:00
6020bfa979 fix hoisting descend point reuse 2026-04-09 23:28:12 +08:00
144ee4e6b2 refine hoisting path fixes 2026-04-09 23:09:56 +08:00
4fbab52880 clean legacy planar pose branches 2026-04-09 23:08:59 +08:00
3892ddabab Stabilize ground incremental rotation and extent tests 2026-04-09 21:09:43 +08:00
405f721811 Ground real-object incremental path updates 2026-04-08 23:06:42 +08:00
042f30bf87 地面路径重构失败,留下一些教训 2026-04-06 22:39:02 +08:00
8a7563a4fe Clarify ground tracked-point semantics 2026-04-06 08:55:53 +08:00
348b3a75bf 修复地面倾斜路径偏移的问题 2026-04-05 14:03:41 +08:00
fb9dac9953 恢复地面路径为3月25号版本 2026-04-05 01:36:24 +08:00
e21e934705 Add rail preferred normal repair script 2026-04-04 10:58:29 +08:00
7058e5fd23 Add hoisting layer height editing 2026-04-02 22:37:16 +08:00
9be5d250e8 Restore tool focus by picking context 2026-04-02 22:35:58 +08:00
4ec4cf77ee Refine path viewpoint behavior and config editing 2026-04-02 22:14:26 +08:00
24951d4205 Stabilize hoisting start-pose logging and caching 2026-04-02 01:32:31 +08:00
9e8fea3241 Unify real-object override rotation application 2026-04-02 00:19:36 +08:00
7ff510daf0 Use actual hoisting pose without fragment fallback 2026-04-02 00:15:58 +08:00
953306fdb1 Stabilize hoisting pose flow and bounds inspection 2026-04-01 23:53:11 +08:00
e71ea81cdf Fix rail assembly normals and tighten rail camera 2026-04-01 20:12:57 +08:00
2e0b26e032 Refine rail assembly workflow and viewpoint strategies 2026-04-01 10:47:34 +08:00
c3b103c159 Add explicit exit for rail assembly workflow 2026-03-31 23:38:47 +08:00
c9744699b1 Separate rail creation and edit assembly flows 2026-03-31 13:41:15 +08:00
d4c49fc227 Stabilize hoisting pose adjustment flow 2026-03-31 00:14:18 +08:00
cb56737041 Preserve terminal pose when translating rail objects to path start 2026-03-30 23:35:01 +08:00
1d71d36e5f 修复rail路径的编辑状态问题 2026-03-30 22:13:16 +08:00
440c41d1d9 Merge branch 'codex/back-to-766b5af' into codex/rail-mount-modes 2026-03-29 23:42:38 +08:00
9093374399 Use actual hoisting pose baseline for real objects 2026-03-29 23:36:43 +08:00
766b5af887 Stop auto-switching to path view before animation playback 2026-03-29 18:28:41 +08:00
382923fa32 Relax fragment pose parsing and add fallback 2026-03-29 01:17:06 +08:00
6214cd4397 Improve clipboard copy utilities and dialogs 2026-03-28 22:55:45 +08:00
f4735b164e Refine rail real-object pose handling 2026-03-28 22:22:18 +08:00
7851e6affa Map real-object angle adjustments from host axes 2026-03-28 22:21:11 +08:00
115d70db66 Add rail start point repositioning 2026-03-28 14:45:24 +08:00
e4788c17a3 Improve rail path installation point editing 2026-03-28 13:54:48 +08:00
95500c9717 修改部分日志级别为Debug 2026-03-28 12:42:54 +08:00
7db5a7a201 Add path duplication workflow 2026-03-28 12:36:24 +08:00
a5d1db6416 Refine collision analysis dialog and rail assembly markers 2026-03-26 22:49:45 +08:00
9f1924a797 Add real object pose source migration draft 2026-03-26 13:46:51 +08:00
ad8d86e79f Restore assembly pick focus with space 2026-03-26 13:45:48 +08:00
2b6f54898a Fix rail assembly anchor alignment 2026-03-26 13:37:33 +08:00
3a5693a453 Add rail mount placement controls and persistence 2026-03-26 01:01:01 +08:00
921dc07856 Fix ground real-object forward axis and compensation 2026-03-25 17:19:21 +08:00
aabf263f0d Fix host rotation adapter split for real and virtual objects 2026-03-25 11:18:03 +08:00
029c7e37ad Checkpoint current pose and path state 2026-03-25 01:41:15 +08:00
138eb43a67 Unify real-object rail pose interpretation 2026-03-25 00:11:07 +08:00
6c74ea1319 Fix host-axis rotation correction for real objects 2026-03-24 21:59:53 +08:00
290df34dec Interpret fragment up for real object pose 2026-03-24 21:04:24 +08:00
8d10f959b2 Add fragment up detection and real object pose probes 2026-03-24 19:06:50 +08:00
48de5aa921 Add geometry transform skill metadata 2026-03-24 15:40:27 +08:00
eaf24420b7 修正真实物体的旋转和通行空间不适配的问题 2026-03-23 22:46:38 +08:00
d52b1aef08 Checkpoint animation rotation refactor state 2026-03-23 21:00:02 +08:00
1cf4fe5967 Align terminal rail normals and object space rendering 2026-03-23 12:25:00 +08:00
e9128282b5 Fix rail rotation correction semantics 2026-03-23 00:11:49 +08:00
93ad55c725 Refine assembly installation workflow UI 2026-03-22 23:58:11 +08:00
70aaff10bf Add assembly end-face guided optical axis 2026-03-22 19:45:09 +08:00
b1d4170334 Fix animation end alignment and YUp viewpoint focus 2026-03-22 14:45:50 +08:00
1802eda971 Separate asset axis conventions from host coordinates 2026-03-22 13:30:35 +08:00
5e7f9bdf51 Finish assembly route generation cleanup 2026-03-22 13:06:08 +08:00
8f55b5e9a1 Fix YUp hoisting path rendering and finish sync 2026-03-22 12:21:25 +08:00
042b9a2804 Refine canonical pose flow and handoff notes 2026-03-22 00:00:16 +08:00
7ccce8cf5a Stabilize rail collision pose recovery and canonical tracking 2026-03-21 21:58:11 +08:00
c53db7a6fd Finalize canonical rail coordinate integration 2026-03-21 19:19:13 +08:00
3fd184934b Establish canonical coordinate framework with tests 2026-03-21 06:10:34 +08:00
a651b69459 Fix batch queue detection record snapshots 2026-03-20 15:42:16 +08:00
c9a926356c 修改终端安装仿真UI,升级数据库版本号 2026-03-20 15:22:58 +08:00
015a2863ea Fix collision pose restore and deployment verification 2026-03-20 14:59:41 +08:00
5e8c50e043 Stabilize rail 3D animation transforms 2026-03-20 12:50:08 +08:00
5847ba9ab3 Fix rail assembly pose and animation flow 2026-03-20 11:13:43 +08:00
9adf9b5baf Fix assembly reference rod and start-point path creation 2026-03-19 23:12:49 +08:00
a55ef42f71 Add linear assembly reference path and rod alignment 2026-03-19 22:00:04 +08:00
0c83c73917 feat: add linear assembly rail path workflow 2026-03-19 00:18:24 +08:00
8b58f2853e 清理过期内容 2026-03-18 09:07:33 +08:00
f596935c76 优化剖面盒导,提高性能,解决漏算的问题 2026-03-12 09:40:14 +08:00
e495ea4620 重构剖面盒导出代码 2026-03-11 23:48:01 +08:00
8b4afcfe04 优化剖面盒导出性能 2026-03-11 23:17:58 +08:00
3b801c6bd4 给“导出剖面盒”增加导出nwd文件;重构了可见性的保存和恢复 2026-03-11 19:03:43 +08:00
0f669fd995 增加导出为Delmia兼容的xml格式 2026-03-09 17:47:39 +08:00
486caf1c38 增加路径文件操作按钮和日志过滤 2026-03-09 15:51:15 +08:00
07b30a4fb5 将自定义类别配置文件整合到系统配置文件中;修复配置事件重复触发问题。 2026-03-09 14:04:25 +08:00
dc29c3526e 修改坐标系探索为检测;去掉坐标系设置的介绍 2026-03-09 12:51:04 +08:00
0fbff5f114 给版本号增加Build Version;实现系统信息的实时获取 2026-03-09 12:41:44 +08:00
6e6932394a 修改InfoTextStyle样式,去掉重复定义 2026-03-09 11:39:23 +08:00
97296085f5 增加了验收设计说明文档 2026-02-27 19:41:12 +08:00
759a41fb7e 编写管理员手册 2026-02-26 00:44:11 +08:00
2b151ebee5 完成工具类skills 2026-02-25 02:01:38 +08:00
cb7e1c0e17 增加吊装路径水平环形检测 2026-02-25 01:37:15 +08:00
369605e12f 修复层间通行空间高度 2026-02-24 18:36:15 +08:00
dda04250b0 继续修复多层吊装路径 2026-02-24 18:09:08 +08:00
62548197b3 完善多层吊装路径 2026-02-24 17:25:15 +08:00
edcbd0802b 初步实现多层吊装路径 2026-02-24 03:57:33 +08:00
ae845bc571 检查并修复弹出窗口、对话框、消息框,未设置owner的情况 2026-02-23 00:25:17 +08:00
c910ab3ae7 修正切换路径剖面盒没变的问题 2026-02-21 16:25:25 +08:00
cc14e8c41d 移除过时的 animationCollisionCount 变量和相关输出 2026-02-21 11:49:45 +08:00
1a959ceb9d 修改空轨的日志级别; 2026-02-21 11:47:32 +08:00
27b4e546a2 程序关闭时静默返回 2026-02-21 11:40:17 +08:00
1047c82c66 纠正先选物体时没记录原始尺寸的问题;清除物体时将角度修正值归零。 2026-02-21 11:26:11 +08:00
11f09faa13 修复无参数时渲染不抛出异常的问题 2026-02-21 11:12:48 +08:00
65939f262c 修复没选择路径先选择物体,造成的路径可视化错误问题。修复临时路径保存到数据库的问题。 2026-02-21 10:28:13 +08:00
5c0e72ec98 修正时序问题导致 _route 为空,日志报错的问题 2026-02-20 23:16:12 +08:00
0262a413dd 避免对运行目录的硬编码 2026-02-20 22:33:00 +08:00
6e09961069 版本提高为0.15.0,更新项目文档 2026-02-20 22:26:57 +08:00
5ca793e093 修正检测配置重复,动画结束还检测的问题。自动打开重复的历史记录报告 2026-02-20 00:13:15 +08:00
3f0b42770d 实现了路径的剖面盒开关 2026-02-19 22:07:51 +08:00
507339d4f7 修正数据管理的内容显示,修正类别自定义错误日志 2026-02-19 20:47:57 +08:00
d336f8302a 对物流自定义文件进行简化,调整部分类别名字 2026-02-19 15:45:34 +08:00
69765e092b 修复自定义类别的障碍物检测问题 2026-02-19 14:52:28 +08:00
f9df83ba5b 实现自定义类别功能 2026-02-19 14:46:34 +08:00
76d277b6c2 从数据库中比对历史检测配置是否重复 2026-02-18 23:25:39 +08:00
7fbbcdd448 合并检测记录和碰撞记录表,重构检测过程 2026-02-18 22:34:18 +08:00
7da2ddf230 修正存储单位错误,规范单位使用 2026-02-18 16:10:36 +08:00
9751287884 纠正碰撞对不显示虚拟物体的问题 2026-02-18 15:19:58 +08:00
dc1380d7fe 增加检测记录表,完善批处理过程 2026-02-18 12:29:09 +08:00
749eb07cef 修复一些日志警告 2026-02-18 09:46:10 +08:00
eec957ad2e 重构虚拟物体管理器;修复碰撞历史列表自动移动物体的bug 2026-02-18 00:05:22 +08:00
1b26e7b640 修改截图使用当前视窗尺寸 2026-02-17 22:35:34 +08:00
ac6d3b4017 给碰撞构件清单,也加上碰撞对聚焦 2026-02-17 11:37:45 +08:00
fac4d3e2e3 去掉截图之间设置的等待时间 2026-02-17 10:44:09 +08:00
a304191b13 碰撞报告自动对碰撞点截图;纠正插件改名造成的路径错误。 2026-02-17 10:21:35 +08:00
08b4ecc5d5 将时标导出到TimeLiner任务 2026-02-16 19:56:07 +08:00
57751e85fa 实现完整的时间标签功能 2026-02-16 19:46:38 +08:00
178aa995ff 对代码进行审查并优化 2026-02-16 11:55:45 +08:00
2fb883ae18 将插件和输出名字中的navisworks去掉 2026-02-16 11:22:03 +08:00
fdcd8edfc0 将虚拟车辆改成虚拟物体 2026-02-16 09:02:50 +08:00
8378872b8d 增加按物流分类筛选显示物流元素 2026-02-15 21:26:49 +08:00
a9ccc057d3 修改Clashdetective进度条不展示百分比,以及不能取消的问题;进度条关闭主窗口失焦问题 2026-02-14 22:22:51 +08:00
3138b73a5f 优化路径分析功能;把资源文件集中到resources目录 2026-02-14 20:52:58 +08:00
77b9da40fc 实现路径分析功能 2026-02-14 17:39:40 +08:00
b36d20933e 修改预计算分析列表滚动条和数量限制 2026-02-13 19:33:52 +08:00
98c55045ad 选择物体或车辆时让它们到起点 2026-02-12 22:12:58 +08:00
c7781b15e4 修复动画结束,物体朝向转动的bug 2026-02-12 21:57:37 +08:00
579434fbff 增加导出剖面盒相交元素的包围盒信息的功能(用于三维场景重建) 2026-02-12 20:30:00 +08:00
64c6079011 调整碰撞分析对话框的尺寸为720x720,并设置数据表的最小高度为350 2026-02-11 03:41:15 +08:00
fdb11d2119 优化物体移动和朝向处理,确保在移动前重置到CAD原始位置,修复获取和保存当前朝向的逻辑 2026-02-11 03:18:06 +08:00
bdb3ed23db 纠正物体移动方法,确保先回到CAD原始位置再进行目标位置和朝向的变换 2026-02-11 02:30:52 +08:00
7ccb0ef6b0 修复动画重新播放物体朝向错误 2026-02-10 19:21:36 +08:00
ee33e43048 新增碰撞时运动物体位置和朝向记录功能,在碰撞报告中可以查看两者碰撞情况 2026-02-10 18:39:18 +08:00
cf5aed422b 优化视角聚焦功能,调整聚焦角度为60度,更新相关文档说明 2026-02-10 16:44:16 +08:00
1ffe567466 排除对象、碰撞对象、指定对象、物流类型等元素都增加视角对焦能力 2026-02-10 16:09:19 +08:00
c28f9762bc 重构视角控制辅助类,优化聚焦方法;记录相关API使用方法 2026-02-10 15:04:46 +08:00
78158c1cb1 新增指定对象聚焦功能 2026-02-10 13:49:43 +08:00
9a7a2da3c6 在碰撞报告中显示排除对象清单 2026-02-10 10:24:01 +08:00
e221d42812 新增排除对象管理功能,支持预计算分析添加到列表,支持用户手动添加和清除排除对象,支持数据库存储 2026-02-09 22:48:23 +08:00
322523bc77 新增预计算碰撞分析对话框及相关功能,支持高频碰撞物体的排除建议(初步实现) 2026-02-08 08:46:16 +08:00
77a49265aa 尽量不重建空间缓存;过滤虚拟车辆引起文档变更事件导致的初始化和缓存重建 2026-02-08 07:36:53 +08:00
5fdccf55c5 新增空间查询AABB范围查询功能,不再使用半径查询。 2026-02-08 06:55:07 +08:00
71fe79a447 修复html报告两种不同生成方法的问题,修复导出报告按钮无碰撞不显示的问题。 2026-02-07 18:30:29 +08:00
a655e8092c 建立简单的数据库备份和恢复机制 2026-02-07 01:56:24 +08:00
d9da8e95ce 给碰撞报告增加多截图的能力;修复路径宽度在修改路径点坐标后回到默认值的问题 2026-02-07 00:22:07 +08:00
84e5abf592 新增物体状态保存与恢复功能;在碰撞检测前后将物体恢复到终点位置 2026-02-06 21:49:57 +08:00
6be1de113e 优化代码格式,调整空行和注释位置;更新生成碰撞报告的方法为异步执行 2026-02-06 20:42:40 +08:00
a94d1c35b7 优化吊装路径管理逻辑,调整提升高度计算方式;更新路径编辑视图中列表高度设置 2026-02-06 03:14:02 +08:00
d9c362dcc3 新增可视化状态变更事件;将可视化控制按钮放到状态栏 2026-02-06 02:10:55 +08:00
333298c902 新增独立可视化开关以支持路径线和通行空间的显示;优化路径编辑视图中的可视化设置;新增布尔值到画刷和不透明度的转换器 2026-02-06 01:48:39 +08:00
a7e1411fd6 优化路径点编辑逻辑,改用Id查找路径点索引,增强索引验证;更新删除路径点时的选中状态处理 2026-02-06 01:06:36 +08:00
b7cdb72ff7 调整导出精度,将路径数据相关的数值精度从6位小数修改为3位小数 2026-02-06 00:36:30 +08:00
9e2ce0a3bc 优化复制粘贴坐标的按钮样式 2026-02-06 00:27:11 +08:00
8e3759adea 新增粘贴坐标功能和复制底面中心坐标按钮 2026-02-06 00:05:35 +08:00
39749dcf3d 新增 ModelItemBoundsWindow 窗口以显示选中模型元素的包围盒信息;调整碰撞检测报告窗口高度 2026-02-05 23:42:50 +08:00
c0333810ec 优化圆弧段渲染,使用完整车辆长方体替代多段长方体,提升可视化效果 2026-02-04 23:48:58 +08:00
1370ce1fd2 动画和碰撞检测后,保持物体在最终位置而非恢复到起点;去掉动画时对物体和虚拟车辆的绿色渲染 2026-02-04 22:11:03 +08:00
a0651d60cd 新增 Navisworks API 开发Skill 2026-02-04 21:50:23 +08:00
0d9eeed89b 优化单位转换的处理,使用缓存提高性能;控制米单位的使用 2026-02-04 12:59:02 +08:00
eeeaceacc4 修改吊装路径通行空间空中段方向的问题 2026-02-04 11:54:39 +08:00
5b6c8906aa 修改吊装路径的通行空间转折段方向错误的问题 2026-02-04 00:03:34 +08:00
1d170cb917 增加ClashDetective检测计数器,优化进度日志输出 2026-02-03 23:26:28 +08:00
8f0a363c9a 增加通行空间透明度配置,支持实时更新和渲染 2026-02-03 23:17:43 +08:00
dcf90a34fd 初始化时和文档就绪时刷新碰撞检测历史列表 2026-02-03 23:08:11 +08:00
0d241532c9 简化Clash Detective碰撞计数逻辑,统一使用权威碰撞计数器。解决批处理碰撞数错误的问题 2026-02-03 23:04:35 +08:00
f9f8b5f9aa 重置残留的Running状态任务为Pending,确保程序异常关闭后恢复状态 2026-02-03 22:56:39 +08:00
2930ea71da 增加通过PathId查找匹配原始对象的方法,优化ClashDetective碰撞结果处理 2026-02-03 22:40:33 +08:00
971c993bb7 纠正安全间隙多次转换的bug;更新运动和指定对象标识为PathId以确保唯一性 2026-02-03 21:57:55 +08:00
4d28ec5547 整合手工模式与全局模式的处理,解决批处理多余建缓存的bug;调整日志查看器窗口尺寸 2026-02-03 21:19:02 +08:00
a305ba31e1 重新截图信息更新至数据库,同时导出html 2026-02-02 22:25:26 +08:00
cb56910d68 默认打开手工指定碰撞检测对象,增加对象序号显示 2026-02-02 21:14:24 +08:00
0be608be82 增加配置兼容性检查,提示用户升级配置文件;优化路径规划管理器注释 2026-02-02 20:23:24 +08:00
438b000296 清理未使用的命名空间,优化代码结构 2026-02-01 19:25:48 +08:00
1d3f46dd00 增加配置变更事件订阅,实现系统配置实时生效 2026-02-01 19:00:31 +08:00
b1461436ff 优化碰撞检测缓存构建逻辑,直接设置移动物体以避免重复构建,简化手工模式下的预计算流程 2026-02-01 17:36:19 +08:00
dcbeabedf1 插件加载时的空文档检查,删除动画时对动画对象的检查 2026-02-01 16:49:32 +08:00
b09564c4aa 增加了修改提升点来调整上升高度的功能,重构了吊装路径的坐标约束 2026-02-01 16:29:18 +08:00
da89105269 优化吊装路径点处理,禁止删除关键点并在删除后自动优化路径 2026-01-31 20:12:44 +08:00
60bbd468d8 修改吊装路径功能,支持天车横移路径 2026-01-31 17:50:59 +08:00
7671360454 坐标系适配(未完成) 2026-01-31 12:13:36 +08:00
289fb2016b 增加坐标系管理功能,支持自动检测和手动选择ZUp/YUp坐标系,更新配置文件和UI界面 2026-01-31 10:02:02 +08:00
5fb18b5869 进行动态坐标系架构设计,增加自动检测模型坐标系探索按钮 2026-01-30 23:39:21 +08:00
bc39552ed2 优化碰撞结果显示名称,预获取运动物体名称并简化父容器查找 2026-01-30 13:11:23 +08:00
cdd6ee9319 优化ClashDetective集成,减少临时测试创建和删除的性能开销,简化碰撞结果的DisplayName设置 2026-01-30 12:41:15 +08:00
9fac32741e 清理渲染插件中的多余日志 2026-01-30 10:19:37 +08:00
dee5f1c834 优化碰撞高亮管理,使用类别管理方法替代直接高亮,调整预计算碰撞颜色为紫色以便区分 2026-01-29 21:44:14 +08:00
6498df1818 批处理不显示报告窗口,批处理执行结束显示汇总信息,修复碰撞为0时使用上个测试的碰撞数 2026-01-29 15:58:30 +08:00
c3cbaaf6e0 添加路径类型属性到批处理队列项 2026-01-29 14:56:10 +08:00
0ee85bba40 修正了空轨路径的垂直偏移逻辑,以确保通行空间顶面对齐轨道 2026-01-29 14:35:52 +08:00
3a79f0e50e 修正了路径导入导出的数据缺少路径类型和吊装高度的问题 2026-01-29 14:28:37 +08:00
c24bff692e 将空轨和吊装路径分开独立 2026-01-29 14:06:08 +08:00
5836da25f5 修复了批处理未加载路径类型,去掉数据库读取的默认值 2026-01-29 13:44:26 +08:00
4069668a2a 修复批处理未加载待处理项,并修复空中路径创建 2026-01-29 13:27:43 +08:00
4aa62864f3 增加切换线框模式的功能(实验性) 2026-01-28 21:47:52 +08:00
eeade5553f 去掉数据库冗余pathname字段,将“PathRouteId”更改为“RouteId” 2026-01-28 19:19:23 +08:00
0869e32ccb 实现路径名称更新功能,增加名称验证及数据库同步处理 2026-01-28 17:40:04 +08:00
d1ae5c83f4 无碰撞也保存数据库,生成碰撞报告 2026-01-28 17:04:26 +08:00
ec135d5469 调整视野扩展范围方法,增加一个固定值 2026-01-28 14:13:23 +08:00
ba51389df3 重命名“Vehicle”字段为“MovingObject”,更新相关逻辑以统一运动物体的命名 2026-01-28 13:09:20 +08:00
b99d4e82b6 修复数据库创建代码的过时字段,增强数据库初始化错误处理 2026-01-28 12:41:29 +08:00
d5cf6b2f8b 清除路径可视化以避免截图干扰,修复运动物体pathid无效的bug 2026-01-27 23:57:39 +08:00
c1e20bf4ee 优化运动物体和碰撞目标的存储逻辑,更新数据库结构 2026-01-27 23:18:27 +08:00
1e277a4bc3 重构路径列表视图,使用DataGrid替代ListView,优化列绑定和操作按钮 2026-01-27 21:39:49 +08:00
eef2d6b01c 添加停止执行功能并更新相关命令和UI 2026-01-27 20:41:54 +08:00
8a3d480a0a 纠正批处理截图使用当前路径和路径可未视化的问题 2026-01-27 13:36:27 +08:00
423290bfd1 重构批处理任务管理,移除过时的命令和模型,优化路径数据库操作 2026-01-27 12:13:33 +08:00
286228c6e7 移除未使用的命名空间引用,并在动画控制视图中添加检测容差属性 2026-01-27 11:10:22 +08:00
f8e7f046ce 增加批处理队列处理中的进度条支持 2026-01-27 10:53:11 +08:00
e0d598a070 将视角调整到标准俯视图视角 2026-01-27 10:36:26 +08:00
a2018576e5 给选择路径和开始动画增加视角自动调整到路径功能 2026-01-26 22:30:03 +08:00
7f72ad5935 增加视角自动调整到路径功能 2026-01-26 21:04:12 +08:00
82c523a3db 增加角度修正功能,增加批处理列表内容 2026-01-26 17:14:36 +08:00
d0ade35042 增加碰撞报告HTML生成器,支持自动导出HTML报告功能 2026-01-26 16:12:47 +08:00
1e1d740aef 完善碰撞报告高亮和截图保存机制 2026-01-26 15:10:03 +08:00
d5d50d7568 增加批处理高亮和报告生成功能 2026-01-26 12:48:24 +08:00
de55d7ea1b 更新批处理队列项获取方法,默认筛选状态为“全部” 2026-01-26 11:27:49 +08:00
c117e0d0c3 重构碰撞检测参数,替换为检测容差;更新相关配置和UI显示,确保一致性 2026-01-26 00:25:05 +08:00
b4a73e2081 简化批处理,变成队列 2026-01-25 17:58:08 +08:00
6dc266e526 增加批处理任务功能,实现基本功能框架和UI,后续还需完善 2026-01-24 19:56:23 +08:00
3905db56bc 重构碰撞报告生成逻辑,无碰撞也生成报告;修改通行空间为浅绿色; 2026-01-23 18:40:54 +08:00
4c34340fdb 给Clashdetective检测增加等待光标;更新数据库保存逻辑使用去重后的碰撞结果;调整碰撞报告导出对话框的文件类型顺序;修改动画控制视图中操作列的宽度 2026-01-23 17:27:46 +08:00
6f5afdd50b 优化ClashDetective碰撞结果处理逻辑,直接使用移动物体本身作为Item1,并按碰撞对象去重,确保结果准确性 2026-01-22 22:48:50 +08:00
8398f0530a 优化碰撞检测逻辑,新增移动物体管理功能,支持从空间索引中排除移动物体及其子节点 2026-01-22 22:26:36 +08:00
af4b1538ab 新增路径处理工具类,支持截图生成和路径计算功能;更新碰撞报告和导航地图对话框,增加截图预览和重新截图功能 2026-01-22 20:28:37 +08:00
f501b40dc4 修复祝贺对话框的bug 2026-01-22 00:48:12 +08:00
58e7b858f1 优化路径数据更新逻辑,统一调用 RecalculateAndSaveRoute 方法,确保路径修改后正确保存和更新 2026-01-22 00:10:30 +08:00
52ca335e9f 移除路径编辑视图中的起吊高度相关功能,简化路径点操作界面 2026-01-21 23:21:35 +08:00
e2fb22efe6 优自动规划几何体获取逻辑,统一使用DFS遍历和过滤,过滤隐藏项 2026-01-21 21:05:35 +08:00
66353f9621 优化路径数据更新逻辑,统一调用更新函数,简化路径视图模型属性设置 2026-01-21 19:58:19 +08:00
b42089d491 无碰撞时触发祝贺对话框,并调整相关事件处理 2026-01-21 17:26:34 +08:00
1f43d919e4 修复路径可视化中的垂直偏移逻辑,确保在车辆通行空间模式下,起点和终点位置沿着长方体的轴线方向正确调整 2026-01-21 15:52:24 +08:00
91c8f5c6f7 优化通行空间参数设置,统一使用模型单位,调整日志输出格式 2026-01-21 15:01:13 +08:00
131dfba768 更完善吊装路径路径点编辑关联逻辑 2026-01-20 20:58:22 +08:00
366d9309f2 将空中路径合并,完善吊装路径相关命令与参数 2026-01-20 18:24:28 +08:00
50814b3f99 调整车辆长度和安全间隙配置,去掉人工路径对路径点必须在通道上的限制 2026-01-20 12:03:50 +08:00
be9131b89f 优化物体选择逻辑,增加角度修正值的直接设置,确保选择相同物体时保持当前角度修正值 2026-01-19 23:13:05 +08:00
ca503c0448 添加物体角度调整功能,新增角度修正窗口及相关逻辑 2026-01-19 22:48:11 +08:00
b6dd1ca61b 明确车辆长宽高和坐标系的关系,修改通行空间对其的使用。 2026-01-19 17:44:05 +08:00
582ccfe1d6 优化通行空间参数设置,调整路径类型下的通行空间计算逻辑 2026-01-19 01:04:41 +08:00
a235a65a20 增加检测间隙的同步 2026-01-18 22:23:12 +08:00
a52d8fdacb 优化通行空间参数设置逻辑,动态根据路径类型调整通行空间尺寸 2026-01-18 21:57:02 +08:00
405ea5ac7c 修改吊装路径的名称和类型bug 2026-01-18 18:48:43 +08:00
35cbd41a9a 让地面路径使用车辆宽度做尺寸 2026-01-18 18:28:22 +08:00
ea3e02cfba 让空轨路径默认显示通行空间 2026-01-18 17:21:59 +08:00
668073b100 修正吊装通行空间显示问题。 2026-01-18 17:14:19 +08:00
bc1ad3f4c3 优化通行空间可视化功能,自动根据路径类型切换可视化模式,并优化相关渲染逻辑 2026-01-18 16:25:21 +08:00
ddf44b887f 增加了吊装路径 2026-01-18 12:59:30 +08:00
1a219b7b0e UI微调 2026-01-17 20:26:28 +08:00
6dcd27708d 缩小插件UI界面宽度,优化布局 2026-01-17 14:39:55 +08:00
1813c9831d 重构网格可视化功能,使用单独的结构和渲染方法 2026-01-16 21:06:20 +08:00
07f8f5b2bf 修复网格可视化性能问题
根本原因:新代码在循环中调用了 AddPoint() 方法,该方法每次都会触发 UpdateTotalLength(),导致:
算法复杂度:从 O(n) 变成 O(n² log n)
排序次数:12,480 个点 = 78,000,000 次排序
性能损失:从 76 毫秒 → 12.5 秒(130倍慢)
修复方案:直接使用 Points.Add() 绕过昂贵的 UpdateTotalLength() 调用
2026-01-16 20:35:33 +08:00
f4fda4e308 优化虚拟车辆管理,避免动态计算尺寸,直接应用缩放值;更新模型加载完成检测逻辑,改进事件处理 2026-01-16 17:29:04 +08:00
519382f375 增加碰撞构件清单 2026-01-14 13:18:43 +08:00
760786d9b1 清理废弃的连线标记,更新异步方法调用方式 2026-01-14 12:12:22 +08:00
fcc87b2cb0 添加空间索引格子大小配置,优化动画碰撞检测性能,修复属性设置的线程安全问题 2026-01-14 11:41:09 +08:00
c4cf718502 使用复合节点改善性能,并且适配Clashdetective输出的结果(只包含实体节点) 2026-01-13 18:21:54 +08:00
cf2e5a4d2f 生成动画时改成等待光标 2026-01-13 14:50:51 +08:00
0cdae60a00 优化ClashDetective集成,移除事务以提高性能 2026-01-13 12:33:39 +08:00
779d043299 解决空轨计算提取三角形引起的程序崩溃问题。 2026-01-13 11:11:56 +08:00
b1da586b27 用最小夹角法修正空轨基线方向错误的问题 2026-01-13 10:29:19 +08:00
99d2690800 修改基线不贴合的问题 2026-01-12 18:47:19 +08:00
9ae4acdb03 采用obb包围盒计算空轨基准线 2026-01-12 18:34:44 +08:00
708bf533f1 将空轨路径集成到动画和碰撞检测中 2026-01-12 12:10:44 +08:00
8424503576 新增空轨数据结构、自动提取基准路径并可视化、路径点吸附空轨基准路径功能 2026-01-11 16:52:07 +08:00
0a2e29cee9 重构创建和显示虚拟车辆的功能 2026-01-10 16:03:46 +08:00
075fd5c602 移除预计算检测间隙扩大率配置,优化碰撞检测逻辑和高亮显示功能 2026-01-09 18:16:33 +08:00
001b45cc9a 增强路径视图模型的时间信息设置功能 2026-01-09 16:46:38 +08:00
98c23b986e 添加预计算检测间隙扩大率配置,优化动画参数初始化和碰撞检测逻辑 2026-01-09 14:47:41 +08:00
1a2312f3f6 重构碰撞高亮逻辑,统一使用ModelHighlightHelper处理碰撞结果高亮 2026-01-09 13:19:44 +08:00
df2c09a167 实现历史碰撞报告生成,调整时间格式显示,重构相关UI组件 2026-01-09 11:28:49 +08:00
a40e52f538 修复历史记录载入后虚拟车辆不显示的问题 2026-01-08 22:43:28 +08:00
42481a5edc 重构碰撞检测结果的保存与加载逻辑,利用DocumentModels的CreatePathId、ResolvePathId等方法,解决保存和加载ModelItem路径的问题 2026-01-08 22:14:20 +08:00
ee1b0cbe32 添加虚拟车辆尺寸参数到路径动画管理器和碰撞检测结果,优化数据库保存功能 2026-01-08 19:50:44 +08:00
40fce35bc8 数据库保存碰撞节点信息表和相关操作 2026-01-08 17:03:19 +08:00
97bf6dbecd 重构GeometryCacheManager以使用ModelItemAnalysisHelper获取ModelItem完整路径,移除冗余代码 2026-01-08 15:53:00 +08:00
a3bee9a0ba 优化动画状态变化处理,清除碰撞高亮并检查ClashDetective结果 2026-01-08 15:32:45 +08:00
f500861179 重构碰撞检测结果处理逻辑,优化数据库保存功能,合并复合对象碰撞结果,更新UI显示名称获取方式 2026-01-08 15:02:26 +08:00
0de096aed0 实现ClashDetective结果保存到数据库功能,并在UI中展示碰撞检测历史记录 2026-01-08 12:50:00 +08:00
d473065025 向上查找复合节点,处理clashdetective碰撞结果,优化碰撞报告生成逻辑 2026-01-08 11:42:56 +08:00
c044be57c0 在clashdetective测试过程中缓存测试结果 2026-01-08 09:50:12 +08:00
ffac0ae146 动画结束用clashdetective结果高亮,整理高亮架构 2026-01-07 19:18:37 +08:00
4f7935499d 修改预计算和clashdetective检测高度不一致的问题 2026-01-07 16:27:38 +08:00
f02e5cfc28 用API精确计算包围盒的距离,解决多检的问题 2026-01-07 13:50:41 +08:00
cbc63809f0 解决车辆漏检和多检的问题 2026-01-07 12:29:19 +08:00
792d6d249c 更正插入路径点的问题。 2026-01-06 18:01:23 +08:00
e3958affb7 添加碰撞检测进度条 2026-01-06 17:10:50 +08:00
fd05ffce3c 增加去重日志,发现单item有2个几何体导致碰撞结果重复,暂时不修改逻辑 2026-01-06 15:45:53 +08:00
aa0fdc2cec 纠正ModelItem比较的使用方法 2026-01-06 13:00:48 +08:00
be174ab6bb 解决动画过程中,高亮时有时无的情况 2026-01-06 12:48:20 +08:00
28c6f18f47 增加动画哈希,判断是否应该重建动画和检测 2026-01-06 12:04:15 +08:00
6fab2b3432 给数据库中路径的edges增加顺序id 2026-01-06 11:16:44 +08:00
2d2e60c58b 增加数据库路径载入事件,避免事件职责混乱 2026-01-06 10:30:34 +08:00
7fc62537d1 将安全阶段的弧线段渲染为告警样式(红色) 2026-01-06 09:53:38 +08:00
409b39ce78 修补了路径可视化中出现的缝隙 2026-01-05 18:04:11 +08:00
3bae0f0274 修改手动新建路径时的可视化bug 2026-01-05 15:57:00 +08:00
6cc48c3500 把曲线化路径与动画集成 2026-01-05 12:48:57 +08:00
174749e287 修改通行空间样式 2026-01-04 17:12:51 +08:00
0702cc879a 修改切点样式为锥体 2026-01-04 14:26:43 +08:00
13e08faa8a 将切点改为立柱样式 2026-01-04 12:39:54 +08:00
ec3ef5b30e 增加带状连线风格 2026-01-04 11:59:50 +08:00
0081015d0b 把路径可视化设置放到路径页签,统一可视化风格架构(颜色+透明度) 2026-01-04 10:55:33 +08:00
10f408e361 实现了曲线化路径可视化 2025-12-31 18:13:11 +08:00
33296c7415 路径导出支持曲线化 2025-12-31 13:10:47 +08:00
aa0557c9e6 修改系统配置的问题。 2025-12-31 11:29:21 +08:00
93135d3c29 增加了路径曲线化的方案,实现了基础的曲线化路径数据结构和存储,以及曲线化核心算法 2025-12-30 18:29:41 +08:00
7c319b199f 增加转弯路径曲线化方案 2025-12-30 14:40:37 +08:00
f3a07eb482 实现路径坐标点手动编辑功能 2025-12-30 09:55:36 +08:00
0c1de9b45d 纠正生成动画第一帧旋转的bug 2025-12-26 09:40:02 +08:00
0d2a240499 让虚拟车辆选择后直接放到起点 2025-12-25 18:11:36 +08:00
736e6e8448 修改移动物体初始角度不对的问题 2025-12-25 17:48:49 +08:00
dab8dc34c3 修复程序崩溃的bug;完善未完成的功能清单 2025-12-25 15:04:40 +08:00
5647ae9134 修复ClashDetective碰撞结果列表项名字只有一半的问题 2025-12-19 18:17:26 +08:00
ccdada3aad 修复部分墙和柱子检测不到的问题 2025-12-19 17:44:12 +08:00
d9c2ec8c12 修复路径列表UI触发事件引起事件循环的问题 2025-12-19 14:17:20 +08:00
d63896bf63 基本实现物流对象沿着路径转向的功能 2025-12-19 10:13:41 +08:00
2bd117ff8a 重构了高亮显示的逻辑,统一处理流程。 2025-12-11 13:23:55 +08:00
adde6cbdf4 直接控制指定对象的高亮;调整样式,修复选择对象因为GUID加不上的问题 2025-12-11 11:38:40 +08:00
2a8425d529 基本实现手工指定碰撞检测对象 2025-12-11 10:52:09 +08:00
7446431f9c 增加了虚拟物流车辆动画生成和碰撞检测功能 2025-12-10 16:59:35 +08:00
e5b8501a63 将ClashDetective检测模式从HardConservative改为Hard,提高检测性能 2025-12-10 14:36:06 +08:00
23801726ab 临时测试对象引用无效,回复未优化ClashDetective测试的版本; 2025-12-10 13:23:24 +08:00
a19e5f91f3 修改了第一个路径导出路径按钮没激活,无法导出的BUG 2025-12-09 17:14:55 +08:00
941bade44a 复用ClashTest,大幅优化碰撞检测性能 2025-12-09 10:51:11 +08:00
393e1c7291 碰撞检测时,只处理可见对象,大幅优化性能。 2025-12-09 10:06:08 +08:00
4736372552 修改了碰撞性能优化记录文档 2025-12-08 17:24:42 +08:00
01f200ca60 改进了动画预计算的几何缓存和空间索引缓存方法,提高了50%的性能。大型模型(50万)提高到120秒 2025-12-08 17:21:34 +08:00
12616629b0 清理多余的动画检测实时计算代码 2025-12-08 15:04:16 +08:00
f8320066c1 优化碰撞检测算法,进行合理的去重后再检测;优化动画控制逻辑,避免重复订阅事件和资源清理; 2025-12-08 12:31:12 +08:00
a832e91b7b 修改了保存当前选择集无响应的bug,去掉了多余的子节点展开代码 2025-12-05 17:48:58 +08:00
a552ea3a1d 不跟踪nuget.exe 2025-12-01 11:40:52 +08:00
18d6fa65fe 不跟踪nuget.exe 2025-12-01 11:40:25 +08:00
221e13cb5a 更新项目进度评估报告 2025-12-01 11:35:44 +08:00
1cf5816cb4 版本升级到 0.14.0 2025-12-01 11:12:55 +08:00
811b815082 删除不必要的文件 2025-11-21 10:45:38 +08:00
ca4488dcb8 根据安装程序的要求,更新项目文件,目标平台为x64,项目设置为Release 2025-11-18 18:27:40 +08:00
fee00dfb82 增加MSI安装程序生成指南,修改README.md 2025-11-17 12:49:35 +08:00
5e1e4b04b2 对项目完成度进行了评估,列出报告;实现了JSON路径文件导入功能。 2025-11-07 15:13:28 +08:00
330f6591a2 删除多余的引用 2025-10-21 18:48:27 +08:00
80975b829a 删除多余的引用 2025-10-21 18:47:55 +08:00
fbe46ebc85 使用场景加载完成事件SceneLoaded实现文档更新后的物流列表刷新 2025-10-21 18:22:23 +08:00
b0a63409b9 删除一些多余代码,注释掉文档更新刷新物流元素列表代码 2025-10-21 17:44:35 +08:00
dba1f76f0a 增加了几何体缓存文件支持 2025-10-20 10:39:21 +08:00
55fbcbdb48 将网格点可视化从球和立方体,改为圆形和正方形 2025-10-18 16:43:17 +08:00
9c97633411 增加网格点可视化类型,立方体(可用)、点(不可用) 2025-10-17 18:47:35 +08:00
fb13b81259 增加iFlow的文档 2025-10-17 14:29:48 +08:00
5791e57192 对系统管理UI进行修改,更改体素路径测试参数 2025-10-14 17:19:17 +08:00
3aaa176ce6 优化插件启动时的动作 2025-10-14 16:29:05 +08:00
b7a112354d merge: 集成空间索引优化碰撞检测性能
- 新增空间哈希网格系统(SpatialHashGrid + SpatialIndexManager)
- 优化碰撞检测查询效率(从O(n)到O(1)平均复杂度)
- 格子大小使用车辆宽度参数(1米),空间分割166格
- 平均每格1.71个对象,查询性能提升约10倍
- 移除DataBindingPerformanceMonitor功能(简化代码)
- 修复碰撞报告检测间隙显示格式
- 净删除代码511行,提升代码质量

测试结果:
- 空间索引构建时间:10ms(冷启动)/ 1ms(缓存)
- 284个对象分布在166个格子中
- 碰撞检测性能显著提升
2025-10-14 15:18:36 +08:00
b05bb727c6 refactor: 完成 DataBindingPerformanceMonitor 功能的完整删除
- 删除 DataBindingPerformanceMonitor.cs 文件
- 从 NavisworksTransportPlugin.csproj 移除编译引用
- 清理 ViewModelBase.cs 中的所有性能监控代码
- 清理 ThreadSafeObservableCollection.cs 中的性能监控集成
- 清理 SmartDataBindingOptimizer.cs 中的性能监控使用
- 清理 BindingExpressionOptimizer.cs 中的性能监控调用

该功能不再需要,移除后简化了代码结构
2025-10-14 15:01:26 +08:00
821725d406 perf: 优化空间索引格子大小,使用车辆宽度参数
问题分析:
1. 原实现使用运动对象包围盒最大边(6-8米)作为格子大小
2. 导致格子过大(仅10-21个格子),每格包含13-28个对象
3. 空间查询效率低,需检查大量候选对象

优化方案:
1. 改用系统配置中的车辆宽度(VehicleWidthMeters,默认1米)
2. 预期效果:格子数增加到150-200个,每格2-5个对象
3. 显著提升空间查询效率

搜索半径说明:
- 当前实现 objectDiagonal/2 + detectionGap 是正确的
- 从中心到最远角的距离恰好是对角线的一半
- 不需要修改

修改文件:
- PathAnimationManager.cs:
  - 添加 ConfigManager 引用
  - 使用车辆宽度计算格子大小
  - 添加详细日志输出

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 14:43:15 +08:00
40072e2eb7 fix: 格式化碰撞报告窗口中检测间隙的显示
问题:检测间隙显示为0.05001768,精度过高不便阅读
解决:在XAML绑定中添加StringFormat={}{0:F2}格式化为两位小数
效果:现在显示为0.05,清晰易读

修改文件:
- CollisionReportDialog.xaml: 为DetectionGap绑定添加格式化

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 12:33:07 +08:00
02ddc812cf refactor: 移除未使用的 IsChannelObjectPublic 方法
优化历程:
1. 最初添加 IsChannelObjectPublic() 用于空间索引中逐个检查对象
2. 后续优化为 GetNonChannelGeometryItemsCache() 在缓存层预过滤通道
3. IsChannelObjectPublic() 方法已不再被使用,删除以保持代码整洁

私有方法 IsChannelObject() 保留,仍在缓存构建时使用

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 12:28:43 +08:00
1fb6c565fd perf: 优化空间索引构建,直接使用预过滤的非通道缓存
问题分析:
- 空间索引获取290个几何对象缓存
- 然后遍历290次,每次调用 IsChannelObjectPublic() 检查是否为通道
- 使用 HashSet.Contains 虽然是O(1),但遍历290次仍然是O(n)
- 通道对象在缓存构建阶段已经识别,不应该重复判断

优化方案:
1. 添加 GetNonChannelGeometryItemsCache() 方法返回已过滤通道的缓存
2. 在 ClashDetectiveIntegration 中一次性过滤(使用LINQ Where)
3. SpatialIndexManager 直接使用284个非通道对象,无需再判断
4. 移除 IsChannelObjectPublic() 方法和相关逻辑(不再需要)

性能提升:
- 空间索引构建:减少290次 HashSet 查询
- 代码更简洁:从遍历+判断改为直接使用预过滤结果
- 逻辑更清晰:通道过滤在缓存层完成,索引层只负责索引

技术细节:
- GetNonChannelGeometryItemsCache() 使用 LINQ Where 过滤
- 线程安全:使用 lock 确保并发访问安全
- 日志优化:明确标注"通道已在缓存阶段过滤"

代码清理:
- 移除 IsChannelObjectPublic() 的使用(不再需要)
- 移除 channelExcludedCount 变量
- 简化索引构建循环逻辑

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 12:25:59 +08:00
f194a835ed refactor: 移除空间索引构建中的冗余缓存调用
问题分析:
- 动画生成阶段已经构建了所有缓存(几何对象+通道对象)
- 空间索引构建时再次调用 BuildChannelObjectsCache() 和 BuildAllGeometryItemsCache()
- 虽然这两个方法有"已缓存则跳过"逻辑,但调用本身是逻辑冗余

优化方案:
- 移除 BuildChannelObjectsCache() 和 BuildAllGeometryItemsCache() 调用
- 直接使用 GetAllGeometryItemsCache() 获取缓存
- 如果缓存不存在,抛出异常而非静默回退(暴露调用顺序错误)

设计原则:
- "让问题快速暴露" > "让程序看起来正常运行"
- 明确约定:调用方必须在动画生成阶段构建缓存
- 如果缓存不存在,说明调用顺序错误,应该报错

技术细节:
- 将回退逻辑从 Warning + 实时获取 改为 Error + 抛出异常
- 错误信息明确指出调用方的责任
- 确保代码的调用契约清晰可追溯

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 12:21:38 +08:00
320dfa23f3 perf: 空间索引复用几何对象缓存避免重复遍历
问题描述:
- BuildAllGeometryItemsCache() 已经遍历模型树获取290个几何对象
- SpatialIndexManager 又重新遍历一次模型树获取290个对象
- 重复工作导致性能浪费

优化方案:
1. 在 ClashDetectiveIntegration 中添加 GetAllGeometryItemsCache() 公开方法
2. SpatialIndexManager 调用该方法获取缓存的几何对象列表
3. 避免重复遍历模型树,提升性能

技术细节:
- GetAllGeometryItemsCache() 返回列表副本保证线程安全
- 添加缓存不存在时的回退逻辑(保险措施)
- 日志输出改为"从缓存获取"以明确数据来源

性能提升:
- 减少模型树遍历次数:2次 → 1次
- 优化空间索引构建流程
- 所有碰撞检测和空间索引共享同一个几何对象列表

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 12:15:29 +08:00
2c6187a674 fix: 修正碰撞报告中检测间隙单位显示错误
问题描述:
- 碰撞报告显示检测间隙为 0.1641米,但实际设置是 0.05米
- 原因:DetectionGap 属性直接返回内部模型单位值,未转换为米制

修复方案:
- 修改 PathAnimationManager.DetectionGap 属性
- 添加单位转换逻辑,将内部模型单位转换为米制单位
- 确保报告中显示的检测间隙与用户设置一致

技术细节:
- _detectionGap 字段以模型单位存储(内部使用)
- DetectionGap 属性返回米制单位(外部接口)
- 使用 UnitsConverter.GetUnitsToMetersConversionFactor() 进行转换

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 12:09:54 +08:00
f761d93676 feat: 添加通道对象排除逻辑到空间索引
修改内容:
1. 在 ClashDetectiveIntegration 中添加 IsChannelObjectPublic() 公开方法
2. SpatialIndexManager.BuildGlobalIndex() 构建索引前调用 BuildChannelObjectsCache()
3. 索引循环中过滤通道对象,避免将其加入空间索引
4. 增强日志输出,显示排除的通道对象数量

技术细节:
- 使用已有的 _channelObjectsCache 进行 O(1) 查询
- 在索引构建时过滤(一次性)而非每次查询时过滤
- 保持与旧架构相同的通道排除逻辑

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 12:05:05 +08:00
c7f6586fa9 feat: 集成空间索引到碰撞检测系统(单层架构)
重构说明:
- 移除两层架构的 GetPotentialColliders() 方法
- 移除不再使用的 CalculatePathBounds() 方法
- 在 PrecomputeAnimationFrames() 中构建全局空间索引
- 每帧使用空间索引直接查询附近对象(O(1) 复杂度)

技术细节:
- 网格大小:基于动画对象包围盒尺寸
- 搜索半径:对象对角线的一半 + 检测间隙
- 自动排除动画对象本身

性能优势:
- 旧架构:290 对象 → 108 对象(预过滤)→ 每帧遍历 108 对象
- 新架构:290 对象 → 每帧空间查询 ~5-15 对象
- 预期性能提升:10-15x
2025-10-14 11:56:03 +08:00
360d55ffa8 fix: 修复 Vector3d.Distance 调用并将 Spatial 文件添加到项目
- 修复 Distance 方法调用(使用实例方法而非静态方法)
- 将 SpatialHashGrid.cs 和 SpatialIndexManager.cs 添加到 csproj
- 在 PathAnimationManager.cs 中添加命名空间引用
2025-10-14 11:53:52 +08:00
8cec18a141 refactor: 借鉴 PointHashGrid3d 优化空间哈希网格
改进内容:
1. 使用 ScaleGridIndexer3 实现更清晰的坐标转换(职责分离)
2. 优化 FindInRadius:提前判断 distanceFunc 是否为 null,避免循环中重复检查
3. 使用 TryGetValue 避免字典的两次查找
4. 增强代码注释,说明设计参考和增强点

参考 geometry3Sharp 的 PointHashGrid3d 设计,但保留了我们实现的优势:
- 支持返回范围内所有对象(不仅是最近的一个)
- 支持直接访问网格单元
- 提供详细的统计信息
2025-10-14 11:49:39 +08:00
d889635c1c feat: 实现自定义空间哈希网格 (SpatialHashGrid)
- 创建 SpatialHashGrid.cs: 基于 Vector3i 的3D空间哈希表
  - 支持对象插入、范围查询、格子查询
  - O(1) 平均查询复杂度
  - 带精确距离检查的范围查询

- 创建 SpatialIndexManager.cs: 全局空间索引管理器
  - 单例模式,所有动画共享
  - 构建全局索引(索引所有几何对象)
  - FindNearbyObjects: 高效范围查询
  - 对象位置缓存(避免重复计算包围盒)

技术细节:
- 使用 geometry4Sharp (g4) 的 Vector3d, Vector3i
- 基于 Dictionary<Vector3i, List<ModelItem>> 实现
- 格子大小可配置(建议设为车辆半径 × 2)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 11:40:04 +08:00
687b342e0f 增加geometry4sharp的评估 2025-10-14 11:17:01 +08:00
bd7eeb3d46 修复路径预计算范围不对、碰撞检测报告名字匹配错误的BUG 2025-10-13 19:14:46 +08:00
882f8283b5 修复AABB包围盒碰撞检测逻辑错误
问题:
BoundingBoxesIntersectWithGap方法中gap参数加在了错误的位置:
- 当前写法:box2.Max.X + gap < box1.Min.X
- 应该写成:box1.Min.X - gap > box2.Max.X

错误原因:
gap应该只用于扩展box1(动画对象),而不是同时扩展box1和box2

修复方案:
采用标准AABB碰撞检测算法(参考MDN文档):
- X轴相交:box1.Max.X + gap >= box2.Min.X && box1.Min.X - gap <= box2.Max.X
- Y轴相交:box1.Max.Y + gap >= box2.Min.Y && box1.Min.Y - gap <= box2.Max.Y
- Z轴相交:box1.Max.Z + gap >= box2.Min.Z && box1.Min.Z - gap <= box2.Max.Z
- 三个轴向都相交才算碰撞

参考:https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_collision_detection

影响:
修复后预计算阶段应该能正确检测到碰撞,但需要进一步排查
为什么当前预计算结果为0个碰撞的根本原因

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 17:47:16 +08:00
5cf9336a9c 修复:恢复PathAnimationManager变量的默认初始值
问题:
- 在修改注释时不小心删除了变量的默认初始值
- 导致_animationDuration=0, _animationFrameRate=0等
- 造成UI界面空白和除零异常

修复:
- _animationDuration = 10.0(默认10秒)
- _animationFrameRate = 30(默认30FPS)
- _collisionDetectionAccuracy = 0.1
- _movementSpeed = 1.0
- _detectionGap = 0.05

影响:修复UI界面显示问题

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 17:28:22 +08:00
0732cb493f 简化日志输出:移除模型单位显示,只保留米单位
修改内容:
- 路径总长度日志:只显示米单位
- 检测精度日志:只显示米单位/帧
- SetCollisionDetectionAccuracy日志:只显示米单位
- SetDetectionGap日志:只显示米单位

理由:
- 用户只需要看到米单位,模型单位是内部实现细节
- 简化日志输出,提高可读性

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 17:23:12 +08:00
3343f6f5c1 修复PathAnimationManager中的单位转换错误
问题:
1. 路径总长度日志显示"米",但实际计算的是模型单位
2. 检测精度内部使用模型单位,但Set方法接收米参数后未转换
3. 检测间隙内部使用模型单位,但Set方法接收米参数后未转换
4. 日志中单位标注不明确,容易造成混淆

修复内容:
1. CalculateTotalPathDistance返回模型单位,增加转换显示米单位
2. SetCollisionDetectionAccuracy接收米单位参数,内部转换为模型单位存储
3. SetDetectionGap接收米单位参数,内部转换为模型单位存储
4. 所有日志同时显示米单位和模型单位,格式:X.XX米 (Y.YY模型单位)

设计原则:
- 内部存储统一使用模型单位(与Navisworks API Point3D/BoundingBox3D一致)
- 对外接口(Set方法参数)使用米单位(符合用户习惯)
- 日志同时显示两种单位,避免歧义

影响范围:
- SetupAnimation()中的路径总长度日志
- PrecomputeAnimationFrames()中的路径总长和检测精度日志
- SetCollisionDetectionAccuracy()参数单位转换
- SetDetectionGap()参数单位转换

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 17:15:28 +08:00
1a3d1e7f49 清理多余文件 2025-10-13 16:53:40 +08:00
ca8bcc0bba 移除碰撞检测中的自定义进度条,避免与ClashDetective进度条冲突
问题:
- 在CreateAllAnimationCollisionTests中显示自定义进度条
- 调用TestsRunTest()时ClashDetective显示自己的自动保存进度条
- 两个进度条同时出现,造成视觉混乱

修改:
- 移除Progress progress变量声明
- 移除BeginProgress()调用
- 移除finally块中的EndProgress()调用

解决方案:
- 只显示ClashDetective的原生自动保存进度条
- 保持与Navisworks API的一致性
- 避免进度条冲突,提供更简洁的用户体验

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 13:00:01 +08:00
89722efaca 修复碰撞检测进度条闪烁:使用不确定进度条样式
移除了CreateAllAnimationCollisionTests方法中的progress.Update()调用,
使进度条保持不确定样式(滚动条,无百分比显示)。

原因分析:
- ClashDetective的TestsRunTest()在后台运行,不提供进度回调
- 频繁调用progress.Update()导致进度条在确定/不确定样式间切换,产生闪烁
- 每个碰撞点处理时间不固定(包括对象移动、测试运行、测试删除)

解决方案:
- 去掉Update()调用,让Progress API保持一致的不确定进度条样式
- 仍保留BeginProgress/EndProgress,提供"正在处理N个碰撞点"的提示

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 12:50:45 +08:00
07bd9351bf 代码优化:简化命名空间引用,使用using别名
修改内容:
- 将 Autodesk.Navisworks.Api.Progress 简化为 Progress
- 将 Autodesk.Navisworks.Api.Application.BeginProgress() 简化为 NavisApplication.BeginProgress()
- 将 Autodesk.Navisworks.Api.Application.EndProgress() 简化为 NavisApplication.EndProgress()

理由:
- 文件顶部已经有 using Autodesk.Navisworks.Api;
- 文件顶部已经有 using NavisApplication = Autodesk.Navisworks.Api.Application;
- 使用using别名可以简化代码,提高可读性

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 12:16:14 +08:00
aaebbcad21 优化批量导出进度显示:统一使用Progress API,分离职责
核心改进:
1. ExportLayerToNwd专注导出逻辑,不再管理进度条
2. LayerManagementViewModel统一管理批量导出进度条
3. 状态栏只在开始/结束时显示,避免UI刷新同步问题

修改内容:

**ModelSplitterManager.cs**
- 移除ExportLayerToNwd内的Progress API相关代码
- 移除进度条声明、初始化、更新和清理代码
- 移除用户取消检查(由调用者管理)
- 保留核心导出逻辑:隔离、导出、恢复

**LayerManagementViewModel.cs**
- 在批量导出循环开始前创建Progress API进度条
- 循环中为每个文件更新进度条描述和百分比
- 显示:"正在导出第 X/Y 个分层:分层名"
- 移除循环中的状态栏进度更新(避免UI同步问题)
- 只在结束时更新状态栏最终结果
- finally块确保进度条被正确关闭

最终效果:
-  单文件导出:无进度条,快速完成
-  批量导出:统一的Progress API显示完整进度信息
-  状态栏:只显示开始/结束提示,不跟踪过程进度
-  职责清晰:导出逻辑与进度显示分离

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 12:14:20 +08:00
2e583cb9b6 清理ModelSplitterManager中未使用的辅助函数
删除以下未使用的方法:
- GetAllModelItems: 已被新的深度遍历函数替代
- PreviewSplitByAttribute: 通用属性预览方法已不再使用

这些函数的功能已经被更优化的实现替代,删除以保持代码整洁。

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 11:48:22 +08:00
fec15d0805 优化进度条功能:移除碰撞检测的取消支持
碰撞检测是一个整体流程,中途取消会导致部分碰撞结果丢失。
因此移除取消检查,但保留进度显示功能。

修改内容:
- 移除 progress.IsCanceled 取消检查代码
- 保留 progress.Update() 进度更新
- 保留 finally 块中的进度条清理
- 添加注释说明不支持取消的原因

对比:
- 分层导出:支持取消(跳过当前文件不影响其他文件)
- 碰撞检测:不支持取消(确保获得完整的碰撞检测结果)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 11:47:47 +08:00
543479ee65 为分层导出和碰撞检测添加Progress API进度条支持
- 分层导出(ExportLayerToNwd):增加三阶段进度条(隔离30%/导出70%/完成100%)和用户取消支持
- 碰撞检测(CreateAllAnimationCollisionTests):增加循环进度条和用户取消支持
- 清理LayerManagementCommands中未使用的辅助函数

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 11:37:44 +08:00
a0a667d45d 修复路径删除时数据库未同步删除的bug
问题描述:
在路径编辑界面删除路径时,虽然UI列表和3D可视化被正确清除,
但数据库中的路径记录没有被删除,导致数据不一致。

根本原因:
PathEditingViewModel.ExecuteDeletePathAsync 方法直接操作集合:
- 直接调用 _pathPlanningManager.ModifiableRoutes.Remove(coreRoute)
- 绕过了 PathPlanningManager.DeleteRoute() 方法
- PathPlanningManager.DeleteRoute() 包含数据库删除逻辑

修复方案:
 使用 PathPlanningManager.DeleteRoute() 替代直接集合操作
 确保完整的删除流程:
   1. 清除3D可视化显示
   2. 调用 PathPlanningManager.DeleteRoute()
      - 从内存集合删除
      - 调用 PathDatabase.DeletePathRoute()
      - 更新当前路径状态
   3. 更新UI列表

修改文件:
- src/UI/WPF/ViewModels/PathEditingViewModel.cs (754-764行)

影响范围:
- 修复数据库一致性问题
- 增强日志记录(记录删除成功/失败)
- 不影响现有UI和可视化功能

测试建议:
1. 在路径列表中删除路径
2. 重启Navisworks验证路径是否真正被删除
3. 检查数据库中PathRoutes表记录是否减少

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 11:05:18 +08:00
e9e0d8c83f 合并体素网格路径规划功能分支到 2026
🎯 核心功能:
-  VoxelGrid 基础数据结构 - 3D体素网格表示
-  VoxelGridGenerator - 使用SDF精确体素化生成器
-  集成 geometry4Sharp 库 (v1.0.0) - MeshSignedDistanceGrid支持
-  XY平面障碍物膨胀算法 - 支持车辆安全间隙
-  3D体素路径规划 - A*算法在体素空间寻路
-  体素网格可视化验证 - UI测试命令

📐 签名距离场 (SDF) 方法:
- 使用 MeshSignedDistanceGrid 计算精确距离场
- 支持复杂几何体的准确体素化
- 门类型留空(不作为障碍物)
- 真实障碍物精确标记

🔧 膨胀算法:
- XY平面(每层独立)障碍物膨胀
- 支持车辆半径安全间隙设置
- 边界检测和特殊处理
- 多层高度支持

 性能优化:
- 批量 COM Selection 创建 - 减少API调用
- 进度条支持 - 实时显示处理进度
- 用户可取消操作 - 优雅中断
- 耗时统计和日志优化

📚 文档完善:
- 扩展 Progress API 使用指南(527行)
- 添加体素网格任务跟踪文档
- 实际应用案例和最佳实践
- 完整代码示例

🧪 测试功能:
- 体素网格SDF测试命令
- 体素路径规划测试命令
- UI测试面板集成

🛠️ 代码改进:
- 统一 ExtractTriangles 方法实现
- 优化集合提取器性能
- 完善错误处理和资源清理
- 移除重复代码

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 10:47:57 +08:00
a1d2a65010 优化集合提取器 2025-10-13 10:41:33 +08:00
e12e1125d2 为几何体提取添加进度条支持,优化用户体验
主要改进:
- 添加 Navisworks Progress API 支持到 ExtractTriangles 方法
- 实时显示片段处理进度(百分比)
- 支持用户取消操作(IsCanceled 检查)
- 移除批量日志输出,改用进度条展示
- 统一 ExtractTriangles 方法,移除单个项目的重复实现
- 在 finally 块中确保进度条正确关闭

文档更新:
- 扩展 NavisworksAPI使用方法.md 中的进度条章节
- 添加详细的 Progress API 使用指南
- 包含实际应用案例、最佳实践和常见陷阱
- 提供完整的代码示例和性能优化建议

性能改进:
- 每个片段更新一次进度(3516次调用,可接受)
- 移除频繁的日志写入(每100个片段),减少 I/O 开销
- 保留关键日志(开始、结束、取消、错误)

用户体验提升:
- 可视化进度反馈,避免假死感
- 支持随时取消长时间操作
- 优雅的错误处理和资源清理

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 10:40:24 +08:00
51be24161d 优化三角形提取性能 - 实现批量COM Selection创建
问题分析:
- 当前逐个ModelItem创建COM Selection(290次)
- 从290个模型项提取776140个三角形耗时较长
- COM对象创建/销毁开销大

优化方案:
1. 新增 GeometryHelper.ExtractTrianglesBatch 方法
   - 批量创建COM Selection(290次 → 1次)
   - 一次性提取所有模型项的三角形
   - 添加进度日志(每100个片段输出一次)

2. 修改 NavisworksToDMesh3Converter.ConvertFromModelItems
   - 从逐个调用 ExtractTriangles 改为批量调用 ExtractTrianglesBatch
   - 添加详细的耗时统计(毫秒和秒)
   - 添加 System.Linq 引用支持ToList()

3. 保留原有 ExtractTriangles 方法
   - 向后兼容单个模型项提取场景
   - 用于小规模提取或特殊场景

预期效果:
- COM Selection创建:290次 → 1次
- 预计性能提升:50-70%
- 日志更详细,便于性能分析

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 09:34:52 +08:00
2e9b9fe2b3 添加三角形提取阶段的耗时统计
在DMesh3转换过程中添加了三角形提取阶段的耗时统计,
便于性能分析和优化。

修改内容:
- 在ConvertFromModelItems方法中添加Stopwatch
- 统计从ModelItem提取三角形的耗时
- 日志格式:从 N 个模型项共提取 M 个三角形,耗时: X ms

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 09:21:38 +08:00
27908540c2 实现XY平面膨胀算法和3D体素路径规划
本次提交包含三个主要改进:

1. XY平面膨胀算法(VoxelGrid.cs)
   - 实现简单迭代形态学膨胀
   - 只在水平方向(XY平面)的4邻域膨胀
   - 不在Z方向(垂直方向)膨胀
   - 符合车辆物流场景:车辆只侧面/顶部碰撞障碍物

2. 3D体素路径规划(VoxelPathFinder.cs)
   - 集成RoyT.AStar库进行3D A*路径规划
   - 支持体素网格上的路径搜索
   - 添加VoxelPathFindingTestCommand测试命令

3. UI和测试改进
   - 删除旧的包围盒测试命令(VoxelGridTestCommand.cs)
   - 更新SystemManagementView UI
   - 添加体素路径规划测试功能

核心设计原则:
- 门模型在SDF生成前被排除(留出通道空洞)
- SDF阶段只标记几何体内部为障碍物
- 安全间隙仅在XY平面膨胀阶段应用
- 避免Z方向的错误膨胀

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-12 21:13:02 +08:00
2f78b4e58e 修复SDF标记阶段在Z方向应用安全间隙的问题
问题:
- SDF标记阶段使用了3D距离判断(distance < safetyMargin)
- 导致薄楼板上下0.6米范围内的体素都被标记为障碍物
- 违反了"车辆只会侧面或顶部碰撞"的设计原则

解决方案:
- SDF标记阶段只标记几何体内部(distance < 0)为障碍物
- 移除SDF阶段的安全间隙判断(minPassableDistance)
- 安全间隙只在后续的XY平面膨胀阶段应用

效果:
- 薄楼板只在其实际占据的Z层被标记为障碍物
- 楼板上下空间不会被错误标记
- 安全间隙仅在水平方向(XY平面)生效

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-12 21:12:27 +08:00
1e11f60042 feat(voxel): 实现障碍物膨胀算法 - 阶段1.5完成
完成任务1.5 - 障碍物膨胀算法(Obstacle Inflation)

核心实现:
-  VoxelGrid.InflateObstacles() 主方法(约200行)
-  PerformFastSweeping() 8方向扫描
-  Sweep() 单向距离传播
-  CheckAndUpdate() 邻居距离更新
-  从geometry4Sharp移植Fast Sweeping算法

技术特性:
- 3D距离变换使用DenseGrid3f存储距离场
- 8方向扫描:(+1,+1,+1), (-1,-1,-1)等8个方向
- 每次扫描检查7个邻居进行距离更新
- 门类型体素保护机制(门不膨胀)
- 正确的模型单位转换处理

性能数据(gatehouse_pub.nwd):
- 初始障碍物:3,747个体素
- 膨胀后障碍物:4,477个体素
- 新增膨胀:730个体素(19.5%增量)
- Fast Sweeping耗时:3ms
- 总膨胀耗时:4-5ms
- 可通行比例:22.9% → 7.9%

集成测试:
- VoxelGridSDFTestCommand新增膨胀测试
- SystemManagementViewModel调整测试参数(0.6米膨胀半径)
- 膨胀半径必须 >= 体素大小才能生效

问题修复:
- 修复膨胀为0的问题(参数配置:0.3米 < 0.5米体素)
- 调整测试参数为0.6米(600模型单位 > 500体素大小)
- 移除VoxelGridSDFTestCommand构造函数默认参数

阶段1完成:
- 任务1.1-1.5全部完成 
- 性能远超目标(< 1秒 vs 目标 < 5秒)
- 提前5天完成阶段1
- 建议继续阶段2

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-12 17:00:06 +08:00
b0b29c581c fix(ui): 修复XAML资源引用错误
问题:
- 使用了不存在的 NavisworksSecondaryTextBrush 资源
- 导致插件加载时 XAML 解析异常崩溃

修复:
- 将 NavisworksSecondaryTextBrush 改为 NavisworksDarkBrush
- 只使用 NavisworksStyles.xaml 中已定义的资源

可用的 Brush 资源:
- NavisworksPrimaryBrush
- NavisworksSecondaryBrush
- NavisworksLightBrush
- NavisworksBackgroundBrush
- NavisworksButtonBrush
- NavisworksTextBrush
- NavisworksDarkBrush

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-12 13:56:56 +08:00
6460dda879 feat(voxel): 添加体素网格测试UI和命令
实现内容:
1. 创建 VoxelGridTestCommand - 体素网格测试命令
   - 选中对象创建体素网格
   - 简单的边界标记(边界为障碍物,内部为自由空间)
   - 生成可视化报告
   - 防止体素数量过多(>100万)

2. 在系统管理页签添加功能测试区域
   - 新增"功能测试"分组
   - 添加"测试体素网格"按钮
   - 集成到 SystemManagementViewModel

3. 功能特性
   - 自动计算选中对象的总包围盒
   - 提取三角网格(为后续SDF做准备)
   - 体素统计信息(总数、可通行、障碍物)
   - 生成详细的测试报告

使用方法:
1. 在 Navisworks 中选择一个或多个模型对象
2. 打开插件面板 -> 系统管理页签
3. 滚动到最下方"功能测试"区域
4. 点击"测试体素网格"按钮
5. 查看测试结果对话框和日志

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-12 13:49:55 +08:00
805814616a feat(voxel): 阶段1.5 - 体素网格可视化验证
实现内容:
- 创建 VoxelGridVisualizer 类,支持多种可视化模式
- VisualizeAsPoints(): 完整网格可视化(支持采样率)
- VisualizeSlice(): 单层切片可视化
- VisualizeBoundary(): 障碍物边界可视化
- ConvertToPathRoute(): 转换为 PathRoute 以便集成到现有渲染系统
- QuickVisualizationTest(): 快速测试套件
- GenerateVisualizationReport(): 文本报告生成

设计策略:
- 复用现有 PathPointRenderPlugin,而非创建新的 RenderPlugin
- 将体素转换为 PathPoint 对象进行渲染
- 使用小球体表示体素,不同颜色区分类型

完成情况:
-  编译通过,无错误
-  阶段 1 全部 5 个任务完成 (100%)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-12 13:38:57 +08:00
f64e79d372 feat(voxel): 阶段1.4 - 测试 geometry4Sharp 的 MeshSignedDistanceGrid
- 创建 MeshSDFTester.cs: 测试 geometry4Sharp 库的可用性和功能
- 实现 Navisworks Triangle3D 到 DMesh3 的转换
- 实现 MeshSignedDistanceGrid (SDF) 计算功能
- 添加库可用性快速测试方法
- 手动构建测试立方体网格

特性(原型版本):
- QuickLibraryTest(): 验证 geometry4Sharp 库是否正常工作
- ConvertToDMesh3(): 将 Navisworks 三角形转换为 DMesh3 格式
- ComputeSDF(): 计算签名距离场(使用 DMeshAABBTree3 加速)
- TestFullPipeline(): 完整测试流程(ModelItem → 三角形 → DMesh3 → SDF)
- CreateTestCubeMesh(): 手动创建测试立方体

注意:
- 这是原型版本,部分 SDF 距离查询 API 需要进一步研究
- 验证了 DMesh3, DMeshAABBTree3, MeshSignedDistanceGrid 基本可用
- 与 GeometryHelper.ExtractTriangles() 集成

下一步: 体素可视化验证(阶段1.5)或根据实际需求调整
2025-10-12 13:20:29 +08:00
40946091dd docs(voxel): 更新任务跟踪 - 任务1.3已完成
- 标记任务 1.3(实现简单体素化原型)为已完成
- 更新总体进度:3/17 任务完成(18%)
- 更新阶段 1 进度:3/5 任务完成(60%)
- 添加变更日志:VoxelGridGenerator 360行代码完成

完成情况:
- 包围盒体素化算法实现
- 物流类型自动识别
- 障碍物膨胀功能
- 单位自动转换
- 性能统计日志
2025-10-12 11:47:01 +08:00
064945bfa6 feat(voxel): 阶段1.3 - 实现简单体素化原型 (VoxelGridGenerator)
- 创建 VoxelGridGenerator.cs: 从 BIM 模型生成体素网格
- 使用包围盒方法进行简单体素化(阶段1原型版本)
- 支持障碍物膨胀(车辆半径)
- 自动识别物流元素类型(从属性或名称推断)
- 详细的性能日志和统计信息
- 包含 CreateTestGrid() 快速测试方法

特性:
- 包围盒体素化算法
- 物流类型自动识别(门、楼梯、电梯、通道、障碍物)
- 体素膨胀处理(仅对障碍物)
- 单位自动转换(米 → 模型单位)
- 完整的 XML 中文注释(360行代码)

性能:
- 支持大规模场景处理
- 详细的时间统计和进度日志

下一步: 测试 MeshSignedDistanceGrid(阶段1.4)
2025-10-12 11:45:38 +08:00
aece9fbbe1 docs(voxel): 更新任务跟踪文档 - 任务1.1和1.2已完成
- 标记任务 1.1(安装 geometry4Sharp)为已完成
- 标记任务 1.2(创建 VoxelGrid 基础数据结构)为已完成
- 更新总体进度:2/17 任务完成(12%)
- 更新阶段 1 进度:2/5 任务完成(40%)
- 添加变更日志条目

完成情况:
- VoxelCell.cs: 167 行代码
- VoxelGrid.cs: 365 行代码
- 完整的 XML 中文注释
- 编译通过,无错误
2025-10-12 11:27:29 +08:00
c9ca6b4d32 feat(voxel): 阶段1.2 - 创建 VoxelGrid 基础数据结构
- 创建 VoxelCell.cs: 体素单元类,包含类型、通行性、距离、成本等属性
- 创建 VoxelGrid.cs: 3D体素网格类,包含坐标转换、邻域查询、统计信息等方法
- 添加到 NavisworksTransportPlugin.csproj 编译项
- 编译成功验证

特性:
- VoxelCell: 物流属性集成,SDF距离存储,成本计算方法
- VoxelGrid: 世界坐标↔体素索引转换,6/26邻域查询,欧几里得/曼哈顿距离计算
- 完整的 XML 文档注释(中文)

下一步: 实现简单体素化原型
2025-10-12 11:25:45 +08:00
0a61057476 feat(voxel): 阶段1.1 - 添加 geometry4Sharp 1.0.0 NuGet 包
- 安装 geometry4Sharp 1.0.0
- 更新 packages.config
- 添加 DLL 引用到 NavisworksTransportPlugin.csproj
- 使用 net48 版本确保 .NET Framework 4.8 兼容性

依赖项: packages\geometry4Sharp.1.0.0\lib\net48\geometry4Sharp.dll

下一步: 创建 VoxelGrid 基础数据结构
2025-10-12 11:22:52 +08:00
2b0b13c43a docs(voxel): 添加体素网格路径规划任务跟踪文档
- 创建详细的 4 阶段任务清单(17 个子任务)
- 每个任务包含:工作量、验收标准、阻塞问题
- 包含风险管理、进度跟踪、决策流程
- 包含 Git 工作流和提交规范

文件位置: doc/working/voxel_pathfinding_task_tracker.md
2025-10-12 11:15:55 +08:00
dd991d38ce 增加体素网格和GPU加速2个可行性方案 2025-10-12 11:05:29 +08:00
455450726c 修复斜线优化有高度差的路径点的问题。 2025-10-12 01:12:31 +08:00
37f03362c4 修复参数配置错误和门网格高度层缺失错误 2025-10-11 18:39:06 +08:00
a938afd946 对齐配置参数 2025-10-11 17:36:33 +08:00
7343133f12 增加了配置管理功能,配置文件保存为toml格式,可在配置窗口编辑;实现了日志级别管理功能 2025-10-11 12:18:33 +08:00
a46568f43e 版本升级到0.13.0:
1、核心突破
3D路径规划系统 - Graph替代Grid,真正的3D路径规划
高度层网格 - 支持楼梯、斜面等复杂多层场景
网格膨胀算法 - 障碍物和边界分离计算,提高精度
2、新增功能
SQLite数据库集成 - 路径分析、碰撞报告存储
导航地图生成 - 场景图片导出功能
无关项过滤 - 新物流类型,提高规划效率
3、性能优化
网格缓存机制 - 大幅提高路径规划速度
高度处理完善 - 斜面路径、楼梯网格生成
几何体过滤 - 避免误判障碍物
4、架构清理
删除冗余UIStateMachine、未使用的WPF Services
删除所有向后兼容代码
简化自动寻路代码,提升可读性
2025-10-11 10:03:22 +08:00
df1885a352 完善了膨胀算法,把障碍物和边界分开计算 2025-10-10 23:51:13 +08:00
83a4a0e7aa 修改楼梯场景路径不能到达却显示到达的bug 2025-10-10 23:27:46 +08:00
2464f17092 修改楼梯边界膨胀的bug 2025-10-10 21:19:13 +08:00
8b5e2baf23 修改网格生成和路径规划中的bug 2025-10-10 18:42:21 +08:00
9ea89aa8d0 修改了膨胀算法,识别多层的边界进行膨胀。给每层增加了IsWalkable。 2025-10-10 14:54:47 +08:00
a4eaf46723 支持多层可视化,并解决楼梯下高度不足的区域网格可视化的问题 2025-10-10 11:51:05 +08:00
59ecebebc4 支持了3D障碍物投影 2025-10-10 11:03:38 +08:00
3cc840b183 删掉serena的提示词 2025-10-10 09:44:29 +08:00
163986f9e5 完善了安全优先和直线优先的算法,去掉了cell.WorldPosition,但高度层引起的问题还需要进一步完善 2025-10-10 02:45:20 +08:00
88712cc156 完善A*算法,用graph代替grid,用3D来进行路径规划。 2025-10-10 01:27:56 +08:00
ed7bc13866 为安全优先和直线优先增加高度层支持。 2025-10-09 19:07:34 +08:00
df6ba1c51e 改进了A*算法中对高度层的处理,初步做到路径优先下从楼面爬上楼梯(楼梯缺少障碍物网格) 2025-10-09 18:29:36 +08:00
9ccf925964 给网格增加了高度层概念,试图支持楼面上的楼梯,还不完善 2025-10-08 23:56:59 +08:00
a8e8760e2b 修改Gridmap缓存重建机制,考虑所有物流构件变化 2025-10-07 15:57:30 +08:00
f5d1361146 增加一个“无关项”的物流类型,用于过滤如地基、建筑结构等与物流无关的构件 2025-10-06 21:31:09 +08:00
8cd988279f 把碰撞报告保存到数据库,修改碰撞报告格式 2025-10-05 19:07:23 +08:00
e46931311f 将最大宽度、长度、高度、安全间隙、网格尺寸,加入路径保存内容。 2025-10-04 21:26:49 +08:00
6091b794de 实现了基本的路径分析功能,增加了文档关联的sqllite数据库 2025-10-03 16:28:02 +08:00
4357b91446 更新serena mcp的使用原则 2025-09-30 22:54:30 +08:00
2de531e98c 阶段二:合并动画管理器
合并策略:
- 保留 PathAnimationManager(实际使用的Transform-based动画引擎)
- 删除 LogisticsAnimationManager(SavedViewpoint功能完全未使用)
- 将碰撞排除列表缓存功能迁移到PathAnimationManager

变更详情:
1. PathAnimationManager.cs
   - 添加碰撞排除列表缓存管理字段
   - 迁移PrecomputeCollisionExclusions等5个方法
   - 添加using NavisworksTransport.Utils

2. AnimationControlViewModel.cs
   - 移除_logisticsAnimationManager字段
   - 将所有缓存调用改为_pathAnimationManager

3. StartAnimationCommand.cs
   - 移除未使用的LogisticsAnimationManager参数

4. 删除文件
   - src/Core/Animation/LogisticsAnimationManager.cs (542行)
   - 从NavisworksTransportPlugin.csproj移除引用

代码减少:约342行(542删除 - 200迁移)
编译验证: 成功

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-30 22:32:59 +08:00
5c98598311 阶段五:删除已集成的UI文档片段和添加清理总结
删除:
- path_visualization_ui.txt (内容已集成到SystemManagementView.xaml)

新增:
- doc/working/cleanup_summary.md (详细的清理总结报告)

清理总成果:
- 删除文件:5个
- 删除代码:约3753行
- 编译状态: 成功

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-30 22:22:50 +08:00
4ddaa0603d 阶段四:删除所有向后兼容代码
删除的旧版兼容代码:
1. PathAnimationManager.cs
   - AnimationCompleted事件声明 (line 143-144)
   - AnimationCompleted事件触发 (line 901-902)

2. ModelSplitterManager.cs
   - GenerateFileName旧版重载方法 (line 1583-1593)

3. LayerManagementViewModel.cs
   - GenerateFileName旧版重载方法 (line 1711-1721)

遵循"明确拒绝向后兼容性"原则,统一使用新版本API

编译验证通过

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-30 22:21:41 +08:00
52bb3da0eb 阶段三:删除未使用的WPF Services
删除的文件:
- src/UI/WPF/Services/DataBindingBestPractices.cs (0次外部引用)
- src/UI/WPF/Services/CrossViewModelSynchronizer.cs (仅被BestPractices使用)
- src/UI/WPF/Collections/VirtualizedObservableCollection.cs (仅被BestPractices引用)

更新:
- 从NavisworksTransportPlugin.csproj中移除引用

编译验证通过

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-30 22:20:29 +08:00
b048235657 阶段一:删除冗余的UIStateMachine
- 删除 src/Core/UIStateMachine.cs
- UIStateMachine和UIState枚举完全未使用
- 项目实际使用PathEditState作为状态管理
- 编译验证通过,无任何错误

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-30 22:17:48 +08:00
47ade72438 简化了自动寻路代码 2025-09-30 00:22:28 +08:00
64211439a8 修改了世界坐标转换到网格坐标(四舍五入) 2025-09-30 00:03:03 +08:00
3f2d66c255 测试和修改A*返回的坐标转换的问题 2025-09-29 23:25:21 +08:00
95bf6e839b 增加生成导航地图的功能,把当前路径场景保存成图片 2025-09-29 14:20:54 +08:00
e061ec4318 修改z值的判断为通道网格z值的最小和最大值 2025-09-29 10:48:21 +08:00
37e7a31a55 修改了障碍物高度判断条件 2025-09-28 20:37:57 +08:00
06faf04b83 用通道的高度范围+车辆高度+安全间隙作为过滤范围。 2025-09-28 20:10:59 +08:00
3b1fae6a1e 实现了路径在斜面(如楼梯)上的规划 2025-09-28 17:46:16 +08:00
8a95820fca 修改了通道网格生成的高度设置,能在斜面上生成网格,可视化也对了 2025-09-28 16:20:04 +08:00
cd74f857a2 对生成的网格地图进行了缓存,提高路径规划速度 2025-09-27 22:50:52 +08:00
c9b7acbd0a 增加过滤snappoints类型几何体 2025-09-24 02:35:30 +08:00
504a2c9862 扫描障碍物时,使用包围盒中心下面的通道网格z高度,进行高度范围筛选。
几何体除了三角形外,还有线形(也许有点、SnapPoints、文字等),要过滤掉,否则也被当成障碍物。
2025-09-24 02:25:25 +08:00
8c8ce89978 修改一个网格生成时的单位转换bug 2025-09-20 11:06:13 +08:00
328263e846 用所有通道顶面最小值作为障碍物扫描基准 2025-09-19 18:15:02 +08:00
1b95c37b80 修改碰撞检测报告到“查看碰撞报告”时,修复异步生成报告产生的崩溃风险。 2025-09-18 22:49:06 +08:00
6e20628bd2 优化路径可视化插件的参数初始化过程 2025-09-17 14:17:55 +08:00
7b11a91da0 修复通行空间可视化没有同步使用路径参数的bug 2025-09-17 12:34:16 +08:00
5dfa24be86 清理一些todo和无效文件和代码 2025-09-17 12:06:28 +08:00
f698322e35 简化自动寻路主函数 2025-09-17 10:53:12 +08:00
d19a5f4ae6 把系统管理中的空间通道高度设置去掉 2025-09-16 19:19:07 +08:00
6f1efe53af 网格生成把给所有网格设置高度区间放到门之前;修复寻路中高度检查的单位转换bug 2025-09-16 18:47:15 +08:00
8fbad77e65 删掉了以前的基于空间索引和垂直扫描的2.5D网格生成方法 2025-09-16 17:00:11 +08:00
5ef1fdc747 升级版本到0.12.0,完善物流属性列表的能力,实现同步选择视图、单个模型可见性、属性数值回填设置区 2025-09-16 12:27:30 +08:00
35226d2209 完善寻路的高度处理,增加路径点的选取高亮,增加删除起点和终点的保护 2025-09-16 10:36:22 +08:00
97b9c2beca 给网格全部加上高度,解决了门限高的问题。 2025-09-16 04:56:56 +08:00
15a3a29a28 可视化色系统一为Google Material Design配色,清除时也清除高亮 2025-09-15 22:58:32 +08:00
a3d1915dec 修复处理通道几何体错误去重的bug 2025-09-15 20:47:38 +08:00
034ca80db2 修复保存选择集使用旧方法引起的保存整个分层的bug 2025-09-15 19:36:01 +08:00
2cb9475847 清理一点多于代码 2025-09-15 18:33:48 +08:00
8a1e7b2614 修改步长和速度的计算,优化检测报告。 2025-09-15 16:41:24 +08:00
387ec332fc 修改碰撞检测报告的统计数据,解决被撞构件去重的问题 2025-09-15 11:20:58 +08:00
30d89b8ad7 修复显示两个检测报告窗口的bug 2025-09-15 01:11:00 +08:00
810f874a50 完善了网格地图的z坐标设置,改进了路径优化步骤,路径可以在台阶处保留2个路径点,更好贴合通道表面 2025-09-15 00:29:12 +08:00
8946873e32 网格点在设置时同步刷新 2025-09-14 21:24:51 +08:00
62349099aa 增加了空间通道方式的路径连线 2025-09-14 17:06:04 +08:00
d1185d986d 清理动画的无效代码 2025-09-14 03:08:12 +08:00
6510a0e124 不用每次动画结束都检测碰撞 2025-09-14 02:03:13 +08:00
403a7ac03b 完善timer机制 2025-09-14 01:23:05 +08:00
69336e2996 去掉碰撞结果的去重。把动画从idle改为timer 2025-09-13 22:31:31 +08:00
e295675fe5 修复自动路径起点和终点设置中,可以点击手动创建的bug 2025-09-13 14:47:07 +08:00
89c98f1556 把动画播放控制改成媒体控制按钮,增加了步进、快放,支持反向播放 2025-09-13 13:34:22 +08:00
9024eb2672 关闭程序时清理动画 2025-09-13 00:51:55 +08:00
cc8842dcd8 把动画改成步进式,在动画生成阶段进行快速碰撞计算。 2025-09-12 22:44:49 +08:00
0b0028c19c 增加了文档变更后,清理和初始化的功能 2025-09-12 17:36:26 +08:00
abda8a4a4d 将插件主控UI移到Views目录 2025-09-12 16:30:20 +08:00
d8b65342e1 彻底清理控件主窗口的代码 2025-09-12 12:50:24 +08:00
049673c6bb 清理类别设置和插件主窗口的无效代码 2025-09-12 11:54:15 +08:00
fc0b6d6aaa 简化可见性控制代码,改成工具类 2025-09-12 11:25:00 +08:00
0195d3e8ad 换用官方API示例,优化可见性控制。 2025-09-12 05:13:57 +08:00
468b3ef0e6 修改了分层导出的bug,提高了性能。 2025-09-12 03:21:29 +08:00
c098fb9b1f 优化类别设置中的显示控制性能 2025-09-11 17:45:09 +08:00
6c5400f172 优化分层属性读取,优化单独显示性能(可见性管理器) 2025-09-11 16:41:19 +08:00
f131d0f8b7 修复斜线路径优化有局部锯齿的情况 2025-09-11 12:38:31 +08:00
4e43fb89b3 统一使用网格左下角坐标系,与A*算法库保持一致 2025-09-11 03:14:02 +08:00
3b5d5963e5 增加门的可视化网格点 2025-09-11 02:34:47 +08:00
739392ef7b 增加了对门的处理,支持设置限宽 2025-09-10 20:08:36 +08:00
13bc16dd62 将可视化元素统一按网格大小,动态调整尺寸 2025-09-10 17:07:49 +08:00
5c21a8569b 修复边界没有膨胀的BUG 2025-09-10 16:23:41 +08:00
491ef09e66 修改路径优化的斜线离障碍网格安全距离不足的情况 2025-09-10 12:41:33 +08:00
4312a158c7 解决路径占用了障碍点的问题(坐标转换不一致) 2025-09-10 10:44:43 +08:00
7e68a3ea65 增加了路径斜线优化,效果非常好 2025-09-10 03:23:40 +08:00
d3feaa7fc0 增加安全优先路径算法(基础版,只计算中心距离) 2025-09-10 02:55:47 +08:00
95a4c444a6 增加安全优先路径算法 2025-09-10 02:55:23 +08:00
02b63111e0 清理网格生成器旧代码 2025-09-09 19:36:52 +08:00
baec804172 大幅提高网格地图生成性能(5倍以上) 2025-09-09 19:12:06 +08:00
b449cf08ad 修改经常崩溃的问题,最大原因是并行路径计算(步骤中调用了NW的API) 2025-09-09 13:16:28 +08:00
d046e31d6c 更新到0.11.0,增加功能和优化:
1. 局部直线优先路径算法 - 详细描述了算法原理、技术实现和效果对比
  2. 路径策略选择系统 - 涵盖了UI界面改进和多策略架构实现
  3. 网格可视化系统 - 描述了可视化功能和用户体验改进
  4. UI架构现代化 - 包含Idle事件机制和统一状态栏系统
  5. 内存管理与性能优化 - 涵盖COM API优化和碰撞算法改进
2025-09-09 02:30:10 +08:00
cd5dd3bf34 修改了物流属性参数不一致的情况,简化了COM API管理 2025-09-08 16:23:47 +08:00
642feb76a2 删除不需要的引用 2025-09-08 13:15:45 +08:00
0ec5989bd4 释放碰撞的COM API内存 2025-09-08 12:49:29 +08:00
235529315e 对COM API的使用进行了一些优化,释放内存 2025-09-08 12:38:41 +08:00
3732c6fa99 用Idle机制改造UI管理框架 2025-09-08 10:01:20 +08:00
9924c3b304 把动画从Timer改成Idle事件机制 2025-09-08 08:38:25 +08:00
bd74b42df3 增加路径优化算法建议方案 2025-09-08 07:58:08 +08:00
7d2edc9862 重构了各ViewModel的消息方法,抽取到ViewModelBase基类 2025-09-07 23:02:21 +08:00
83aad61147 将路径、动画、系统的消息也迁移到状态栏 2025-09-07 22:46:20 +08:00
3341ef82b7 将“分层管理”的消息提示和进度条迁移到状态栏 2025-09-07 16:23:09 +08:00
ba01624152 增加底部状态栏,统一提示消息和进度条显示 2025-09-07 15:52:56 +08:00
f32c367fd0 去掉了测试按钮,修改了环境检测按钮的位置 2025-09-07 13:18:38 +08:00
dd62a6dce4 换成了基于网格坐标的路径优化算法 2025-09-07 12:44:54 +08:00
1622d6cb90 清理一点过时代码 2025-09-07 02:46:02 +08:00
ceb37e33a4 增加网格可视化开关,路径优化不成功 2025-09-07 02:30:31 +08:00
9f42c6f381 修改网格点大小为自适应 2025-09-07 00:45:45 +08:00
eba60b23c7 修改了一些路径可视化的bug 2025-09-06 23:36:35 +08:00
fb8d52398b 修改了寻路算法穿洞的bug 2025-09-06 20:47:12 +08:00
289eff5554 增加了空洞和障碍物网格的可视化 2025-09-06 19:13:58 +08:00
e73cd2113e 通道网格用几何三角形精确计算,增加通行网格可视化 2025-09-06 16:26:39 +08:00
2955bfd38b 重构了一些重复的几何计算 2025-09-06 14:06:05 +08:00
101c929f15 用ClashDetective API的标准用法重构碰撞检测部分,增加了碰撞分组;
三维视图选点光标改成十字形,当失去焦点时,按空格键切换回来。
2025-09-06 04:13:12 +08:00
3ba3d328b8 增加了焦点捕捉功能,即使切换导航工具依然可以获取鼠标焦点,用于路径点设置 2025-09-05 17:49:20 +08:00
722e2ce9cc 修改UI更新的定时器线程安全导致崩溃问题 2025-09-05 12:59:11 +08:00
1f82eb814f 清理碰撞检测的无用代码 2025-09-05 02:31:11 +08:00
ca3a1e5ccf 用标准的ClashDetective的API重新碰撞部分。 2025-09-04 19:26:51 +08:00
41cac3dedd 重构了碰撞代码,抽取包围盒几何计算到Uitls 2025-09-04 17:14:04 +08:00
4411618662 升级版本 2025-09-04 14:35:35 +08:00
1d28c71cba 用SearchAPI来搜索CategoryAttributeManager中的FilterByLogisticsType()、FilterTraversableItems()等方法 2025-09-04 13:54:24 +08:00
e4771663b4 统一了物流属性查询用CategoryAttributeManager 2025-09-04 13:41:59 +08:00
d75582d664 用节点类型(是否只包含几何体)来进行节点包含判断,废掉包围盒的方式。 2025-09-04 12:31:29 +08:00
2cd3772105 将节点关系和几何体关系代码从动画管理器中抽取出来,形成工具类 2025-09-04 10:16:25 +08:00
8438d809ae 修复测试记录重复的BUG 2025-09-04 01:47:57 +08:00
d09ac6434b 增加包围盒为基础的2.5D网格生成方法。 2025-09-04 01:02:12 +08:00
0b27c609c3 增加了时间线和路径规划的UI原型 2025-09-03 14:09:04 +08:00
c40e1219a7 修改程序关闭崩溃的bug 2025-09-02 18:49:28 +08:00
3c1458245c 清理了一些过期代码 2025-09-01 21:42:19 +08:00
508c3e8e79 修改路径点预览效果和退出清理的bug 2025-09-01 18:36:23 +08:00
e72e581f85 去掉了传统路径算法 2025-09-01 15:40:20 +08:00
c71ae54ed0 对空间索引进行参数调试。 2025-09-01 11:41:32 +08:00
385815cd28 文档更新 2025-08-31 17:55:03 +08:00
f05a6c30d0 自动路径优化第二阶段完成,稳定性提高。 2025-08-31 17:51:53 +08:00
c3c1b8b994 网格生成第一阶段,空间索引优化,性能提高3倍 2025-08-31 16:46:01 +08:00
6893b7efeb 完善了碰撞报告 2025-08-31 15:45:21 +08:00
3df7124cf8 修复路径切换时后台数据和UI数据不同步的问题
问题:
- 当用户在路径列表中切换到另一条路径时,3D视图正确切换,路径点编辑列表也正确显示
- 但增加路径点和修改路径点操作仍使用上一个路径的数据,因为PathPlanningManager.CurrentRoute没有同步更新

修复:
- 在PathEditingViewModel.SelectedPathRoute属性setter中添加逻辑
- 当UI选中路径变化时,查找对应的Core路径对象并调用_pathPlanningManager.SetCurrentRoute()同步后台数据
- 确保UI路径切换和后台CurrentRoute保持一致,解决增加/修改路径点操作的数据不同步问题

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-31 11:30:32 +08:00
6efabb6dae 修复自动路径结束的状态bug 2025-08-31 00:50:08 +08:00
1ae3ace54e 增加了修改路径点的功能。 2025-08-30 23:14:19 +08:00
0f8728ca4a 完成了添加路径点的功能。 2025-08-30 21:58:42 +08:00
b7cbc64dd4 修复路径点插入位置问题,新路径点现在会插入到预览连线显示的正确位置
1. 在PathPlanningManager中添加_previewInsertIndex字段保存预览插入索引
2. 添加FindNearestLineSegmentWithIndex等方法计算最近线段和插入位置
3. 修改SetPreviewPoint方法,在设置预览点时计算并保存插入索引
4. 修改ConfirmPreviewPoint方法,使用Insert()而不是Add()进行插入操作
5. 修改ClearPreviewPoint方法,清除预览时也清除保存的插入索引

问题:之前预览连线工作正常能显示正确插入位置,但确认添加时新路径点被错误地添加到路径末尾
解决:现在新路径点会插入到预览时计算出的正确位置,确保路径点顺序符合用户期望

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-30 21:54:15 +08:00
1e046e1e4d 增加添加路径点功能,还差智能插入位置和预览连线功能。 2025-08-30 20:53:10 +08:00
5938c817a4 修改碰撞间隙不一致的问题,自动规划显示网格大小 2025-08-30 14:56:37 +08:00
f91d142bc7 采用包围体方法代替射线法进行垂直扫描,效果还可以。 2025-08-30 03:39:51 +08:00
eece385313 修复坐标计算的不一致和射线起点z坐标的问题 2025-08-29 23:01:15 +08:00
ea809277c3 开始实现2.5D空间网格,进行到空间哈希成功,垂直扫描失败 2025-08-29 20:43:02 +08:00
4dc989926e 优化了分层预览的遍历机制,提高性能 2025-08-28 18:31:57 +08:00
2b92e783bb 修改车辆和网格大小默认值,删掉分层的高程检测。 2025-08-28 14:50:41 +08:00
c9cd17c24a 清理临时的项目文件 2025-08-28 10:00:55 +08:00
2d1c835398 修复碰撞报告无结果的问题(碰撞需要找到容器节点) 2025-08-27 20:53:09 +08:00
0943637f5a 对动画性能做了优化,预先建缓存,把单步时间从5秒降到15毫秒。 2025-08-27 19:54:40 +08:00
065a9a2341 增加路径导出的文件格式:JSON和CSV,修改导出按钮文本 2025-08-27 17:55:12 +08:00
0ded3fca2e 实现了自定义分层属性的预览,完善了预览列表的可见性控制和保存开关,调整了文件名格式。 2025-08-27 17:18:52 +08:00
ad86c2ab76 修复分层预览功能中深度遍历逻辑不统一的问题
## 主要修复内容:

### 1. 创建统一的深度遍历核心函数
- 新增 GetItemsByDepthUnified() 方法实现精确的深度控制
- 确保所有策略使用相同的深度遍历逻辑
- 消除不同策略间深度处理的差异

### 2. 重构分层策略架构
- 引入 IGroupingStrategy 接口统一所有分层策略
- 创建 FloorDetectionStrategy、FloorAttributeStrategy、ZoneAttributeStrategy、SubSystemAttributeStrategy
- 所有策略现在接收完全相同的模型项集合

### 3. 修复智能楼层检测策略
- 新增 FloorDetector.DetectFloorsFromGivenItems() 方法
- FloorDetectionStrategy 不再依赖 FloorDetector 内部的深度逻辑
- 确保智能楼层检测使用与属性策略相同的模型项集合

### 4. 统一缓存和进度处理
- 更新缓存键生成使用统一的深度遍历函数
- 为所有策略添加详细的调试日志
- 标记所有分组项使用了统一深度遍历

## 解决的问题:
- 深度1级:现在所有策略基于相同的第一级节点集合
- 深度2级:现在所有策略基于相同的第一+二级节点集合
- 深度3级:现在所有策略基于相同的第一+二+三级节点集合
- 全部深度:现在所有策略基于相同的完整节点集合

## 预期结果:
当用户在二级节点设置3个不同属性值时,所有深度设置下的分层结果将完全一致。

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-27 13:28:43 +08:00
0de9de617f 完善了自定义分层属性,提供三种预定义属性 2025-08-27 02:37:25 +08:00
944f83bd7e 增加自定义分层设置,修改物流属性设置的问题。 2025-08-26 18:35:25 +08:00
2845f949e3 完善了碰撞报告和路径规划问题 2025-08-26 11:47:25 +08:00
099afd3f93 实现了日志管理,修复了碰撞自身的问题。 2025-08-21 14:27:49 +08:00
7a5aa413bc 增加了网格大小设置,修复了子对象被设成障碍物的bug 2025-08-20 20:52:26 +08:00
9c83af59ca 修改路径文件导入重复显示的问题 2025-08-20 19:24:06 +08:00
3012e4752f 修复了路径编辑的事件分发问题。 2025-08-20 14:33:47 +08:00
0e20be9e86 重新设计了路径可视化机制,重构了系统管理UI 2025-08-20 12:47:12 +08:00
1add8c6410 增加了同步UI刷新机制和定时器保底UI刷新机制 2025-08-19 23:35:36 +08:00
5d2ed56936 解决了有些操作需要UI实时更新的问题 2025-08-19 23:04:46 +08:00
9928dda6e3 把所有路径设置的功能移到对应子模块 2025-08-19 22:42:05 +08:00
ede5ac68c9 修改了碰撞间隙过大的问题 2025-08-19 19:02:36 +08:00
4d4889e9d9 实现了动画的设置、生成和播放 2025-08-19 17:49:30 +08:00
531e07f25d 更新了版本,删掉2个测试按钮 2025-08-19 01:00:32 +08:00
cddb7de71e 完成了分层导出的功能,还是线程安全的问题 2025-08-19 00:26:46 +08:00
3bdffc2b37 增加了文件导出为nwd 2025-08-18 23:55:01 +08:00
773e3e63ae 完成了UI架构重构,增加了一些测试。解决了测试框架依赖导致的启动崩溃问题。 2025-08-17 23:02:09 +08:00
2f86f70a80 修改了一些bug,设计了UI重构方案 2025-08-17 09:02:26 +08:00
0d918d32b5 更新了版本 2025-08-15 20:44:53 +08:00
720727a370 先过滤楼层,再进行规划 2025-08-15 20:34:04 +08:00
b261efcaae 用射线法计算通道表面高度 2025-08-15 15:31:41 +08:00
7d97dd1f86 视图解决z轴高度的问题(没有完成),顺便做了一些膨胀算法的优化 2025-08-15 12:56:33 +08:00
da28fe411a 实现了自动规划算法第一阶段 2025-08-14 18:33:43 +08:00
67a988286e 实现了自动路径规划算法的第一阶段,实现基础功能 2025-08-14 18:29:13 +08:00
a625a498a1 用DockPanePlugin代替AddInPlugin,控件UI使用WPF,属性管理和路径设置两个功能的迁移 2025-08-14 09:30:13 +08:00
4dc188f857 创建新的2026分支,进行迁移方案的设计,对项目结构重新整理,增加了视点动画的部分功能 2025-08-12 13:36:00 +08:00
553 changed files with 206479 additions and 19491 deletions

View File

@ -0,0 +1,56 @@
---
name: geometry-transform
description: NavisworksTransport 几何与姿态专用技能,用于坐标系变换、物体旋转平移、虚拟物体定位、通行空间几何和相关单元测试;处理 Navisworks 中的姿态问题时优先使用。
---
# Geometry & Transform
Use this skill for any work involving coordinate-system transforms, object pose, rotation, translation, virtual-object placement, passage-space geometry, and the tests that lock those behaviors down.
## Scope
- Keep host coordinates, internal canonical coordinates, and asset coordinates distinct.
- Prefer existing helpers over ad hoc math.
- Protect the object-start, restore, and playback chains from regressions.
- Treat geometry changes as test-first work whenever practical.
## Expected Ownership
- `src/Utils/CoordinateSystem/HostCoordinateAdapter.cs`
- `src/Utils/CoordinateSystem/ModelAxisConvention.cs`
- `src/Utils/CoordinateSystem/RotatedObjectExtentHelper.cs`
- `src/Utils/CoordinateSystem/CanonicalPlanarPoseBuilder.cs`
- `src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs`
- `src/Utils/CoordinateSystem/CanonicalTrackedPositionResolver.cs`
- `src/Utils/CoordinateSystem/RealObjectPlanarPoseSolver.cs`
- `src/Utils/CoordinateSystem/FragmentRepresentativePoseHelper.cs`
- `src/Utils/ModelItemTransformHelper.cs`
- `src/Core/Animation/PathAnimationManager.cs`
- `src/Core/VirtualObjectManager.cs`
- `src/UI/WPF/ViewModels/AnimationControlViewModel.cs`
- geometry-focused tests under `UnitTests/CoordinateSystem/`
## Invariants
- UI, logs, and user inputs/output stay in host coordinates.
- Internal pose solving stays in canonical space.
- Asset coordinates only apply to plugin-owned assets such as the virtual object and the unit cylinder/reference rod.
- Real-object planar pose solving must stay in host coordinates and should keep the reference-pose source injectable so fragment-derived representative pose can be wired in later.
- Real-object planar pose solving should prefer fragment-derived representative pose when available; original `Transform` is only an explicit fallback, not the primary source.
- Do not assume `BoundingBox.Center` is a stable pose anchor after rotation unless the flow explicitly proves it.
- Do not add temporary force-sync or fallback logic unless it is removed after the root cause is fixed.
- Do not mix virtual-object behavior into real-object behavior through shared mutable mode flags.
## Working Rules
1. Identify which coordinate system each value lives in before changing code.
2. Reuse project helpers first.
3. If a rotation change affects position, extents, or restore behavior, update the corresponding tests in the same change.
4. For Navisworks API details and examples, also consult the `nw-api` skill and `doc/design/2026/NavisworksAPI使用方法.md`.
## Test Expectations
- Add or update host-type coverage for both `YUp` and `ZUp` when the change touches transforms.
- Lock baseline pose, rotated pose, and restore behavior.
- For passage-space or footprint changes, verify extents after rotation, not just orientation.
- For virtual objects, verify scale preservation and CAD restore behavior.

View File

@ -0,0 +1,8 @@
interface:
display_name: "Geometry Transform"
short_description: "坐标、姿态、位移与通行空间专用技能"
brand_color: "#2E8B57"
default_prompt: "Use $geometry-transform to analyze or implement a geometry, transform, or coordinate-system fix in NavisworksTransport."
policy:
allow_implicit_invocation: true

View File

@ -0,0 +1,53 @@
# 几何问题排查清单
## 先问自己
1. 这是虚拟物体还是真实物体?
2. 路径类型是 `Ground / Hoisting / Rail` 哪一种?
3. 错的是:
- 旋转轴
- 起点位置
- 逐帧位置
- 通行空间
- 终点诊断
## 日志优先看什么
### 平面路径
- `[移动到起点] 路径方向yaw`
- `[动画姿态入口] ... 目标姿态`
- `[模型增量姿态] ... 当前/目标/增量`
### Rail
- `[移动到起点] Rail旋转姿态`
- `RailPathPoseHelper` 相关诊断
### 虚拟物体
- `[虚拟物体姿态] 应用前`
- `[虚拟物体姿态] 应用后`
- 关注 `BoundingBox.Center` 是否符合资源原点预期
## 需要同步检查的链
出现几何问题时,至少对齐这几条:
1. 起点落位
2. 逐帧位置
3. 通行空间
4. 终点诊断
如果其中一条使用的是“原始高度”,另一条使用的是“旋转后法线尺寸”,就很容易出现看起来互相矛盾的问题。
## 测试建议
至少补一个最短链测试:
- `YUp``ZUp`
- 指定单一路径类型
- 指定单个旋转轴
- 验证尺寸或姿态的关键不变量
能测数学层,就不要先靠现场猜。

View File

@ -0,0 +1,34 @@
# Navisworks Transform Patterns
## Use These First
- Use `HostCoordinateAdapter` for host <-> canonical coordinate conversion.
- Use `ModelAxisConvention` only when the object really has an asset axis convention.
- Use `RotatedObjectExtentHelper` for rotated footprint/height calculations.
- Use `CanonicalTrackedPositionResolver` for contact-point -> center-point conversion.
## Real Objects
- Real objects do not have their own asset coordinate system.
- Treat UI angle input as host-axis rotation.
- Keep the pose solve stable in canonical space, then apply the host-axis correction on the host-side result.
## Virtual Objects
- Virtual objects do have an asset coordinate system.
- Preserve current scale when moving or rotating the virtual object.
- When returning to CAD position, reset permanent transforms first, then restore the expected scale state.
## Common Pitfalls
- Do not use `BoundingBox.Center` as a stable anchor after rotation unless the flow has already been validated for that case.
- Do not infer height from the raw unrotated axis when the visual footprint already depends on the rotated extents.
- Do not mix host-axis correction and canonical correction in the same branch unless the branch semantics require it.
## Testing Checklist
- Verify `YUp` and `ZUp`.
- Verify zero-correction baseline first.
- Verify rotated extents and start-position compensation together.
- Verify that restore-to-CAD returns the object to the original state, not just an approximate pose.

View File

@ -0,0 +1,58 @@
# Navisworks 变换与旋转规则
本文件只记录和本项目最相关的结论。
## 关键结论
1. `Rotation` 不是“绕物体中心旋转”,而是和整体变换组合后共同决定结果。
2. `ResetPermanentTransform` 会清掉覆盖变换,后续 `BoundingBox()``Transform` 都会回到设计文件原始状态。
3. `OverridePermanentTransform``SetModelUnitsAndTransform` 会直接改变当前模型覆盖变换。
4. 对真实模型做完整姿态时,要特别区分:
- 目标姿态算得对不对
- 还是增量/绝对应用方式把正确姿态弄坏
5. 对虚拟物体做完整姿态时,若尺寸依赖缩放实现,应用姿态时必须明确是否保留当前缩放。
## 本项目里的常见坑
### 1. UI 轴和内部轴混用
- UI `X/Y/Z` 一律是宿主坐标系
- 内部坐标系只用于数学求解
- 不要把 UI 输入直接当成资产坐标系角度
### 2. 通行空间和真实物体使用不同尺寸语义
如果出现:
- 通行空间对,物体位置错
- 或物体对,通行空间错
优先检查:
- 是否一边用了原始高度
- 另一边用了旋转后的法线尺寸
### 3. 虚拟物体资源原点问题
若虚拟物体在“原点”时 `BoundingBox.Center` 不是 `(0,0,0)`
- 先检查 `unit_cube.nwc` 是否为最新资源
- 再查代码
不要一上来怀疑移动逻辑。
### 4. Rail 角度修正
`Rail` 不能像平面路径那样在宿主空间直接补转。
必须:
- 先并入 `canonical -> rail pose`
- 再落位
## 推荐排查顺序
1. 先确认资源文件是否正确
2. 再确认日志里的目标姿态/目标位置是否合理
3. 最后才看 Navisworks 应用变换的方法是否有问题

View File

@ -0,0 +1,139 @@
---
name: nw-api
description: Navisworks API 开发助手,用于开发 Navisworks 插件。功能包括:(1) 查看 NET API 文档位置和使用方法,(2) 查看官方 NET API 示例代码,(3) 搜索 Navisworks NET API 使用方法和示例。当用户询问 Navisworks API、插件开发、NET API、C# 开发相关内容时使用此技能。
---
# Navisworks NET API 开发助手
帮助开发 Autodesk Navisworks 插件的专用技能。主要基于 .NET APICOM API 作为补充参考。
## 本地文档位置
### API 文档(主要)
| 类型 | 路径 | 格式 |
|------|------|------|
| NET API | `doc/navisworks_api/NET/documentation/NET API.chm` | CHM 帮助文件 |
| NET API HTML | `doc/navisworks_api/NET/documentation/NetAPIHtml/` | HTML 文档 |
**推荐导航入口**: `doc/navisworks_api/NET/documentation/NetAPIHtml/index.html`
**原始 HTML 文档入口**: `doc/navisworks_api/NET/documentation/NetAPIHtml/html/index.html`
### API 文档搜索方法
HTML 文档已生成为可搜索的网页形式,可以直接用 Grep 工具搜索:
```bash
# 搜索 ModelItem 的属性
Grep -i "ModelItem.*property" doc/navisworks_api/NET/documentation/NetAPIHtml/html/
# 搜索特定方法
Grep -i "BoundingBox" doc/navisworks_api/NET/documentation/NetAPIHtml/html/
# 搜索类定义
Grep "class.*ModelItem" doc/navisworks_api/NET/documentation/NetAPIHtml/html/
```
**文件命名规律**:
- 类型页面: `T_Autodesk_Navisworks_Api_{ClassName}.htm`
- 属性页面: `P_Autodesk_Navisworks_Api_{ClassName}_{PropertyName}.htm`
- 方法页面: `M_Autodesk_Navisworks_Api_{ClassName}_{MethodName}.htm`
- 所有成员: `AllMembers_T_Autodesk_Navisworks_Api_{ClassName}.htm`
- 属性列表: `Properties_T_Autodesk_Navisworks_Api_{ClassName}.htm`
- 方法列表: `Methods_T_Autodesk_Navisworks_Api_{ClassName}.htm`
### 官方示例代码NET API
| 示例类型 | 路径 |
|----------|------|
| 基础插件 | `doc/navisworks_api/NET/examples/Basic Examples/CSharp/` |
| 插件集合 | `doc/navisworks_api/NET/examples/PlugIns/` |
| 自动化 | `doc/navisworks_api/NET/examples/Automation/` |
| 控件集成 | `doc/navisworks_api/NET/examples/Controls/` |
| 工具插件 | `doc/navisworks_api/NET/examples/Tools/` |
### 项目设计文档
- `doc/design/2026/NavisworksAPI使用方法.md` - API 使用指南
- `doc/migration/API_Migration_Analysis_2017_to_2026.md` - 版本迁移指南
- `doc/migration/Navisworks_2026_API_Changes.md` - 2026 API 变更
## COM API 参考(补充)
| 类型 | 路径 |
|------|------|
| COM API 文档 | `doc/navisworks_api/COM/documentation/NavisWorksCOM.chm` |
| COM 示例 | `doc/navisworks_api/COM/examples/` |
## 在线资源
- **Autodesk Platform Services**: <https://aps.autodesk.com/developer/overview/navisworks>
- **AEC DevBlog**: <https://adndevblog.typepad.com/aec/navisworks/>
- **ApiDocs (2017-2018)**: <https://apidocs.co/apps/navisworks/>
## 快速参考
### 命名空间
```csharp
// 核心 API
using Autodesk.Navisworks.Api;
using Autodesk.Navisworks.Api.Plugins;
using Autodesk.Navisworks.Api.Clash;
using Autodesk.Navisworks.Api.Timeliner;
// COM API需要时
using Autodesk.Navisworks.ComApi;
using Autodesk.Navisworks.Interop.ComApi;
```
### 插件类型
| 基类 | 用途 | 示例位置 |
|------|------|----------|
| `DockPanePlugin` | 停靠面板插件 | `Basic Examples/CSharp/BasicDockPanePlugin/` |
| `ToolPlugin` | 鼠标交互工具 | `PlugIns/InputAndRenderHandling/` |
| `RenderPlugin` | 3D 渲染插件 | `PlugIns/InputAndRenderHandling/` |
| `AddInPlugin` | 简单命令插件 | `PlugIns/Examiner/` |
| `EventWatcherPlugin` | 监听文档事件 | `PlugIns/ClashDetective/EventLog/` |
### 关键 API 入口
```csharp
// 当前文档
Document doc = Application.ActiveDocument;
// 当前选择
ModelItemCollection selection = doc.CurrentSelection.SelectedItems;
// 模型遍历
foreach (ModelItem item in doc.Models.RootItemDescendantsAndSelf) { }
// 搜索
Search search = new Search();
search.SearchConditions.Add(SearchCondition.HasCategoryByName(PropertyCategoryNames.Geometry));
ModelItemCollection results = search.FindAll(doc, false);
```
## 参考资料
- **常用 API 模式**: 见 `references/api-usage-patterns.md`
- **插件类型详解**: 见 `references/plugin-types.md`
- **NET 示例速查**: 见 `references/net-examples-guide.md`
## Web 搜索
如需查找最新的 API 用法或示例,使用 `SearchWeb` 工具搜索:
```
SearchWeb: "Navisworks .NET API {query} example C#"
```
常用搜索模板:
- `Navisworks .NET API ModelItem transform example`
- `Navisworks .NET API plugin DockPane C#`
- `Navisworks .NET API search condition example`
- `Navisworks 2026 .NET API changes`

View File

@ -0,0 +1,292 @@
# Navisworks API 常用模式
## 1. 模型遍历
### 遍历所有模型项
```csharp
// 获取所有模型项
IEnumerable<ModelItem> allItems =
Application.ActiveDocument.Models.RootItemDescendantsAndSelf;
// 遍历特定模型
foreach (Model model in document.Models)
{
foreach (ModelItem item in model.RootItem.DescendantsAndSelf)
{
// 处理每个模型项
}
}
```
### 获取子节点
```csharp
// 获取所有后代(不包括自己)
var childItems = selectedItem.DescendantsAndSelf.Where(x => x != selectedItem);
// 只获取直接子节点
foreach (ModelItem child in parentItem.Children) { }
// 向上遍历祖先
var current = selectedItem.Parent;
while (current != null)
{
current = current.Parent;
}
```
## 2. 搜索和查询
### 使用 Search 类
```csharp
Search search = new Search();
// 添加搜索条件
search.SearchConditions.Add(
SearchCondition.HasCategoryByName(PropertyCategoryNames.Geometry));
search.SearchConditions.Add(
SearchCondition.HasPropertyByName(PropertyCategoryNames.Item, DataPropertyNames.ItemHidden)
.EqualValue(VariantData.FromBoolean(false)));
// 设置搜索范围
search.Selection.SelectAll();
search.Locations = SearchLocations.DescendantsAndSelf;
// 执行搜索
ModelItemCollection results = search.FindAll(document, false);
```
### 使用 LINQ 查询
```csharp
var results = document.Models.RootItemDescendantsAndSelf
.Where(x => x.HasGeometry && !x.IsHidden)
.Where(x => x.ClassDisplayName.ToLower().Contains("wall"));
```
## 3. 选择操作
```csharp
// 获取当前选择
var currentSelection = document.CurrentSelection.SelectedItems;
// 清空选择
document.CurrentSelection.Clear();
// 添加到选择
document.CurrentSelection.Add(modelItem);
// 复制集合到选择
document.CurrentSelection.CopyFrom(modelItems);
```
## 4. 可见性控制
```csharp
// 隐藏项目
ModelItemCollection itemsToHide = new ModelItemCollection();
itemsToHide.Add(modelItem);
document.Models.SetHidden(itemsToHide, true);
// 显示项目
document.Models.SetHidden(itemsToHide, false);
// 检查是否隐藏
if (modelItem.IsHidden) { }
```
## 5. 文件导出
```csharp
// 保存 NWD 文件
document.SaveFile(filePath);
// 使用导出选项
var exportOptions = new NwdExportOptions();
exportOptions.ExcludeHiddenItems = true;
exportOptions.EmbedXrefs = false;
exportOptions.PreventObjectPropertyExport = false;
document.ExportToNwd(saveFilePath, exportOptions);
```
## 6. Transform 变换操作
### 核心概念
- `ModelItem.Transform` - 返回设计文件中的原始变换(只读)
- `OverridePermanentTransform()` - 应用增量变换(累积)
- `ResetPermanentTransform()` - 清除所有增量变换
- `ModelItem.BoundingBox()` - 返回当前实际显示的包围盒
### 应用变换
```csharp
// 获取原始 Transform
Transform3D originalTransform = modelItem.Transform;
// 应用增量变换
var doc = Application.ActiveDocument;
var modelItems = new ModelItemCollection { modelItem };
doc.Models.OverridePermanentTransform(modelItems, newTransform, false);
// 重置到原始位置
doc.Models.ResetPermanentTransform(modelItems);
```
### 绕物体中心旋转(需要补偿)
```csharp
// 关键API 的旋转总是绕世界原点(0,0,0)
// 要实现绕物体中心旋转,需要计算补偿
double deltaYaw = newYaw - currentYaw;
double cos = Math.Cos(deltaYaw);
double sin = Math.Sin(deltaYaw);
// 计算绕原点旋转后的位置
double rotatedX = currentPosition.X * cos - currentPosition.Y * sin;
double rotatedY = currentPosition.X * sin + currentPosition.Y * cos;
// 计算补偿平移
var compensatedTranslation = new Vector3D(
newPosition.X - rotatedX,
newPosition.Y - rotatedY,
newPosition.Z - currentPosition.Z
);
// 组合变换
var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0));
var components = identity.Factor();
components.Rotation = new Rotation3D(new UnitVector3D(0, 0, 1), deltaYaw);
components.Translation = compensatedTranslation;
var incrementalTransform = components.Combine();
doc.Models.OverridePermanentTransform(modelItems, incrementalTransform, false);
```
## 7. 属性访问
### NET API 方式
```csharp
// 查找属性分类
var category = item.PropertyCategories.FindCategoryByDisplayName("Material");
// 基础属性
string displayName = item.DisplayName;
string className = item.ClassName;
bool hasGeometry = item.HasGeometry;
bool isHidden = item.IsHidden;
```
### COM API 方式(当 NET API 不满足需求时使用)
**参考**: `doc/navisworks_api/NET/examples/Basic Examples/CSharp/APICallsCOMPlugin/`
```csharp
using Autodesk.Navisworks.ComApi;
using Autodesk.Navisworks.Interop.ComApi;
// 获取 COM 状态
ComApi.InwOpState10 state = ComApiBridge.State;
// 获取属性节点
InwOpSelection2 selection = state.CurrentSelection as InwOpSelection2;
InwGUIPropertyNode2 propertyNode =
state.GetGUIPropertyNode(selection.Paths()[1], true) as InwGUIPropertyNode2;
// 遍历属性分类
foreach (InwGUIAttribute2 guiAttribute in propertyNode.GUIAttributes())
{
foreach (InwOaProperty property in guiAttribute.Properties())
{
string name = property.name;
string value = property.value;
}
}
```
### 添加自定义属性COM API
```csharp
InwOaPropertyVec propertyVector =
state.ObjectFactory(nwEObjectType.eObjectType_nwOaPropertyVec);
InwOaProperty property =
state.ObjectFactory(nwEObjectType.eObjectType_nwOaProperty);
property.name = "CustomProperty1";
property.UserName = "自定义属性1";
property.value = value;
propertyVector.Properties().Add(property);
propertyNode.SetUserDefined(0, categoryName, internalName, propertyVector);
```
## 8. 线程安全 - 关键
**所有 Navisworks API 调用必须在主 UI 线程中执行**
```csharp
// ❌ 错误:在后台线程调用
await Task.Run(() =>
{
var document = Application.ActiveDocument; // 可能崩溃
});
// ✅ 正确:使用 Dispatcher.Invoke
await Task.Run(() =>
{
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
var document = Application.ActiveDocument;
document.ExportToNwd(path, options); // 安全执行
});
});
```
### 检查线程状态
```csharp
var apartmentState = System.Threading.Thread.CurrentThread.GetApartmentState();
bool isMainThread = System.Windows.Application.Current.Dispatcher.CheckAccess();
```
## 9. 碰撞检测 (ClashDetective)
```csharp
// 获取碰撞测试
document.ClashTestsData.Tests;
// 创建碰撞选择
var selectionA = new ClashSelection(modelItemsA, document);
var selectionB = new ClashSelection(modelItemsB, document);
// 创建碰撞测试
var clashTest = new ClashTest();
clashTest.Name = "Test Name";
clashTest.TestType = ClashTestType.Hard;
clashTest.SelectionA = selectionA;
clashTest.SelectionB = selectionB;
```
## 10. TimeLiner 集成
```csharp
// 获取 TimeLiner 数据
var timeliner = document.Timeliner;
// 添加任务
var task = new TimelinerTask();
task.Name = "Task Name";
task.SimulationStatus = SimulationStatus.Active;
task.StartDate = DateTime.Now;
task.EndDate = DateTime.Now.AddDays(7);
timeliner.Tasks.Add(task);
// 关联模型项
task.Selection.CopyFrom(modelItems);
```

View File

@ -0,0 +1,309 @@
# Navisworks NET API 示例指南
## 示例代码位置总览
所有示例位于:`doc/navisworks_api/NET/examples/`
## 基础示例 (Basic Examples)
### 1. BasicDockPanePlugin
**路径**: `Basic Examples/CSharp/BasicDockPanePlugin/`
最简单的停靠面板插件示例。
```csharp
[Plugin("BasicDockPanePlugin.BasicDockPanePlugin", "ADSK",
DisplayName = "BasicDockPanePlugin",
ToolTip = "Basic Docking Pane Plugin")]
[DockPanePlugin(100, 300)]
public class BasicDockPanePlugin : DockPanePlugin
{
public override Control CreateControlPane()
{
HelloWorldControl control = new HelloWorldControl();
control.Dock = DockStyle.Fill;
control.CreateControl();
return control;
}
public override void DestroyControlPane(Control pane)
{
pane.Dispose();
}
}
```
### 2. WPF DockPane
**路径**: `Basic Examples/CSharp/WPF/WpfDockPane/`
使用 WPF 控件的停靠面板插件。
```csharp
public override Control CreateControlPane()
{
// 使用 ElementHost 托管 WPF 控件
ElementHost host = new ElementHost();
host.Child = new WPFHelloWorldControl();
host.Dock = DockStyle.Fill;
host.CreateControl();
return host;
}
```
### 3. DatabaseDockPane
**路径**: `Basic Examples/CSharp/WPF/DatabaseDockPane/`
展示如何在停靠面板中使用模型数据库。
### 4. BasicPlugIn
**路径**: `Basic Examples/CSharp/BasicPlugIn/`
最简单的 AddIn 插件示例。
### 5. APICallsCOMPlugin
**路径**: `Basic Examples/CSharp/APICallsCOMPlugin/`
展示如何在 NET API 插件中调用 COM API。
### 6. CustomRibbon
**路径**: `Basic Examples/CSharp/CustomRibbon/`
自定义 Ribbon 界面示例,使用 XAML 定义 Ribbon。
## 插件示例 (PlugIns)
### 1. Examiner
**路径**: `PlugIns/Examiner/`
全面的 LINQ 搜索和模型操作示例。
**关键功能**:
- 使用 LINQ 查询模型项
- 属性搜索和过滤
- 颜色和透明度覆盖
- Required/Hidden 状态修改
- 导出结果到 NWD
**核心代码片段**:
```csharp
// LINQ 搜索示例
IEnumerable<ModelItem> result =
Application.ActiveDocument.Models.RootItemDescendantsAndSelf
.Where(x => x.HasGeometry)
.Where(x => !x.IsHidden)
.Where(x => x.DisplayName.Contains(searchName));
// 颜色覆盖
ModelItemCollection items = new ModelItemCollection();
items.AddRange(result);
Application.ActiveDocument.Models.OverridePermanentColor(items, color);
```
### 2. SearchComparisonPlugIn
**路径**: `PlugIns/SearchComparisonPlugIn/`
对比不同搜索方法的性能。
### 3. InputAndRenderHandling
**路径**: `PlugIns/InputAndRenderHandling/`
ToolPlugin 和 RenderPlugin 的综合示例。
**关键功能**:
- 鼠标输入处理
- 自定义渲染
- 3D 拾取
```csharp
[ToolPlugin("InputAndRenderHandling", "ADSK")]
[RenderPlugin("InputAndRenderHandling", "ADSK")]
public class InputAndRenderHandling : ToolPlugin
{
public override bool MouseDown(MouseButton button, int x, int y)
{
// 处理鼠标点击
return true;
}
public override void Render(View view, RenderContext context)
{
// 自定义渲染
}
}
```
### 4. ClashDetective 示例集合
**路径**: `PlugIns/ClashDetective/`
包含多个碰撞检测相关示例:
| 示例 | 说明 |
|------|------|
| `ClashGrouper/` | 碰撞结果分组 |
| `ClashMarkers/` | 碰撞标记显示 |
| `EventLog/` | 事件日志监听 |
| `GenerateMatrix/` | 生成碰撞矩阵 |
| `SimpleUI/` | 简单 UI 界面 |
**碰撞测试基础代码**:
```csharp
// 创建碰撞测试
ClashTest clashTest = new ClashTest();
clashTest.Name = "Test Name";
clashTest.TestType = ClashTestType.Hard;
// 设置选择
clashTest.SelectionA = new ClashSelection(itemsA, document);
clashTest.SelectionB = new ClashSelection(itemsB, document);
// 添加到文档
document.ClashTestsData.Tests.Add(clashTest);
```
### 5. Timeliner 示例
**路径**: `PlugIns/Timeliner/`
TimeLiner 集成示例,展示 4D 模拟功能。
```csharp
// 获取 Timeliner 数据
var timeliner = document.Timeliner;
// 创建任务
TimelinerTask task = new TimelinerTask();
task.Name = "Construction Task";
task.StartDate = DateTime.Now;
task.EndDate = DateTime.Now.AddDays(7);
task.SimulationStatus = SimulationStatus.Active;
// 关联模型
task.Selection.CopyFrom(modelItems);
// 添加任务
timeliner.Tasks.Add(task);
```
### 6. Takeoff 示例
**路径**: `PlugIns/Takeoff/`
工程量计算 (Quantification) 示例。
## 自动化示例 (Automation)
### 1. CallExaminer
**路径**: `Automation/CallExaminer/`
展示如何通过自动化接口调用 Examiner 插件。
```csharp
// 启动 Navisworks 自动化
NavisworksApplication app = new NavisworksApplication();
app.OpenFile(filePath);
// 执行插件
app.ExecutePlugin("Examiner.Examiner", parameters);
```
### 2. MessageClientServer
**路径**: `Automation/MessageClientServer/`
展示插件间通信机制。
### 3. MessageSenderReceiver
**路径**: `Automation/MessageSenderReceiver/`
消息发送和接收示例。
## 控件示例 (Controls)
### 1. Viewers
**路径**: `Controls/Viewers/`
独立的 Navisworks 查看器控件示例。
| 示例 | 说明 |
|------|------|
| `SDIViewer/` | 单文档界面查看器 |
| `MDIViewer/` | 多文档界面查看器 |
```csharp
// 使用 DocumentControl
DocumentControl docControl = new DocumentControl();
docControl.Dock = DockStyle.Fill;
this.Controls.Add(docControl);
// 打开文件
docControl.Document.OpenFile(filePath);
```
### 2. PublishFile
**路径**: `Controls/PublishFile/`
文件发布选项示例。
## 工具示例 (Tools)
### 1. AppInfo
**路径**: `Tools/AppInfo/`
展示应用程序信息和事件监听。
### 2. CodeRun
**路径**: `Tools/CodeRun/`
动态代码执行工具,支持 C# 和 IronPython。
## 按功能查找示例
| 功能需求 | 推荐示例 |
|----------|----------|
| 创建停靠面板 | `BasicDockPanePlugin/`, `WpfDockPane/` |
| 使用 WPF | `WpfDockPane/`, `DatabaseDockPane/` |
| 模型搜索 | `Examiner/`, `SearchComparisonPlugIn/` |
| 鼠标交互 | `InputAndRenderHandling/` |
| 自定义渲染 | `InputAndRenderHandling/` |
| 碰撞检测 | `ClashDetective/` 下的各个示例 |
| TimeLiner | `Timeliner/` |
| 独立查看器 | `Controls/Viewers/` |
| 插件通信 | `MessageClientServer/` |
## 项目文件参考
每个示例包含的项目文件:
- `.csproj` - C# 项目文件
- `Properties/AssemblyInfo.cs` - 程序集信息
- 源代码文件 (.cs)
## 构建和部署
1. 打开 `doc/navisworks_api/NET/examples/Examples.sln`
2. 选择项目并构建
3. 插件输出到 Navisworks 插件目录:
```
C:\ProgramData\Autodesk\Navisworks Manage 2026\plugins\
```

View File

@ -0,0 +1,286 @@
# Navisworks 插件类型详解
## 插件基类对比
| 基类 | 继承关系 | 主要用途 | 触发方式 |
|------|---------|---------|---------|
| `DockPanePlugin` | ← `Plugin` | 停靠面板 UI | 用户点击 Ribbon 按钮 |
| `ToolPlugin` | ← `Plugin` | 鼠标 3D 交互工具 | 用户点击后进入工具模式 |
| `RenderPlugin` | ← `Plugin` | 自定义 3D 渲染 | 自动在渲染时调用 |
| `AddInPlugin` | ← `Plugin` | 简单命令 | 用户点击 Ribbon 按钮 |
| `EventWatcherPlugin` | ← `Plugin` | 监听文档事件 | 自动注册,事件触发 |
| `CommandHandlerPlugin` | - | 自定义命令处理器 | 命令调用时触发 |
## 1. DockPanePlugin
用于创建停靠在 Navisworks 窗口中的面板。
```csharp
[DockPanePlugin(400, 600, FixedSize = false)]
[Plugin("PluginName", "DeveloperID", DisplayName = "Display Name")]
[RibbonLayout("RibbonTabName")]
[RibbonTab("Home", DisplayName = "Home")]
[Command("CommandName", DisplayName = "Open Panel", Icon = "icon.png")]
public class MyDockPanePlugin : DockPanePlugin
{
public override Control CreateControlPane()
{
// 返回 Windows Forms 或 WPF 控件
var host = new ElementHost();
host.Child = new MyWpfUserControl();
host.Dock = DockStyle.Fill;
return host;
}
public override void DestroyControlPane(Control control)
{
control?.Dispose();
}
}
```
## 2. ToolPlugin
用于创建需要与 3D 视图交互的工具。
```csharp
[ToolPlugin("ToolName", "DeveloperID", DisplayName = "Tool Display Name")]
[Plugin("PluginName", "DeveloperID")]
[RibbonLayout("RibbonTabName")]
[RibbonTab("Home", DisplayName = "Home")]
[Command("CommandName", DisplayName = "Activate Tool", Icon = "icon.png")]
public class MyToolPlugin : ToolPlugin
{
public override bool MouseDown(MouseButton button, int x, int y)
{
// 处理鼠标按下
// 返回 true 表示已处理事件
return true;
}
public override bool MouseMove(int x, int y)
{
// 处理鼠标移动
return true;
}
public override bool MouseUp(MouseButton button, int x, int y)
{
// 处理鼠标释放
return true;
}
public override void OnActivate()
{
// 工具激活时调用
base.OnActivate();
}
public override void OnDeactivate()
{
// 工具停用时调用
base.OnDeactivate();
}
}
```
### 获取 3D 点击坐标
```csharp
public override bool MouseDown(MouseButton button, int x, int y)
{
if (button == MouseButton.Left)
{
// 将屏幕坐标转换为 3D 射线
var view = Application.ActiveDocument.CurrentViewpoint;
var pickResult = view.PickItemFromPoint(x, y);
if (pickResult != null)
{
Point3D point = pickResult.Point;
// 使用点击的 3D 坐标
}
}
return true;
}
```
## 3. RenderPlugin
用于在 3D 视图中渲染自定义图形。
```csharp
[RenderPlugin("RenderPluginName", "DeveloperID")]
[Plugin("PluginName", "DeveloperID")]
public class MyRenderPlugin : RenderPlugin
{
public override void Render(View view, RenderContext context)
{
// 在这里进行自定义渲染
// 使用 context 绘制线条、点、文本等
// 示例:绘制一条红线
var points = new List<Point3D>
{
new Point3D(0, 0, 0),
new Point3D(10, 10, 10)
};
context.DrawLines(points, new Color(1, 0, 0), 2.0f);
}
}
```
## 4. AddInPlugin
最简单的插件类型,用于执行一次性命令。
```csharp
[AddInPlugin("AddInName", "DeveloperID", DisplayName = "Command Name")]
[RibbonTab("Home", DisplayName = "Home")]
[Command("CommandName", DisplayName = "Run Command", Icon = "icon.png")]
public class MyAddInPlugin : AddInPlugin
{
public override int Execute(params string[] parameters)
{
// 执行命令逻辑
MessageBox.Show("Hello from AddIn!");
return 0; // 返回 0 表示成功
}
}
```
## 5. EventWatcherPlugin
用于监听文档事件。
```csharp
[EventWatcherPlugin("EventWatcherName", "DeveloperID")]
[Plugin("PluginName", "DeveloperID")]
public class MyEventWatcher : EventWatcherPlugin
{
public override void OnDocumentChanged(object sender, DocumentEventArgs e)
{
// 文档变更时触发
}
public override void OnSelectionChanged(object sender, EventArgs e)
{
// 选择变更时触发
}
public override void OnViewChanged(object sender, ViewEventArgs e)
{
// 视图变更时触发
}
}
```
## 插件属性详解
### Plugin 属性
```csharp
[Plugin("PluginName", "DeveloperID",
DisplayName = "显示名称",
ToolTip = "鼠标悬停提示",
LoadForCanExecute = true)]
```
### RibbonTab 属性
```csharp
[RibbonTab("TabName", DisplayName = "Tab 显示名称")]
```
### RibbonLayout 属性
```csharp
[RibbonLayout("LayoutName")]
```
### Command 属性
```csharp
[Command("CommandName",
DisplayName = "命令显示名称",
ToolTip = "命令提示",
Icon = "icon.png",
LargeIcon = "icon_large.png",
Shortcut = "Ctrl+Shift+X")]
```
### DockPanePlugin 属性
```csharp
[DockPanePlugin(width, height,
FixedSize = false, // 是否固定大小
MinimumWidth = 200, // 最小宽度
MinimumHeight = 300, // 最小高度
HasDockingFrames = true)] // 是否有停靠框架
```
## COM 插件注册
COM 插件需要注册到系统注册表。
### 1. 为程序集添加 GUID
```csharp
// 在 AssemblyInfo.cs 中
[assembly: Guid("YOUR-GUID-HERE")]
// 在插件类上添加 ProgId
[ProgId("YourCompany.YourPlugin")]
public class YourPlugin : DockPanePlugin { }
```
### 2. 注册插件
```bash
# 使用 Visual Studio 自动注册(开发时)
# 或在注册表中手动添加:
# HKLM\SOFTWARE\Autodesk\Navisworks Manage\23.0\COM Plugins
# 创建字符串值,名称为 ProgId值为空
# 或使用 nwregasm.bat
nwregasm.bat "path\to\your\plugin.dll"
```
## 项目文件配置
### 必要的引用
```xml
<!-- Navisworks API -->
<Reference Include="Autodesk.Navisworks.Api" />
<Reference Include="Autodesk.Navisworks.ComApi" />
<Reference Include="Autodesk.Navisworks.Interop.ComApi" />
<Reference Include="Autodesk.Navisworks.Timeliner" />
<Reference Include="Autodesk.Navisworks.Clash" />
<Reference Include="Autodesk.Navisworks.Controls" />
```
### 输出路径
插件应该输出到 Navisworks 插件目录:
```
C:\ProgramData\Autodesk\Navisworks Manage 2026\plugins\YourPluginName\
```
## 调试插件
1. 设置启动外部程序:
- 路径:`C:\Program Files\Autodesk\Navisworks Manage 2026\Roamer.exe`
2. 附加到进程调试:
- 在 Visual Studio 中选择 "附加到进程"
- 选择 `Roamer.exe`
3. 日志输出:
```csharp
// 使用 System.Diagnostics.Debug
System.Diagnostics.Debug.WriteLine("Debug message");
```

View File

@ -0,0 +1,48 @@
# Project Tools Skill
## 概述
本 skill 用于指导 NavisworksTransport 项目中工具类的正确使用。
## 文件结构
```
project-tools/
├── SKILL.md # 主文档:工具类总览和快速参考
├── README.md # 本文件
├── utils/ # 各工具类详细文档
│ ├── DialogHelper.md # 对话框工具
│ ├── UnitsConverter.md # 单位转换工具
│ ├── LogManager.md # 日志工具
│ └── GeometryHelper.md # 几何计算工具
└── examples/ # 使用示例
└── common-usage.md # 常见场景示例
```
## 使用方法
1. **编写代码前**:查看 `SKILL.md` 确定需要的工具类
2. **具体使用时**:查看 `utils/` 下的详细文档
3. **参考示例**:查看 `examples/` 下的实际代码
## 核心原则
> **优先复用现有工具类,禁止重复造轮子**
### 必须使用的工具类(强制)
| 场景 | 工具类 | 错误示例 |
|------|--------|----------|
| 对话框Owner设置 | DialogHelper | `dialog.Owner = Application.Current.MainWindow` |
| 单位转换 | UnitsConverter | `value * 0.001` |
| 日志记录 | LogManager | `Console.WriteLine(...)` |
| 几何计算 | GeometryHelper | `Math.Sqrt(dx*dx + dy*dy)` |
## 检查清单
编写代码前,请确认:
- [ ] 阅读了 `SKILL.md` 中相关工具类的说明
- [ ] 查阅了 `utils/` 下的详细文档
- [ ] 代码中使用了正确的工具类方法
- [ ] 没有重复实现已有功能

View File

@ -0,0 +1,157 @@
# NavisworksTransport 项目工具类使用指南
## 概述
本skill记录 NavisworksTransport 项目中的所有工具类及其使用方法。在编写代码前,请务必查看此文档,**优先使用现有工具类,避免重复造轮子**。
## 工具类总览
### 核心工具类(⭐⭐⭐ 必须)
| 工具类 | 文件 | 用途 |
|--------|------|------|
| **DialogHelper** | `src/Utils/DialogHelper.cs` | 对话框Owner设置和置顶显示 |
| **UnitsConverter** | `src/Utils/UnitsConverter.cs` | 单位转换(米↔模型单位) |
| **LogManager** | `src/Utils/LogManager.cs` | 日志记录 |
| **GeometryHelper** | `src/Utils/GeometryHelper.cs` | 3D几何计算 |
| **PathHelper** | `src/Utils/PathHelper.cs` | 文件路径相关工具 |
### 重要工具类(⭐⭐ 推荐)
| 工具类 | 文件 | 用途 |
|--------|------|------|
| **CoordinateConverter** | `src/Utils/CoordinateConverter.cs` | 2D地图↔3D世界坐标转换 |
| **ModelHighlightHelper** | `src/Utils/ModelHighlightHelper.cs` | 模型高亮显示 |
| **NavisworksSelectionHelper** | `src/Utils/NavisworksSelectionHelper.cs` | 选择集操作 |
| **ViewpointHelper** | `src/Utils/ViewpointHelper.cs` | 视点操作 |
| **NavisworksApiHelper** | `src/Utils/NavisworksApiHelper.cs` | API通用工具 |
| **VisibilityHelper** | `src/Utils/VisibilityHelper.cs` | 可见性管理 |
### 专项工具类(⭐ 按需使用)
| 工具类 | 文件 | 用途 |
|--------|------|------|
| **SectionClipHelper** | `src/Utils/SectionClipHelper.cs` | 剖切操作(性能优化) |
| **BoundingBoxGeometryUtils** | `src/Utils/BoundingBoxGeometryUtils.cs` | 包围盒几何计算 |
| **FloorDetector** | `src/Utils/FloorDetector.cs` | 楼层检测 |
| **GeometryCacheManager** | `src/Utils/GeometryCacheManager.cs` | 几何缓存管理 |
## 快速参考
### 对话框开发
```csharp
// ❌ 错误直接设置Owner可能在Navisworks环境中失败
var dialog = new MyDialog();
dialog.Owner = Application.Current.MainWindow; // 可能抛出异常!
dialog.Show();
// ✅ 正确:使用 DialogHelper
var dialog = new MyDialog();
DialogHelper.SetOwnerSafely(dialog); // 自动处理Navisworks环境
dialog.Show();
```
### 单位转换
```csharp
// ❌ 错误:硬编码转换因子
double meters = modelUnits * 0.001; // 错误!不同文档单位不同
// ✅ 正确:使用 UnitsConverter
double meters = UnitsConverter.ConvertToMeters(modelUnits);
double modelUnits = UnitsConverter.ConvertFromMeters(meters);
```
### 日志记录
```csharp
// ❌ 错误使用Console或直接输出
Console.WriteLine($"Error: {ex.Message}"); // 不可见!
// ✅ 正确:使用 LogManager
LogManager.Info("操作成功");
LogManager.Warning("警告信息");
LogManager.Error("错误信息", exception);
```
### 几何计算
```csharp
// ❌ 错误:自己实现距离计算
double dx = p2.X - p1.X;
double dy = p2.Y - p1.Y;
double distance = Math.Sqrt(dx*dx + dy*dy);
// ✅ 正确:使用 GeometryHelper
double distance = GeometryHelper.Distance(p1, p2);
double angle = GeometryHelper.AngleBetweenDegrees(v1, v2);
```
### 模型高亮
```csharp
// ✅ 使用 ModelHighlightHelper 高亮对象
ModelHighlightHelper.HighlightItems("myCategory", modelItems);
ModelHighlightHelper.ClearCategory("myCategory");
ModelHighlightHelper.ClearAllHighlights();
```
### 视角调整
```csharp
// ✅ 使用 ViewpointHelper 调整视角
ViewpointHelper.AdjustViewpointToPathCenter(path);
ViewpointHelper.FocusOnModelItem(modelItem, 45, 0.25);
Viewpoint saved = ViewpointHelper.SaveCurrentViewpoint();
ViewpointHelper.RestoreViewpoint(saved);
```
## 详细文档
- [DialogHelper 使用指南](utils/DialogHelper.md) - 对话框Owner设置和置顶显示
- [UnitsConverter 使用指南](utils/UnitsConverter.md) - 单位转换(极其重要)
- [LogManager 使用指南](utils/LogManager.md) - 日志记录
- [GeometryHelper 使用指南](utils/GeometryHelper.md) - 3D几何计算
- [PathHelper 使用指南](utils/PathHelper.md) - 文件路径工具
- [CoordinateConverter 使用指南](utils/CoordinateConverter.md) - 2D/3D坐标转换
- [ModelHighlightHelper 使用指南](utils/ModelHighlightHelper.md) - 模型高亮
- [NavisworksSelectionHelper 使用指南](utils/NavisworksSelectionHelper.md) - 选择集操作
- [ViewpointHelper 使用指南](utils/ViewpointHelper.md) - 视点操作
- [SectionClipHelper 使用指南](utils/SectionClipHelper.md) - 剖切操作
- [NavisworksApiHelper 使用指南](utils/NavisworksApiHelper.md) - API通用工具
- [VisibilityHelper 使用指南](utils/VisibilityHelper.md) - 可见性管理
## 使用检查清单
在编写新功能前,请检查:
- [ ] 需要显示对话框?→ 使用 **DialogHelper**
- [ ] 涉及单位转换?→ 使用 **UnitsConverter**
- [ ] 需要记录日志?→ 使用 **LogManager**
- [ ] 需要几何计算?→ 使用 **GeometryHelper**
- [ ] 需要高亮模型?→ 使用 **ModelHighlightHelper**
- [ ] 需要处理选择集?→ 使用 **NavisworksSelectionHelper**
- [ ] 需要调整视角?→ 使用 **ViewpointHelper**
- [ ] 需要坐标转换?→ 使用 **CoordinateConverter**
- [ ] 需要文件路径操作?→ 使用 **PathHelper**
- [ ] 需要剖切优化?→ 使用 **SectionClipHelper**
- [ ] 需要显示/隐藏对象?→ 使用 **VisibilityHelper**
- [ ] 需要线程安全操作?→ 使用 **NavisworksApiHelper**
## 禁止行为
以下行为在项目中是**禁止**的:
1. ❌ 直接设置 `Window.Owner = Application.Current.MainWindow`
2. ❌ 硬编码单位转换因子(如 `* 0.001`
3. ❌ 使用 `Console.WriteLine` 输出日志
4. ❌ 自己实现距离/角度计算(已有 GeometryHelper
5. ❌ 自己实现集合的线程安全包装(使用 ThreadSafeObservableCollection
6. ❌ 自己实现缓存刷新逻辑(使用 NavisworksApiHelper.SafeCacheRefresh
## 扩展阅读
- 项目规范:`AGENTS.md`
- UI开发规范`doc/guide/design_principles.md`
- Navisworks API 使用:`SKILL.md` (nw-api)

View File

@ -0,0 +1,178 @@
# 常见使用场景示例
## 场景1创建并显示对话框
```csharp
using NavisworksTransport.Utils;
public class MyViewModel
{
private MyDialog _dialog;
public void ShowDialog()
{
// 防止重复打开
if (_dialog != null && _dialog.IsVisible)
{
_dialog.Activate();
return;
}
// 创建对话框
_dialog = new MyDialog();
// ✅ 使用 DialogHelper 设置 Owner
DialogHelper.SetOwnerSafely(_dialog);
// 显示非模态对话框
_dialog.Show();
// 清理引用
_dialog.Closed += (s, e) => _dialog = null;
}
}
```
## 场景2带单位转换的计算
```csharp
using NavisworksTransport.Utils;
public class PathPlanner
{
/// <summary>
/// 设置网格大小
/// </summary>
/// <param name="cellSizeInMeters">网格单元大小(米)</param>
public void SetCellSize(double cellSizeInMeters)
{
// ✅ 转换为模型单位存储
double cellSize = UnitsConverter.ConvertFromMeters(cellSizeInMeters);
// 后续计算使用 cellSize模型单位
_grid = new GridMap(cellSize);
}
/// <summary>
/// 获取路径长度(用于显示)
/// </summary>
public string GetPathLengthDisplay(PathRoute route)
{
// ✅ 转换为米显示
double lengthInMeters = UnitsConverter.ConvertToMeters(route.TotalLength);
return $"{lengthInMeters:F2} 米";
}
}
```
## 场景3完整的操作日志记录
```csharp
using NavisworksTransport.Utils;
public class PathService
{
public async Task<PathRoute> CreatePathAsync(PathParameters parameters)
{
LogManager.Info("[路径创建] === 开始创建路径 ===");
LogManager.Info($"[路径创建] 参数: {parameters}");
try
{
// 验证参数
if (!ValidateParameters(parameters))
{
LogManager.Warning("[路径创建] 参数验证失败");
return null;
}
// 执行创建
var route = await GeneratePathAsync(parameters);
LogManager.Info($"[路径创建] 成功: {route.Name}, ID: {route.Id}");
LogManager.Info($"[路径创建] 长度: {route.TotalLength:F2} 米, 点数: {route.Points.Count}");
return route;
}
catch (Exception ex)
{
// ✅ 记录完整异常信息
LogManager.Error($"[路径创建] 失败: {ex.Message}", ex);
throw;
}
finally
{
LogManager.Info("[路径创建] === 创建完成 ===");
}
}
}
```
## 场景4几何计算
```csharp
using NavisworksTransport.Utils;
public class PathAnalyzer
{
/// <summary>
/// 计算路径总长度
/// </summary>
public double CalculateTotalLength(List<Point3D> points)
{
double totalLength = 0;
for (int i = 0; i < points.Count - 1; i++)
{
// ✅ 使用 GeometryHelper
totalLength += GeometryHelper.Distance(points[i], points[i + 1]);
}
return totalLength;
}
/// <summary>
/// 检查转弯是否过急
/// </summary>
public bool IsTurnTooSharp(Point3D prev, Point3D current, Point3D next, double maxAngle)
{
Vector3D v1 = GeometryHelper.Subtract(current, prev);
Vector3D v2 = GeometryHelper.Subtract(next, current);
double angle = GeometryHelper.AngleBetweenDegrees(v1, v2);
return angle > maxAngle;
}
}
```
## 场景5确认对话框
```csharp
using NavisworksTransport.Utils;
using System.Windows;
public class PathViewModel
{
public void DeletePath(PathRoute route)
{
// ✅ 使用 DialogHelper 显示确认对话框
var result = DialogHelper.ShowMessageBox(
$"确定要删除路径 \"{route.Name}\" 吗?\n此操作不可撤销。",
"确认删除",
MessageBoxButton.YesNo,
MessageBoxImage.Warning
);
if (result != MessageBoxResult.Yes)
{
LogManager.Info($"[删除] 用户取消删除: {route.Name}");
return;
}
// 执行删除
_pathPlanningManager.DeleteRoute(route.Id);
LogManager.Info($"[删除] 路径已删除: {route.Name}");
}
}
```

View File

@ -0,0 +1,182 @@
# CoordinateConverter 使用指南
## 文件位置
`src/Utils/CoordinateConverter.cs`
## 用途
负责 2D 地图坐标与 3D 世界坐标之间的转换,用于导航地图和路径规划可视化。
## 核心概念
- **地图坐标MapPoint2D**2D 像素坐标原点在左上角Y轴向下
- **世界坐标Point3D**3D 模型坐标,单位与文档单位一致
- **通道边界ChannelBounds**:定义了有效转换的区域范围
## 构造函数
```csharp
// 使用通道边界和地图尺寸创建转换器
var converter = new CoordinateConverter(
channelBounds: bounds, // ChannelBounds 对象
mapWidth: 800, // 地图窗口宽度(像素)
mapHeight: 600 // 地图窗口高度(像素)
);
```
## 核心方法
### 坐标转换
```csharp
// 2D地图坐标 → 3D世界坐标
MapPoint2D mapPoint = new MapPoint2D(400, 300);
Point3D worldPoint = converter.MapToWorld(mapPoint);
// 3D世界坐标 → 2D地图坐标
Point3D worldPoint = new Point3D(x, y, z);
MapPoint2D mapPoint = converter.WorldToMap(worldPoint);
// 批量转换
var worldPoints = converter.MapToWorldBatch(mapPoints);
var mapPoints = converter.WorldToMapBatch(worldPoints);
```
### 有效性检查
```csharp
// 检查地图坐标是否在有效范围内
bool isValid = converter.IsValidMapPoint(mapPoint);
// 检查世界坐标是否在通道范围内
bool isValid = converter.IsValidWorldPoint(worldPoint);
```
### 距离计算
```csharp
// 计算两个地图点之间的像素距离
double pixelDistance = converter.CalculateMapDistance(point1, point2);
// 计算两个世界坐标点之间的距离(模型单位)
double worldDistance = converter.CalculateWorldDistance(point1, point2);
```
### 视图操作
```csharp
// 放大视图
converter.ZoomIn(factor: 1.5, centerPoint: mapCenter);
// 缩小视图
converter.ZoomOut(factor: 1.5, centerPoint: mapCenter);
// 平移视图
converter.Pan(deltaX: 100, deltaY: -50);
// 重置视图到最佳适应
converter.ResetView();
// 计算最佳适应视图
converter.CalculateBestFitView();
```
### 尺寸更新
```csharp
// 更新地图尺寸(窗口大小变化时)
converter.UpdateMapSize(newWidth: 1024, newHeight: 768);
// 更新通道边界
converter.UpdateChannelBounds(newChannelBounds);
```
### 辅助方法
```csharp
// 获取地图中心点的世界坐标
Point3D center = converter.GetMapCenterInWorld();
// 调整高程
Point3D adjusted = converter.AdjustElevation(worldPoint, elevationOffset: 0.5);
// 获取地图缩放比例
converter.GetMapScale(out double scaleX, out double scaleY);
// 获取转换器信息摘要
string info = converter.ToString();
```
## 属性
```csharp
// 地图尺寸
converter.MapWidth = 800;
converter.MapHeight = 600;
// 通道边界
converter.ChannelBounds = newChannelBounds;
// 默认高程Z坐标
converter.DefaultElevation = 10.0;
// 缩放因子(限制范围 0.1 - 50.0
converter.ZoomFactor = 2.0;
// 偏移量
converter.OffsetX = 100;
converter.OffsetY = 50;
```
## 使用示例
### 示例1基本的坐标转换
```csharp
// 创建转换器
var bounds = new ChannelBounds(minPoint, maxPoint);
var converter = new CoordinateConverter(bounds, 800, 600);
// 用户在地图上点击的位置
var clickPoint = new MapPoint2D(400, 300);
// 转换为世界坐标进行路径规划
Point3D worldPoint = converter.MapToWorld(clickPoint);
LogManager.Info($"点击位置对应世界坐标: ({worldPoint.X:F2}, {worldPoint.Y:F2}, {worldPoint.Z:F2})");
```
### 示例2路径点批量转换
```csharp
// 将规划好的路径点转换为地图坐标显示
var pathWorldPoints = pathRoute.Points.Select(p => p.Position);
var pathMapPoints = converter.WorldToMapBatch(pathWorldPoints);
// 在地图上绘制路径
foreach (var mapPoint in pathMapPoints)
{
DrawPointOnMap(mapPoint.X, mapPoint.Y);
}
```
### 示例3响应窗口大小变化
```csharp
// 地图控件大小变化时更新转换器
void OnMapSizeChanged(double newWidth, double newHeight)
{
converter.UpdateMapSize(newWidth, newHeight);
converter.CalculateBestFitView();
// 重绘地图...
}
```
## 注意事项
1. **Y轴翻转**:地图坐标系 Y 轴向下(屏幕坐标),世界坐标系 Y 轴向上,转换时自动处理
2. **边距设置**:从配置读取 `MarginRatio`,默认 10%,确保内容不会贴边显示
3. **Z坐标处理**`MapToWorld` 返回的 Z 坐标使用 `DefaultElevation`,可通过 `AdjustElevation` 调整
4. **缩放限制**:缩放因子限制在 0.1 - 50.0 范围内,防止过度缩放
5. **线程安全**:此类的实例方法不是线程安全的,需要在单线程中使用或使用外部锁

View File

@ -0,0 +1,127 @@
# DialogHelper 使用指南
## 文件位置
`src/Utils/DialogHelper.cs`
## 用途
统一处理对话框的 Owner 设置和置顶显示,解决 Navisworks 插件环境中的窗口焦点问题。
## 核心方法
### 1. SetOwnerSafely - 安全设置Owner推荐
```csharp
// 场景:在 ViewModel 中创建对话框
var dialog = new MyDialog();
DialogHelper.SetOwnerSafely(dialog);
dialog.Show(); // 或 dialog.ShowDialog();
```
**特点**
- 自动查找可用的 Owner 窗口
- 处理 Navisworks 插件环境的特殊情况
- 捕获异常,不会导致程序崩溃
### 2. ShowDialog - 显示模态对话框(完整处理)
```csharp
// 场景:显示需要返回结果的模态对话框
var dialog = new MyInputDialog();
bool? result = DialogHelper.ShowDialog(dialog);
```
### 3. ShowMessageBox - 显示消息框
```csharp
// 场景:显示错误提示或确认对话框
DialogHelper.ShowMessageBox(
"操作成功完成",
"提示",
MessageBoxButton.OK,
MessageBoxImage.Information
);
```
### 4. SetWin32Owner - 强制置顶到Navisworks主窗口
```csharp
// 场景对话框必须置顶到Navisworks主窗口极少数情况
var dialog = new MyDialog();
DialogHelper.SetWin32Owner(dialog);
dialog.Show();
```
## 使用示例
### 示例1ViewModel中显示非模态对话框
```csharp
private MyDialog _myDialog;
private void ShowMyDialog()
{
if (_myDialog != null && _myDialog.IsVisible)
{
_myDialog.Activate();
return;
}
_myDialog = new MyDialog();
DialogHelper.SetOwnerSafely(_myDialog); // ✅ 使用工具方法
_myDialog.Show();
_myDialog.Closed += (s, e) => _myDialog = null;
}
```
### 示例2显示确认对话框
```csharp
private bool ConfirmDelete()
{
var result = DialogHelper.ShowMessageBox(
"确定要删除此路径吗?",
"确认删除",
MessageBoxButton.YesNo,
MessageBoxImage.Warning
);
return result == MessageBoxResult.Yes;
}
```
## 注意事项
1. **不要在构造函数中设置 Owner**
```csharp
// ❌ 错误
public MyDialog()
{
InitializeComponent();
this.Owner = Application.Current.MainWindow; // 可能抛出异常
}
// ✅ 正确:在外部设置
var dialog = new MyDialog();
DialogHelper.SetOwnerSafely(dialog);
```
2. **非模态对话框也需要设置 Owner**
```csharp
// 即使是 Show() 而非 ShowDialog(),也需要设置 Owner
var dialog = new MyDialog();
DialogHelper.SetOwnerSafely(dialog); // 确保正确置顶
dialog.Show();
```
3. **XAML 中不要设置 Owner**
```xml
<!-- ❌ 错误 -->
<Window ...
Owner="{x:Static Application.Current.MainWindow}">
<!-- ✅ 正确XAML中不设置代码中设置 -->
<Window ...>
```

View File

@ -0,0 +1,134 @@
# GeometryHelper 使用指南
## 文件位置
`src/Utils/GeometryHelper.cs`
## 用途
3D 几何计算工具类,提供距离、角度、向量等常用几何计算。**禁止自己实现几何计算**,必须复用此类。
## 核心方法
### 距离计算
```csharp
// 两点间距离
Point3D a = new Point3D(0, 0, 0);
Point3D b = new Point3D(3, 4, 0);
double distance = GeometryHelper.Distance(a, b); // 5.0
// 2D距离忽略Z坐标
double distance2D = GeometryHelper.Distance2D(a, b);
// 距离的平方(性能优化,避免开方)
double distanceSq = GeometryHelper.DistanceSquared(a, b);
```
### 向量运算
```csharp
// 向量减法b - a
Vector3D vector = GeometryHelper.Subtract(b, a);
// 向量长度
double length = GeometryHelper.VectorLength(vector);
// 单位向量
Vector3D normalized = GeometryHelper.Normalize(vector);
// 点积
double dot = GeometryHelper.DotProduct(v1, v2);
// 叉积
Vector3D cross = GeometryHelper.CrossProduct(v1, v2);
```
### 角度计算
```csharp
// 向量夹角(弧度)
double angle = GeometryHelper.AngleBetween(v1, v2);
// 向量夹角(角度)
double angleDegrees = GeometryHelper.AngleBetweenDegrees(v1, v2);
// 三点形成的角度以b为顶点
double angle = GeometryHelper.AngleAtVertex(a, b, c);
```
### 点与线的关系
```csharp
// 点到线段的最近点
Point3D closestPoint = GeometryHelper.ProjectPointOnLineSegment(
point, lineStart, lineEnd);
// 点到线段的距离
double distance = GeometryHelper.DistancePointToLineSegment(
point, lineStart, lineEnd);
// 点是否在线段上(含容差)
bool isOnLine = GeometryHelper.IsPointOnLineSegment(
point, lineStart, lineEnd, tolerance: 0.001);
```
## 使用示例
### 示例1计算路径总长度
```csharp
// ❌ 错误:自己实现距离计算
double dx = p2.X - p1.X;
double dy = p2.Y - p1.Y;
double dz = p2.Z - p1.Z;
double distance = Math.Sqrt(dx*dx + dy*dy + dz*dz);
// ✅ 正确:使用 GeometryHelper
double distance = GeometryHelper.Distance(p1, p2);
```
### 示例2检查三点是否共线
```csharp
public bool AreCollinear(Point3D a, Point3D b, Point3D c, double tolerance = 0.001)
{
// 计算叉积如果为0则共线
Vector3D ab = GeometryHelper.Subtract(b, a);
Vector3D ac = GeometryHelper.Subtract(c, a);
Vector3D cross = GeometryHelper.CrossProduct(ab, ac);
return GeometryHelper.VectorLength(cross) < tolerance;
}
```
### 示例3计算转弯角度
```csharp
public double CalculateTurnAngle(Point3D prev, Point3D current, Point3D next)
{
// 入向量从prev到current
Vector3D v1 = GeometryHelper.Subtract(current, prev);
// 出向量从current到next
Vector3D v2 = GeometryHelper.Subtract(next, current);
// 计算夹角
double angle = GeometryHelper.AngleBetweenDegrees(v1, v2);
return angle; // 0-180度
}
```
## 性能提示
```csharp
// 大量距离比较时,使用距离的平方避免开方
// ✅ 更快
double distSq = GeometryHelper.DistanceSquared(a, b);
if (distSq < threshold * threshold)
// ❌ 更慢(需要开方)
double dist = GeometryHelper.Distance(a, b);
if (dist < threshold)
```

View File

@ -0,0 +1,144 @@
# LogManager 使用指南
## 文件位置
`src/Utils/LogManager.cs`
## 用途
统一的日志记录管理器。在 Navisworks 插件环境中,`Console.WriteLine` 不可见,必须使用 LogManager。
## 日志级别
```csharp
LogManager.Debug("调试信息"); // 开发调试使用
LogManager.Info("普通信息"); // 一般操作记录
LogManager.Warning("警告信息"); // 需要注意但非错误
LogManager.Error("错误信息"); // 错误(可选异常参数)
LogManager.Fatal("致命错误"); // 严重错误
```
## 核心方法
### 基本信息日志
```csharp
LogManager.Info("路径创建成功");
LogManager.Info($"路径名称: {route.Name}, 长度: {route.TotalLength}");
```
### 带异常的错误日志
```csharp
try
{
// 某些操作
}
catch (Exception ex)
{
// ✅ 正确:记录异常详情
LogManager.Error($"操作失败: {ex.Message}", ex);
}
```
### 调试日志(开发时)
```csharp
LogManager.Debug($"当前路径点数: {path.Points.Count}");
LogManager.Debug($"网格大小: {grid.Width} x {grid.Height}");
```
## 使用示例
### 示例1记录操作开始和完成
```csharp
public void CreatePath()
{
LogManager.Info("=== 开始创建路径 ===");
try
{
// 执行操作
var route = GeneratePath();
LogManager.Info($"路径创建成功: {route.Name}, ID: {route.Id}");
}
catch (Exception ex)
{
LogManager.Error($"路径创建失败: {ex.Message}", ex);
throw; // 重新抛出或处理
}
LogManager.Info("=== 创建路径完成 ===");
}
```
### 示例2记录重要状态变更
```csharp
public void SetStartPoint(Point3D point)
{
_startPoint = point;
LogManager.Info($"起点已设置: ({point.X:F2}, {point.Y:F2}, {point.Z:F2})");
if (_endPoint != null)
{
double distance = GeometryHelper.Distance(point, _endPoint);
LogManager.Info($"起点终点距离: {distance:F2} 米");
}
}
```
## 日志文件位置
日志文件存储在:
```
C:\ProgramData\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin\logs\
```
文件命名:
- `debug.log` - 当前日志
- `debug.log.1`, `debug.log.2` - 历史日志(自动轮转)
## 注意事项
1. **永远不要使用 Console.WriteLine**
```csharp
// ❌ 错误在Navisworks中不可见
Console.WriteLine("Error occurred");
// ✅ 正确:写入日志文件
LogManager.Error("Error occurred");
```
2. **错误日志要包含异常**
```csharp
// ❌ 不够详细
LogManager.Error($"失败: {ex.Message}");
// ✅ 包含堆栈信息
LogManager.Error($"失败: {ex.Message}", ex);
```
3. **避免在循环中大量记录**
```csharp
// ❌ 可能导致日志文件过大
foreach (var point in points)
{
LogManager.Debug($"处理点: {point.Name}"); // 1000个点=1000条日志
}
// ✅ 只记录摘要
LogManager.Debug($"开始处理 {points.Count} 个点...");
// ... 处理 ...
LogManager.Debug($"点处理完成");
```
4. **使用类别前缀便于过滤**
```csharp
LogManager.Info("[路径规划] 开始生成");
LogManager.Info("[碰撞检测] 发现3个碰撞");
LogManager.Info("[渲染] 路径显示完成");
```

View File

@ -0,0 +1,130 @@
# ModelHighlightHelper 使用指南
## 文件位置
`src/Utils/ModelHighlightHelper.cs`
## 用途
统一管理 Navisworks 临时高亮,支持按类别高亮与清除。使用不同颜色区分不同类型的对象(碰撞结果、通道预览、动画物体等)。
## 颜色定义
| 类别 | 颜色 | RGB | 用途 |
|------|------|-----|------|
| `PrecomputeCollisionResultsCategory` | Material Purple | (156, 39, 176) | 预计算碰撞结果 |
| `ManualTargetsCategory` | 橙色 | (255, 170, 0) | 手工指定对象 |
| `AnimatedObjectCategory` | Amber/Yellow | (255, 193, 7) | 动画物体 |
| `ClashDetectiveResultsCategory` | 红色 | Color.Red | ClashDetective碰撞结果 |
| `ChannelPreviewCategory` | 绿色 | Color.Green | 通道预览 |
| `excludedObjects` | Material Green | (76, 175, 80) | 排除对象 |
## 核心方法
### 基础高亮操作
```csharp
// 高亮指定对象集合(使用类别默认颜色)
ModelHighlightHelper.HighlightItems("myCategory", modelItems);
// 清除指定类别的高亮
ModelHighlightHelper.ClearCategory("myCategory");
// 清除所有高亮
ModelHighlightHelper.ClearAllHighlights();
// 清除碰撞相关高亮
ModelHighlightHelper.ClearCollisionHighlights();
```
### 碰撞结果高亮
```csharp
// 按类别管理碰撞高亮
ModelHighlightHelper.ManageCollisionHighlightsByCategory(
"collisionResults",
collisionResultsList,
clearOtherCategories: false
);
// 高亮ClashDetective结果
ModelHighlightHelper.HighlightClashDetectiveResults(
testName,
getResultsFunc
);
// 清除ClashDetective高亮
ModelHighlightHelper.ClearClashDetectiveHighlights();
```
### 获取高亮状态
```csharp
// 获取当前高亮快照
var highlights = ModelHighlightHelper.GetActiveHighlightSnapshot();
foreach (var info in highlights)
{
LogManager.Info($"类别: {info.Category}, 颜色: {info.Color}, 数量: {info.Count}");
}
// 获取类别颜色
Color color = ModelHighlightHelper.GetCategoryColor("myCategory");
```
## 使用示例
### 示例1高亮碰撞检测结果
```csharp
// 获取碰撞结果
var collisions = collisionDetector.DetectCollisions();
// 高亮显示碰撞对象(使用预计算碰撞类别)
var collidingItems = collisions.Select(c => c.Item2).ToList();
ModelHighlightHelper.HighlightItems(
ModelHighlightHelper.PrecomputeCollisionResultsCategory,
collidingItems
);
// 操作完成后清除高亮
ModelHighlightHelper.ClearCategory(ModelHighlightHelper.PrecomputeCollisionResultsCategory);
```
### 示例2高亮通道预览
```csharp
// 高亮显示通道对象
var channelItems = GetChannelItems();
ModelHighlightHelper.HighlightItems(
ModelHighlightHelper.ChannelPreviewCategory,
channelItems
);
// 清除时只清除通道预览,不影响其他高亮
ModelHighlightHelper.ClearCategory(ModelHighlightHelper.ChannelPreviewCategory);
```
### 示例3多类别管理
```csharp
// 同时高亮不同类型对象
ModelHighlightHelper.HighlightItems("doors", doorItems);
ModelHighlightHelper.HighlightItems("elevators", elevatorItems);
ModelHighlightHelper.HighlightItems("stairs", stairItems);
// 只清除门的高亮
ModelHighlightHelper.ClearCategory("doors");
// 清除所有高亮
ModelHighlightHelper.ClearAllHighlights();
```
## 注意事项
1. **类别颜色自动管理**:每个类别有预定义颜色,也可以使用自定义类别(默认白色)
2. **自动去重**:同一对象在不同类别中可能被多次高亮,后设置的会覆盖先前的
3. **线程安全**:内部使用锁保护,可从多线程调用
4. **清除策略**
- 清除单个类别:`ClearCategory()`
- 清除所有碰撞相关:`ClearCollisionHighlights()`
- 清除全部:`ClearAllHighlights()`

View File

@ -0,0 +1,108 @@
# NavisworksApiHelper 使用指南
## 文件位置
`src/Utils/NavisworksApiHelper.cs`
## 用途
提供 Navisworks API 通用工具方法,包括缓存刷新、线程安全操作、模型项查找等。
## 核心方法
### 缓存刷新
```csharp
// 安全的缓存刷新(轻量级)
NavisworksApiHelper.SafeCacheRefresh("MyComponent");
// 线程安全的缓存刷新自动确保在UI线程执行
NavisworksApiHelper.SafeCacheRefreshUIThread("MyComponent");
```
### 线程检查与执行
```csharp
// 检查当前是否在主UI线程
bool isUIThread = NavisworksApiHelper.IsOnUIThread();
// 在UI线程执行操作无返回值
NavisworksApiHelper.ExecuteOnUIThread(() =>
{
// 这段代码确保在UI线程执行
document.CurrentSelection.Clear();
});
// 在UI线程执行操作有返回值
var result = NavisworksApiHelper.ExecuteOnUIThread(() =>
{
return document.CurrentSelection.SelectedItems.Count;
});
```
### 模型项操作
```csharp
// 获取ModelItem的显示名称安全
string name = NavisworksApiHelper.GetModelItemName(modelItem);
// 使用PathId查找ModelItem
ModelItem item = NavisworksApiHelper.FindModelItemByPathId(modelIndex, pathId);
```
## 使用示例
### 示例1API操作后刷新缓存
```csharp
// 执行某些API操作
document.Models.SetHidden(items, true);
// 刷新缓存以确保状态同步
NavisworksApiHelper.SafeCacheRefresh("VisibilityManager");
```
### 示例2确保在UI线程操作
```csharp
// 在后台线程中需要操作UI
Task.Run(() =>
{
// 后台计算...
var result = DoCalculation();
// 回到UI线程更新界面
NavisworksApiHelper.ExecuteOnUIThread(() =>
{
StatusText = $"计算完成: {result}";
ProgressBar.Value = 100;
});
});
```
### 示例3查找模型项
```csharp
// 从路径ID恢复模型项引用
var item = NavisworksApiHelper.FindModelItemByPathId(0, "1/2/3/4");
if (item != null)
{
LogManager.Info($"找到模型项: {item.DisplayName}");
}
```
### 示例4获取模型项名称安全
```csharp
// 安全的获取名称,处理空值和异常
string name = NavisworksApiHelper.GetModelItemName(modelItem);
// 如果 modelItem 为 null返回 "未知对象"
// 如果获取失败,返回 "获取失败"
```
## 注意事项
1. **缓存刷新机制**:使用空集合的 `SetHidden` 操作触发缓存更新,开销极小
2. **线程安全**`SafeCacheRefreshUIThread` 会自动检查当前线程必要时切换到UI线程
3. **异常处理**:所有方法内部都处理了异常,不会抛出错误
4. **日志前缀**:传入的日志前缀用于标识调用来源,便于调试

View File

@ -0,0 +1,202 @@
# NavisworksSelectionHelper 使用指南
## 文件位置
`src/Utils/NavisworksSelectionHelper.cs`
## 用途
提供 Navisworks 选择状态管理帮助功能,包括选择状态查询、格式化显示、事件订阅等。
## 核心方法
### 选择状态查询
```csharp
// 获取当前选择状态信息(同步)
SelectionStateResult result = NavisworksSelectionHelper.GetCurrentSelectionState();
// 获取当前选择状态信息(异步)
SelectionStateResult result = await NavisworksSelectionHelper.GetCurrentSelectionStateAsync();
// 快速检查是否有选中项
bool hasSelection = NavisworksSelectionHelper.HasSelectedItems();
```
### 设置选择
```csharp
// 设置单个模型项为选中状态
bool success = NavisworksSelectionHelper.SetModelSelection(modelItem);
// 设置多个模型项为选中状态
bool success = NavisworksSelectionHelper.SetModelSelection(modelItems);
// 传入 null 或空集合会清除选择
```
### 选择文本格式化
```csharp
// 格式化选择状态文本
string text = NavisworksSelectionHelper.FormatSelectionText(
count: result.Count,
selectedItems: result.SelectedItems,
unitName: "个模型", // 单位名称
maxDisplayCount: 3, // 最多显示名称数量
maxTotalLength: 80 // 最大总长度
);
// 基于选择结果对象格式化
string text = NavisworksSelectionHelper.FormatSelectionText(
selectionResult: result,
unitName: "个对象",
maxDisplayCount: 3,
maxTotalLength: 80
);
```
### 选择事件订阅
```csharp
// 订阅选择变化事件
var subscription = NavisworksSelectionHelper.SubscribeToSelectionChanges(
async (selectionResult) =>
{
// 处理选择变化
LogManager.Info($"选择变化: {selectionResult.Count} 个对象");
},
uiStateManager: _uiStateManager // 可选用于确保UI线程执行
);
// 取消订阅(使用完释放)
subscription.Dispose();
```
## SelectionStateResult 属性
```csharp
public class SelectionStateResult
{
public bool Success { get; set; } // 操作是否成功
public int Count { get; set; } // 选择数量
public List<ModelItem> SelectedItems { get; set; } // 选择的项目列表
public bool HasSelection { get; set; } // 是否有选择
public string ErrorMessage { get; set; } // 错误信息(失败时)
}
```
## 使用示例
### 示例1检查并显示选择状态
```csharp
// 获取选择状态
var result = NavisworksSelectionHelper.GetCurrentSelectionState();
if (!result.Success)
{
LogManager.Error($"获取选择状态失败: {result.ErrorMessage}");
return;
}
// 格式化显示
string statusText = NavisworksSelectionHelper.FormatSelectionText(result);
StatusLabel.Text = statusText;
// 启用/禁用相关按钮
EditButton.IsEnabled = result.Count == 1;
DeleteButton.IsEnabled = result.Count > 0;
```
### 示例2同步UI选择状态
```csharp
// 在ViewModel中保持选择状态同步
private void UpdateSelectionState()
{
var result = NavisworksSelectionHelper.GetCurrentSelectionState();
if (result.Count > 0)
{
// 更新属性
SelectedItems = result.SelectedItems;
SelectionText = NavisworksSelectionHelper.FormatSelectionText(result);
// 如果有单个选择,显示详细信息
if (result.Count == 1)
{
var item = result.SelectedItems[0];
SelectedItemName = item.DisplayName;
}
}
else
{
SelectedItems = new List<ModelItem>();
SelectionText = "请选择模型对象";
}
}
```
### 示例3订阅选择变化事件
```csharp
public class MyViewModel : IDisposable
{
private SelectionEventSubscription _selectionSubscription;
public void Initialize()
{
// 订阅选择变化
_selectionSubscription = NavisworksSelectionHelper.SubscribeToSelectionChanges(
OnSelectionChanged,
uiStateManager: _uiStateManager
);
}
private async Task OnSelectionChanged(SelectionStateResult result)
{
// 此方法在UI线程执行通过uiStateManager
if (result.HasSelection)
{
SelectionText = NavisworksSelectionHelper.FormatSelectionText(result);
await LoadSelectionDetails(result.SelectedItems);
}
else
{
SelectionText = "请在主界面中选择需要设置的对象";
}
}
public void Dispose()
{
_selectionSubscription?.Dispose();
}
}
```
### 示例4程序化设置选择
```csharp
// 选择特定对象
var targetItem = FindModelItemById(id);
if (targetItem != null)
{
bool success = NavisworksSelectionHelper.SetModelSelection(targetItem);
if (success)
{
// 聚焦到该对象
ViewpointHelper.FocusOnModelItem(targetItem, 45, 0.25);
}
}
// 清除选择
NavisworksSelectionHelper.SetModelSelection(null);
```
## 注意事项
1. **线程安全**`GetCurrentSelectionState` 等方法是线程安全的但设置选择最好在UI线程执行
2. **事件处理**:选择变化事件使用 `async void` 内部处理确保不会阻塞UI
3. **订阅管理**:使用 `SelectionEventSubscription` 模式确保事件正确取消订阅,避免内存泄漏
4. **格式化限制**`FormatSelectionText` 会自动处理长名称截断和多个项目的显示
5. **空值处理**:所有方法都处理了空值情况,不会抛出异常

View File

@ -0,0 +1,140 @@
# PathHelper 使用指南
## 文件位置
`src/Utils/PathHelper.cs`
## 用途
提供文件路径相关的通用方法,包括插件目录管理、文件名处理、截图生成等。
## 核心方法
### 目录操作
```csharp
// 获取插件目录路径
string pluginDir = PathHelper.GetPluginDirectory();
// 返回: C:\ProgramData\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin
// 获取截图目录路径
string screenshotDir = PathHelper.GetScreenshotDirectory();
// 获取报告目录路径
string reportDir = PathHelper.GetReportDirectory();
// 确保目录存在(不存在则创建)
PathHelper.EnsureDirectoryExists("C:\\MyFolder\\SubFolder");
```
### 文件名处理
```csharp
// 清理文件名中的非法字符
string safeName = PathHelper.SanitizeFileName("文件<名称>:非法*字符?");
// 返回: "文件名称非法字符"
// 生成带时间戳的文件名
string fileName = PathHelper.GenerateTimestampedFileName("screenshot", "png");
// 返回: "screenshot_20240225_143052.png"
```
### 路径计算
```csharp
// 计算从HTML文件到目标文件的相对路径
string relativePath = PathHelper.GetRelativePath(
@"C:\reports\index.html",
@"C:\images\photo.png"
);
// 返回: "../images/photo.png"
```
### 图像格式转换
```csharp
// ImageFormat 转文件扩展名
string ext = PathHelper.ImageFormatToExtension(ImageFormat.Jpeg); // "jpg"
string ext = PathHelper.ImageFormatToExtension(ImageFormat.Png); // "png"
// 文件扩展名转 ImageFormat
ImageFormat format = PathHelper.ExtensionToImageFormat(".jpg"); // Jpeg
ImageFormat format = PathHelper.ExtensionToImageFormat(".png"); // Png
```
### 截图生成
```csharp
// 生成场景截图到默认目录
string path = PathHelper.GenerateSceneScreenshot(
sceneName: "collision_view",
width: 1920,
height: 1080,
format: ImageFormat.Png,
prefix: "collision"
);
// 返回: 截图文件完整路径
// 生成场景截图到指定目录
string path = PathHelper.GenerateSceneScreenshotToDirectory(
outputDirectory: @"C:\MyScreenshots",
sceneName: "path_view",
width: 1920,
height: 1080,
format: ImageFormat.Jpeg,
prefix: "path"
);
```
## 使用示例
### 示例1生成带时间戳的报告文件
```csharp
// 生成报告文件名
string reportDir = PathHelper.GetReportDirectory();
PathHelper.EnsureDirectoryExists(reportDir);
string safeRouteName = PathHelper.SanitizeFileName(route.Name);
string reportFile = PathHelper.GenerateTimestampedFileName(
$"collision_report_{safeRouteName}",
"html"
);
string fullPath = Path.Combine(reportDir, reportFile);
// 保存报告...
```
### 示例2批量处理文件名
```csharp
// 清理多个文件名
var fileNames = new[] { "文件:1", "名称*2", "测试?3" };
var safeNames = fileNames.Select(f => PathHelper.SanitizeFileName(f));
// 结果: "文件1", "名称2", "测试3"
```
### 示例3生成碰撞报告截图
```csharp
// 在显示碰撞结果后生成截图
string screenshotPath = PathHelper.GenerateSceneScreenshot(
sceneName: $"collision_{collisionId}",
width: 1920,
height: 1080,
format: ImageFormat.Png,
prefix: "collision"
);
if (!string.IsNullOrEmpty(screenshotPath))
{
LogManager.Info($"截图已保存: {screenshotPath}");
}
```
## 注意事项
1. **目录自动创建**`GenerateSceneScreenshot` 会自动创建必要的目录
2. **非法字符处理**`SanitizeFileName` 会移除所有 Windows 文件系统不支持的字符
3. **时间戳格式**:使用 `yyyyMMdd_HHmmss` 格式,确保文件名唯一且可排序
4. **相对路径**`GetRelativePath` 返回的路径使用正斜杠(/),适合在 HTML 中使用

View File

@ -0,0 +1,213 @@
# SectionClipHelper 使用指南
## 文件位置
`src/Utils/SectionClipHelper.cs`
## 用途
剖面盒辅助类,用于优化碰撞检测性能。通过设置视口剖面盒,只处理路径周围的对象,大幅减少需要检测的对象数量。
## 核心方法
### 设置剖面盒
```csharp
// 根据路径点列表设置剖面盒
bool success = SectionClipHelper.SetClipBoxByPath(
pathPoints: points,
marginInMeters: 2.0, // 水平边距(米)
heightMarginInMeters: 1.0 // 高度边距(米)
);
// 根据路径点设置剖面盒(支持分别设置上下边距)
bool success = SectionClipHelper.SetClipBoxByPathWithMargins(
pathPoints: points,
marginInMeters: 2.0,
heightMarginBottomInMeters: 0.5, // 底部边距
heightMarginTopInMeters: 2.0 // 顶部边距
);
// 根据单个点设置剖面盒(用于吊装路径等)
bool success = SectionClipHelper.SetClipBoxByPoint(
centerPoint: point,
rangeMeters: 10.0, // 范围(米)
heightRangeMeters: 5.0 // 高度范围(米)
);
// 根据楼层设置剖面盒
bool success = SectionClipHelper.SetClipBoxByFloor(
floorItem: floor,
marginMeters: 1.0
);
```
### 清除剖面盒
```csharp
// 清除剖面盒,显示全部模型
SectionClipHelper.ClearClipBox();
```
### 状态查询
```csharp
// 检查剖面盒是否已启用
bool isEnabled = SectionClipHelper.IsClipBoxEnabled;
// 获取当前剖面盒
if (SectionClipHelper.TryGetCurrentClipBox(out BoundingBox3D clipBox))
{
LogManager.Info($"剖面盒范围: {clipBox.Min} - {clipBox.Max}");
}
```
### 包含性测试
```csharp
// 测试点是否在剖面盒内
bool isInside = SectionClipHelper.IsPointInClipBox(point);
// 测试包围盒是否与剖面盒相交
bool intersects = SectionClipHelper.IntersectsClipBox(itemBox);
// 使用角点检测(排除大包围盒假阳性)
bool intersects = SectionClipHelper.IntersectsByCornerPoints(itemBox, clipBox);
// 使用棱上自适应采样检测(长物体不会漏检)
bool intersects = SectionClipHelper.IntersectsByEdgeSampling(
itemBox, clipBox, minSamplesPerEdge: 3
);
```
### 统计功能
```csharp
// 统计剖面盒内/外的对象数量
SectionClipHelper.CountObjectsInClipBox(
out int totalCount,
out int insideCount,
out int outsideCount
);
// 使用角点检测统计(排除大包围盒假阳性)
SectionClipHelper.CountObjectsInClipBoxByCorners(
out int totalCount,
out int insideCount,
out int outsideCount,
out int largeBoxFiltered,
debugDetails: detailsList // 可选的详细信息列表
);
// 测试相交检测(调试用)
SectionClipHelper.TestIntersection();
```
## 使用示例
### 示例1路径碰撞检测前设置剖面盒
```csharp
// 获取路径点
var pathPoints = pathRoute.Points.Select(p => p.Position).ToList();
// 设置剖面盒
bool success = SectionClipHelper.SetClipBoxByPath(
pathPoints,
marginInMeters: 2.0, // 路径周围2米
heightMarginInMeters: 1.0 // 上下各1米
);
if (success)
{
// 统计过滤效果
SectionClipHelper.CountObjectsInClipBoxByCorners(
out int total, out int inside, out int outside, out int filtered
);
LogManager.Info($"剖面盒过滤: 总数{total}, 盒内{inside}, 过滤{filtered}");
// 执行碰撞检测(只检测剖面盒内对象)
var collisions = collisionDetector.DetectCollisions();
}
// 完成后清除剖面盒
SectionClipHelper.ClearClipBox();
```
### 示例2吊装路径场景
```csharp
// 吊装路径通常只需要关注起吊点周围区域
if (liftPoint != null)
{
SectionClipHelper.SetClipBoxByPoint(
liftPoint,
rangeMeters: 15.0, // 15米范围
heightRangeMeters: 20.0 // 20米高度范围
);
}
```
### 示例3精确过滤排除大包围盒假阳性
```csharp
// 获取剖面盒
if (!SectionClipHelper.TryGetCurrentClipBox(out var clipBox))
return;
// 遍历模型进行过滤
foreach (var item in modelItems)
{
var itemBox = item.BoundingBox();
// 先用包围盒快速排除
if (!clipBox.Intersects(itemBox))
continue;
// 再用角点检测精确判断
if (!SectionClipHelper.IntersectsByCornerPoints(itemBox, clipBox))
{
// 包围盒相交但没有角点在盒内,可能是大包围盒假阳性
LogManager.Debug($"跳过假阳性: {item.DisplayName}");
continue;
}
// 该对象真正与剖面盒相交,加入检测列表
itemsToCheck.Add(item);
}
```
### 示例4长物体检测使用棱上采样
```csharp
// 对于长管道等物体,使用棱上采样确保不遗漏
if (SectionClipHelper.TryGetCurrentClipBox(out var clipBox))
{
var itemBox = longPipeItem.BoundingBox();
// 使用棱上自适应采样(根据物体大小动态计算采样点)
if (SectionClipHelper.IntersectsByEdgeSampling(itemBox, clipBox, minSamplesPerEdge: 5))
{
// 长管道与剖面盒相交
itemsToCheck.Add(longPipeItem);
}
}
```
## 默认参数
```csharp
private const double DEFAULT_MARGIN_METERS = 2.0; // 默认水平边距
private const double DEFAULT_HEIGHT_MARGIN_METERS = 1.0; // 默认高度边距
```
## 注意事项
1. **性能优化**:剖面盒可以大幅减少碰撞检测的对象数量,提高性能
2. **JSON设置**:使用 JSON 字符串方式设置剖面盒,避免 "Object is Read-Only" 错误
3. **过滤策略**
- 先用 `IntersectsClipBox` 快速排除
- 再用 `IntersectsByCornerPoints` 排除大包围盒假阳性
- 最后用 `IntersectsByEdgeSampling` 确保长物体不遗漏
4. **清除责任**:使用完剖面盒后应调用 `ClearClipBox()` 恢复完整视图
5. **统计功能**:使用 `CountObjectsInClipBoxByCorners` 查看过滤效果和假阳性数量

View File

@ -0,0 +1,126 @@
# UnitsConverter 使用指南
## 文件位置
`src/Utils/UnitsConverter.cs`
## 用途
处理模型单位与米制单位之间的转换。Navisworks 文档可以使用不同的单位(米、毫米、英尺等),必须通过此工具类进行转换。
## 命名规范(极其重要)
| 单位类型 | 命名规则 | 示例 |
|---------|---------|------|
| **米单位** | 变量名必须以 `InMeters` 结尾 | `lengthInMeters`, `heightInMeters` |
| **模型单位** | 变量名**无后缀** | `length`, `height`, `cellSize` |
```csharp
// ✅ 正确
public void SetSize(double lengthInMeters)
{
double factor = UnitsConverter.GetMetersToUnitsConversionFactor(...);
double length = lengthInMeters * factor; // 模型单位,无后缀
boundingBox = new BoundingBox3D(0, 0, 0, length, width, height);
}
// ❌ 错误
public void SetSize(double length) // 无法区分是米还是模型单位!
{
double scale = length / baseSize; // 单位不明确!
}
```
## 核心方法
### 获取转换因子
```csharp
// 获取模型单位到米的转换因子(模型单位 × factor = 米)
double factor = UnitsConverter.GetUnitsToMetersConversionFactor();
// 获取米到模型单位的转换因子(米 × factor = 模型单位)
double factor = UnitsConverter.GetMetersToUnitsConversionFactor();
```
### 直接转换值
```csharp
// 模型单位 → 米
double meters = UnitsConverter.ConvertToMeters(distanceInModelUnits);
// 米 → 模型单位
double modelUnits = UnitsConverter.ConvertFromMeters(distanceInMeters);
```
### 带单位的转换(从文档)
```csharp
// 从当前文档获取单位信息
Units units = Application.ActiveDocument.Units;
// 使用特定单位进行转换
double meters = UnitsConverter.ConvertToMeters(distance, units);
double modelUnits = UnitsConverter.ConvertFromMeters(distanceInMeters, units);
```
## 使用示例
### 示例1读取配置并转换为模型单位
```csharp
// 配置中存储的是米
double cellSizeInMeters = ConfigManager.Instance.Current.PathEditing.CellSizeMeters;
// 转换为模型单位用于计算
double cellSize = UnitsConverter.ConvertFromMeters(cellSizeInMeters);
```
### 示例2显示长度给用户
```csharp
// 路径长度是模型单位
double totalLength = route.TotalLength;
// 转换为米显示
double lengthInMeters = UnitsConverter.ConvertToMeters(totalLength);
string displayText = $"总长度: {lengthInMeters:F2} 米";
```
### 示例3完整的函数参数处理
```csharp
/// <summary>
/// 生成网格地图
/// </summary>
/// <param name="cellSizeInMeters">网格单元大小(米)</param>
public GridMap Generate(double cellSizeInMeters)
{
// 转换为模型单位
double cellSize = UnitsConverter.ConvertFromMeters(cellSizeInMeters);
// 后续计算使用 cellSize模型单位
int gridX = (int)(bounds.Width / cellSize);
int gridY = (int)(bounds.Height / cellSize);
// ...
}
```
## 常见错误
```csharp
// ❌ 错误1硬编码转换因子
private const double MM_TO_METERS = 0.001; // 危险!不同文档单位不同
double meters = modelValue * MM_TO_METERS; // 只在毫米文档正确
// ❌ 错误2混淆命名
double cellSize = 0.5; // 这是米还是模型单位?
if (height > cellSize) // 单位不匹配的严重bug
// ❌ 错误3混用不同文档的单位
Units doc1Units = doc1.Units;
Units doc2Units = doc2.Units;
double value1 = ConvertToMeters(modelValue, doc1Units);
double value2 = ConvertToMeters(modelValue, doc2Units); // 可能不同!
```

View File

@ -0,0 +1,144 @@
# ViewpointHelper 使用指南
## 文件位置
`src/Utils/ViewpointHelper.cs`
## 用途
提供 Navisworks 视角调整功能,用于自动调整摄像机位置和视角,支持路径居中显示、碰撞位置聚焦等。
## 核心方法
### 路径视角调整
```csharp
// 智能调整视角到路径中心
ViewpointHelper.AdjustViewpointSmart(path, collisions);
// 调整视角到路径中心,确保整个路径在视野内
ViewpointHelper.AdjustViewpointToPathCenter(path);
// 计算路径的包围盒
BoundingBox3D bounds = ViewpointHelper.CalculatePathBoundingBox(path);
```
### 碰撞视角调整
```csharp
// 计算碰撞位置的包围盒
BoundingBox3D bounds = ViewpointHelper.CalculateCollisionsBoundingBox(collisions);
// 聚焦到碰撞对象(两个对象)
ViewpointHelper.FocusOnCollision(item1, item2, viewAngleDegrees: 45, targetViewRatio: 0.25);
// 聚焦到指定位置
ViewpointHelper.FocusOnPosition(center, targetSize, viewAngleDegrees: 45, targetViewRatio: 0.25);
```
### 模型元素聚焦
```csharp
// 聚焦到单个模型元素
ViewpointHelper.FocusOnModelItem(modelItem, viewAngleDegrees: 45, targetViewRatio: 0.25);
// viewAngleDegrees: 视角角度(度)
// targetViewRatio: 目标占据视图比例0.25 = 1/4
```
### 视角保存与恢复
```csharp
// 保存当前视角
Viewpoint savedViewpoint = ViewpointHelper.SaveCurrentViewpoint();
// 恢复视角
ViewpointHelper.RestoreViewpoint(savedViewpoint);
```
## 使用示例
### 示例1生成碰撞报告前调整视角
```csharp
// 保存当前视角
Viewpoint originalViewpoint = ViewpointHelper.SaveCurrentViewpoint();
try
{
// 调整视角到路径中心
ViewpointHelper.AdjustViewpointToPathCenter(path);
// 生成截图
string screenshotPath = PathHelper.GenerateSceneScreenshot(
"collision_report", 1920, 1080, ImageFormat.Png
);
}
finally
{
// 恢复原始视角
ViewpointHelper.RestoreViewpoint(originalViewpoint);
}
```
### 示例2聚焦到碰撞位置
```csharp
// 检测到碰撞后,聚焦到碰撞对象
if (collision.Item1 != null && collision.Item2 != null)
{
ViewpointHelper.FocusOnCollision(
collision.Item1,
collision.Item2,
viewAngleDegrees: 45, // 45度视角
targetViewRatio: 0.3 // 占据视图30%
);
// 高亮碰撞对象
ModelHighlightHelper.HighlightItems("collision", new[] { collision.Item1, collision.Item2 });
}
```
### 示例3聚焦到特定模型元素
```csharp
// 用户选择了一个门对象
var doorItem = selectedItems.FirstOrDefault();
if (doorItem != null)
{
// 聚焦到该门45度斜上方视角
ViewpointHelper.FocusOnModelItem(doorItem, 45, 0.25);
LogManager.Info($"已聚焦到: {doorItem.DisplayName}");
}
```
### 示例4路径规划完成后调整视角
```csharp
// 路径规划完成
if (pathRoute.Points.Count > 0)
{
// 智能调整视角
ViewpointHelper.AdjustViewpointSmart(pathRoute, collisionResults);
// 高亮路径点
var pathItems = pathRoute.Points.Select(p => p.AssociatedModelItem).Where(i => i != null);
ModelHighlightHelper.HighlightItems("pathPreview", pathItems);
}
```
## 视角参数说明
| 参数 | 说明 | 推荐值 |
|------|------|--------|
| `viewAngleDegrees` | 相机视角角度(度) | 45°斜上方 |
| `targetViewRatio` | 目标占据视图比例 | 0.251/4视图 |
| `baseDimension` | 基准尺寸计算相机距离 | 路径长度或包围盒最大边 |
## 注意事项
1. **视角保存**:在进行视角调整前建议保存当前视角,便于后续恢复
2. **异常处理**:方法会抛出 `InvalidOperationException`(无活动文档)和 `ArgumentException`(参数错误),需要适当捕获
3. **单位转换**:内部自动使用 `UnitsConverter` 进行米和模型单位的转换
4. **标准视角**`FocusOnModelItem` 和 `FocusOnPosition` 使用模型的标准前右上视角(`FrontRightTopViewVector`
5. **性能考虑**:频繁调整视角可能影响性能,建议在关键操作后统一调整

View File

@ -0,0 +1,112 @@
# VisibilityHelper 使用指南
## 文件位置
`src/Utils/VisibilityHelper.cs`
## 用途
可见性管理器,负责 ModelItem 可见性控制的核心业务逻辑,包括显示全部、隔离显示、设置可见性等。
## 核心方法
### 显示所有项目
```csharp
// 显示所有模型项目(重置所有隐藏状态)
bool success = VisibilityHelper.ShowAllItems();
```
### 隔离显示
```csharp
// 仅显示指定的模型项集合,隐藏其他所有项
bool success = VisibilityHelper.IsolateSpecificItems(itemsToShow);
// 示例:隔离显示路径相关的对象
var pathItems = GetPathRelatedItems();
VisibilityHelper.IsolateSpecificItems(pathItems);
```
### 设置可见性
```csharp
// 设置指定模型项集合的可见性
bool success = VisibilityHelper.SetItemsVisibility(items, isHidden: true); // 隐藏
bool success = VisibilityHelper.SetItemsVisibility(items, isHidden: false); // 显示
```
## 使用示例
### 示例1隔离显示碰撞对象
```csharp
// 收集所有碰撞涉及的对象
var collisionItems = new ModelItemCollection();
foreach (var collision in collisionResults)
{
if (collision.Item1 != null) collisionItems.Add(collision.Item1);
if (collision.Item2 != null) collisionItems.Add(collision.Item2);
}
// 只显示碰撞对象
VisibilityHelper.IsolateSpecificItems(collisionItems);
// 高亮碰撞对象
ModelHighlightHelper.HighlightItems("collisions", collisionItems);
```
### 示例2分步显示/隐藏
```csharp
// 隐藏所有障碍物
var obstacles = GetObstacleItems();
VisibilityHelper.SetItemsVisibility(obstacles, isHidden: true);
// 后续操作...
// 重新显示障碍物
VisibilityHelper.SetItemsVisibility(obstacles, isHidden: false);
```
### 示例3完整的显示重置流程
```csharp
// 清除所有高亮
ModelHighlightHelper.ClearAllHighlights();
// 显示所有项目
VisibilityHelper.ShowAllItems();
// 清除剖面盒
SectionClipHelper.ClearClipBox();
LogManager.Info("视图已重置");
```
### 示例4通道可视化
```csharp
// 只显示通道相关的对象
var channelItems = GetChannelItems();
VisibilityHelper.IsolateSpecificItems(channelItems);
// 调整视角到通道
ViewpointHelper.AdjustViewpointToPathCenter(channelPath);
// 生成通道预览图
string screenshot = PathHelper.GenerateSceneScreenshot(
"channel_view", 1920, 1080, ImageFormat.Png
);
// 恢复显示所有
VisibilityHelper.ShowAllItems();
```
## 注意事项
1. **隔离显示原理**`IsolateSpecificItems` 会先显示所有,然后隐藏不需要的项目
2. **祖先节点处理**:隔离显示会自动包含选中项的所有祖先节点,确保路径完整
3. **性能考虑**:大量项目的可见性操作可能影响性能,建议批量处理
4. **与剖面盒配合**:可以与 `SectionClipHelper` 配合使用,进一步减少可见对象
5. **错误处理**:所有方法都返回 `bool` 表示成功/失败,内部已处理异常

View File

@ -1,14 +0,0 @@
{
"permissions": {
"allow": [
"Bash(dotnet build)",
"Bash(dotnet build:*)",
"WebFetch(domain:github.com)",
"WebFetch(domain:adndevblog.typepad.com)",
"Bash(.compile.bat)",
"Bash(rg:*)",
"Bash(findstr:*)"
],
"deny": []
}
}

View File

@ -1,17 +0,0 @@
---
description:
globs:
alwaysApply: true
---
本项目中设计方案和开发任何代码都要先参考Navisworks2017的API文档
每次完成一个开发任务,更新 [VERSION.md](mdc:NavisworksTransport/NavisworksTransport/NavisworksTransport/NavisworksTransport/VERSION.md) 和 [change_log.md](mdc:NavisworksTransport/NavisworksTransport/NavisworksTransport/NavisworksTransport/change_log.md)
生成的任务清单文件和其他临时文件,放在 doc/working目录下
每次分析错误,要看日志文件[NavisworksTransport_Debug.log](mdc:NavisworksTransport/NavisworksTransport/NavisworksTransport/Desktop/NavisworksTransport_Debug.log)
每次增加新的代码文件,要把文件增加到 [NavisworksTransportPlugin.csproj](mdc:NavisworksTransport/NavisworksTransport/NavisworksTransport/NavisworksTransport/NavisworksTransportPlugin.csproj)中;
这个项目的开发环境是windows生成命令时要注意
在对代码进行修改时,不能随意删掉代码中原有的和此次修改无关的代码
编译使用命令:
```sh
.\compile.bat
```

15
.gitignore vendored
View File

@ -1,6 +1,19 @@
.DS_Store
# Build outputs
bin/
obj/
.vs/
navisworks_api/
.vscode/
.idea/
.serena/
packages/
# API documentation
navisworks_api/
*.exe
*.db
.codex-temp

View File

@ -1,221 +0,0 @@
# 设计文档UI缩放增强
## 概述
本设计文档描述了如何改善Navisworks插件在4K高分辨率显示器上的用户界面显示效果。根据用户反馈插件在2K显示器上显示正常但在4K显示器上使用150%缩放实际显示为2560*1440显示过小导致按钮中的文字显示不全、组件高度不足等问题。模型分层拆分工具的弹出对话框在4K显示器上已经能够正常显示因此本设计将参考该实现来调整MainPlugin中的按钮尺寸、分组区域高度和主窗口大小。
## 设计方案
本设计采用最简单直接的方案只调整MainPlugin中的按钮尺寸、分组区域高度和主窗口大小不修改其他任何代码。主要调整内容包括
1. **增加主窗口尺寸**参考ModelSplitterDialog的尺寸适当增加MainPlugin主窗口的宽度和高度。
2. **调整按钮尺寸**参考ModelSplitterDialog中的按钮尺寸增加MainPlugin中所有按钮的宽度和高度。
3. **调整分组区域高度**增加GroupBox的高度确保能容纳调整后的按钮并有合理间距。
4. **增加路径列表和表格高度**特别增加路径编辑标签页中的路径列表和当前路径编辑表格的高度使其能容纳的行数从3行增加到4行。
### 参考ModelSplitterDialog的实现
通过分析ModelSplitterDialog.cs我们发现以下关键设置
```csharp
// 窗口大小设置
this.Size = new Size(800, 720);
// 按钮大小设置
Size = new Size(100, 30) // 主要按钮
Size = new Size(80, 30) // 次要按钮
// GroupBox高度设置
Size = new Size(350, 160) // 较大的分组区域
Size = new Size(350, 80) // 较小的分组区域
```
## 具体实现方案
### 1. 主窗口尺寸调整
将MainPlugin中的主窗口从当前尺寸调整为更大的尺寸
```csharp
// 原始尺寸
_controlPanelForm = new Form
{
Text = "物流路径规划控制面板",
Size = new Size(420, 700),
// 其他属性...
};
// 调整后的尺寸
_controlPanelForm = new Form
{
Text = "物流路径规划控制面板",
Size = new Size(500, 820), // 增加宽度和高度,以容纳更大的路径列表和表格
// 其他属性...
};
```
### 2. 按钮尺寸调整
增加MainPlugin中所有按钮的宽度和高度参考ModelSplitterDialog中的按钮尺寸
```csharp
// 原始按钮尺寸
Button button = new Button
{
Text = "按钮文本",
Size = new Size(60, 25),
// 其他属性...
};
// 调整后的按钮尺寸 - 主要按钮
Button mainButton = new Button
{
Text = "主要按钮",
Size = new Size(100, 30), // 参考ModelSplitterDialog的主要按钮尺寸
// 其他属性...
};
// 调整后的按钮尺寸 - 次要按钮
Button secondaryButton = new Button
{
Text = "次要按钮",
Size = new Size(80, 30), // 参考ModelSplitterDialog的次要按钮尺寸
// 其他属性...
};
```
### 3. 分组区域高度调整
增加GroupBox的高度确保能容纳调整后的按钮并有合理间距
```csharp
// 原始GroupBox尺寸
GroupBox groupBox = new GroupBox
{
Text = "分组标题",
Size = new Size(350, 70), // 或其他高度
// 其他属性...
};
// 调整后的GroupBox尺寸
GroupBox groupBox = new GroupBox
{
Text = "分组标题",
Size = new Size(350, 90), // 增加高度,确保有足够空间容纳更大的按钮
// 其他属性...
};
```
### 4. 路径列表和表格高度调整
特别增加路径编辑标签页中的路径列表和当前路径编辑表格的高度:
```csharp
// 原始路径列表尺寸
_pathListView = new ListView
{
// 其他属性...
Size = new Size(350, 100), // 假设原始高度为100
};
// 调整后的路径列表尺寸
_pathListView = new ListView
{
// 其他属性...
Size = new Size(350, 140), // 增加高度使其能容纳4行
};
// 原始路径点列表尺寸
_currentPathPointsListView = new ListView
{
// 其他属性...
Size = new Size(350, 100), // 假设原始高度为100
};
// 调整后的路径点列表尺寸
_currentPathPointsListView = new ListView
{
// 其他属性...
Size = new Size(350, 140), // 增加高度使其能容纳4行
};
```
## 需要修改的位置
### 1. MainPlugin.cs中的ShowCategorySelectionDialog方法
修改主窗口尺寸:
```csharp
_controlPanelForm = new Form
{
Text = "物流路径规划控制面板",
Size = new Size(500, 820), // 从(420, 700)调整为(500, 820)
// 其他属性保持不变...
};
```
### 2. 各个标签页中的分组区域和按钮
在以下方法中找到所有GroupBox和Button的Size属性并进行调整
- CreateModelSettingsTab
- CreatePathEditingTab
- CreateAnimationControlTab
- CreateSystemManagementTab
例如:
```csharp
// 原始代码
GroupBox groupBox = new GroupBox
{
Text = "分组标题",
Size = new Size(350, 70),
// 其他属性...
};
Button button = new Button
{
Text = "按钮文本",
Size = new Size(60, 25),
// 其他属性...
};
// 修改为
GroupBox groupBox = new GroupBox
{
Text = "分组标题",
Size = new Size(350, 90), // 增加高度
// 其他属性保持不变...
};
Button button = new Button
{
Text = "按钮文本",
Size = new Size(100, 30), // 或 Size = new Size(80, 30),根据按钮重要性
// 其他属性保持不变...
};
```
### 3. 路径编辑标签页中的列表
特别关注CreatePathEditingTab方法中的路径列表和当前路径编辑表格
```csharp
// 找到路径列表和当前路径编辑表格的定义
_pathListView.Size = new Size(350, 140); // 增加高度
_currentPathPointsListView.Size = new Size(350, 140); // 增加高度
// 同时调整包含这些列表的GroupBox高度
pathListGroupBox.Size = new Size(350, 200); // 从160增加到200
currentPathGroupBox.Size = new Size(350, 290); // 从250增加到290
```
## 实现注意事项
1. **只修改尺寸数据**只调整窗口、分组区域、列表和按钮的Size属性不修改其他任何代码。
2. **保持一致性**:确保所有按钮的调整保持一致,主要按钮使用(100, 30),次要按钮使用(80, 30)。
3. **合理间距**确保增加GroupBox高度后内部控件有足够的间距不会显得拥挤。
4. **路径列表特别关注**确保路径列表和当前路径编辑表格能容纳4行数据提高用户体验。
5. **最小化修改**只针对MainPlugin.cs文件进行修改不涉及其他文件。

View File

@ -1,67 +0,0 @@
# 需求文档
## 简介
UI缩放增强功能旨在改善插件在高分辨率4K显示器上的用户界面显示效果。目前插件的UI元素按钮、文本、间距在2K显示器上显示正常但在4K显示器上显示过小导致按钮中的文字显示不全、组件高度不足等问题。模型分层拆分工具的弹出对话框在4K显示器上已经能够正常显示因此本功能将参考该实现来调整插件主窗口及其标签页。
## 需求
### 需求1插件主窗口的UI缩放
**用户故事:** 作为使用4K显示器的插件用户我希望插件主窗口能够适当缩放以便我能清晰地看到所有UI元素而不会出现文字被截断的情况。
#### 验收标准1
1. 当插件在4K显示器上启动时插件主窗口应自动调整其大小以保持可读性。
2. 当插件在4K显示器上显示时按钮和标签中的所有文本应完全可见不会被截断。
3. 当插件在4K显示器上显示时UI元素之间的间距应按比例调整以保持视觉层次结构。
4. 当插件在2K显示器上显示时UI应保持其当前的适当大小。
5. 当插件窗口大小改变时所有UI元素应保持其相对比例和可读性。
### 需求2标签页控件的UI缩放
**用户故事:** 作为使用4K显示器的插件用户我希望所有标签页及其控件能够适当缩放以便我能有效地与它们交互。
#### 验收标准2
1. 当任何标签页在4K显示器上显示时标签页内的所有控件应具有适当的大小和间距。
2. 当"类别设置"标签页在4K显示器上显示时所有列表视图、组合框和按钮应具有适当的大小。
3. 当"路径编辑"标签页在4K显示器上显示时所有路径编辑控件应具有适当的大小并完全可用。
4. 当"检测动画"标签页在4K显示器上显示时所有动画控件应具有适当的大小并完全可用。
5. 当"系统管理"标签页在4K显示器上显示时所有系统管理控件应具有适当的大小并完全可用。
### 需求3字体缩放
**用户故事:** 作为使用4K显示器的插件用户我希望插件中的所有文本都能适当调整大小以便我能够轻松阅读。
#### 验收标准3
1. 当插件在4K显示器上显示时所有字体应按比例缩放以保持可读性。
2. 当按钮在4K显示器上显示文本时字体大小应按比例增加以确保文本完全可见。
3. 当标签在4K显示器上显示文本时字体大小应按比例增加以确保可读性。
4. 当列表视图在4K显示器上显示文本时字体大小应按比例增加以确保可读性。
5. 当插件在2K显示器上显示时字体大小应保持其当前的适当大小。
### 需求4DPI感知实现
**用户故事:** 作为插件开发人员我希望在插件中实现适当的DPI感知功能以便它能够自动适应不同的显示分辨率。
#### 验收标准4
1. 当插件启动时它应检测当前显示器的DPI设置。
2. 当插件检测到高DPI设置时它应自动调整UI缩放因子。
3. 当插件在具有不同DPI设置的显示器之间移动时它应相应地调整其缩放。
4. 当系统DPI设置更改时插件应在下次启动时适当响应。
5. 如果插件无法确定DPI设置则应默认为标准缩放不会出现错误。
### 需求5一致的UI元素比例
**用户故事:** 作为插件用户我希望UI元素在不同显示分辨率下保持一致的比例以便界面保持熟悉和可用。
#### 验收标准5
1. 当插件针对不同分辨率进行缩放时UI元素的相对大小应保持一致。
2. 当插件针对不同分辨率进行缩放时UI元素的布局和定位应保持一致。
3. 当列表视图在不同分辨率上显示时,列宽应按比例缩放。
4. 当分组框在不同分辨率上显示时,其内容应适当排列,不会重叠。
5. 当插件在任何支持的分辨率上显示时,滚动条应仅在必要时出现。

View File

@ -1,4 +1,4 @@
{
"dotnet.preferCSharpExtension": true,
"dotnet.defaultSolution": "NavisworksTransport.sln"
"dotnet.defaultSolution": "TransportPlugin.sln"
}

587
AGENTS.md Normal file
View File

@ -0,0 +1,587 @@
# AGENTS.md
本文件面向后续会话中的 AI 编码助手。目标不是写一份“大而全”的历史说明,而是让新会话能快速理解:
- 项目现在在做什么
- 哪些架构已经稳定
- 哪些开发原则不能再破坏
- 遇到问题时优先看哪里
如果本文件与更细的专项文档冲突,优先参考:
1. `doc/working/current-engineering-state.md`
2. `doc/design/2026/coordinate-system-canonical-space-design.md`
3. `doc/design/2026/NavisworksAPI使用方法.md`
---
## 1. 项目现状
**NavisworksTransport** 是一个面向 **Autodesk Navisworks Manage 2026** 的物流路径规划与动画仿真插件。
当前已经不只是“自动寻路”项目,而是一套同时覆盖以下能力的工程:
- 物流属性与对象分类
- 地面路径、吊装路径、Rail 路径编辑与可视化
- 终端安装仿真
- 动画播放、起点落位、终点诊断
- ClashDetective 碰撞检测与恢复
- 路径、检测记录、批处理相关数据存储
### 当前重点功能
- `Ground / Hoisting / Rail` 三类路径
- 真实物体与虚拟物体的起点摆放、动画姿态、通行空间
- `YUp / ZUp` 两类宿主模型坐标系支持
- 终端安装仿真中的:
- 端面三点分析
- 光轴辅助线
- 安装面双点确定
- 安装点与 Rail 法向联动
---
## 2. 技术栈与构建
- 平台Navisworks Manage 2026
- 框架:.NET Framework 4.8
- 语言C# 7.3
- 架构x64
- UIWPF + Navisworks DockPane
- 测试MSTest
### 关键脚本
- `compile.bat`
- `run-unit-tests.bat`
- `deploy-plugin.bat`
### 极重要的执行顺序
构建和部署必须严格按顺序执行:
1. `./run-unit-tests.bat`(需要时)
2. `./compile.bat`
3. **等待编译完整结束并确认成功**
4. `./deploy-plugin.bat`
不要并行执行编译和部署。否则很容易把旧 DLL 部署到插件目录。
### 并行执行边界
后续会话中的 AI 助手必须把命令分成两类:
- **允许并行的纯读取操作**
- `rg`
- `Get-Content`
- `ls / Get-ChildItem`
- 读取日志
- 查询状态
- **禁止并行的产出型/宿主相关操作**
- `run-unit-tests.bat`
- `compile.bat`
- `deploy-plugin.bat`
- 启动/关闭 Navisworks
- 会占用 DLL、修改 `bin/obj`、写插件部署目录、依赖上一步产物的任何命令
硬约束:
- 只有“纯读取、无副作用、且互不依赖”的命令才允许并行
- 只要命令会生成、覆盖、部署、锁定文件、启动宿主进程,必须串行执行
- 对本仓库,默认把 `run-unit-tests -> compile -> deploy -> start Navisworks` 视为**单通道流水线**
- 不允许把这条流水线放进任何并行工具里,即使只是为了节省时间
### 插件部署目录
- `C:\ProgramData\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin\`
日志目录:
- `C:\ProgramData\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin\logs\debug.log`
---
## 3. 目录与职责
### 核心目录
- `src/Core/`
- 插件主入口、路径管理、动画、碰撞、渲染、配置
- `src/UI/WPF/`
- 视图、ViewModel、交互命令
- `src/Utils/`
- 单位、几何、坐标、变换、日志等公共工具
- `src/PathPlanning/`
- 网格、A*、路径几何与优化
- `UnitTests/`
- 数学层、工具层、姿态层的回归测试
### 当前最关键的文件
- `src/Core/Animation/PathAnimationManager.cs`
- `src/Core/VirtualObjectManager.cs`
- `src/Core/PathPointRenderPlugin.cs`
- `src/UI/WPF/ViewModels/AnimationControlViewModel.cs`
- `src/UI/WPF/ViewModels/PathEditingViewModel.cs`
- `src/Utils/CoordinateSystem/HostCoordinateAdapter.cs`
- `src/Utils/CoordinateSystem/CanonicalPlanarPoseBuilder.cs`
- `src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs`
- `src/Utils/CoordinateSystem/CanonicalTrackedPositionResolver.cs`
- `src/Utils/CoordinateSystem/RotatedObjectExtentHelper.cs`
- `src/Utils/RailPathPoseHelper.cs`
- `src/Utils/ModelItemTransformHelper.cs`
---
## 4. 当前稳定架构
### 4.1 坐标系三层语义
以后统一只使用这三种说法:
- **宿主坐标系**
- Navisworks 文档坐标系
- `YUp``ZUp`
- UI 输入输出、日志、拾取结果都按这一层解释
- **内部坐标系**
- 项目内部统一使用的 `Canonical Space`
- 固定 `ZUp`
- 纯数学姿态和几何计算优先在这里完成
- **资产坐标系**
- 只属于插件自带资源
- 当前主要是:
- 虚拟物体 `unit_cube.nwc`
- 参考杆 `unit_cylinder.nwc`
禁止再使用“本地坐标系”这种含糊说法。
### 4.1.2 坐标命名规则
以后凡是变量名、字段名、日志名里出现 `X/Y/Z`、正负轴、forward/up/side 等方向语义,必须在名字上直接带出所属坐标系,避免只看名字时无法判断语义层。
允许的前缀示例:
- `Host...`
- 宿主坐标系
- `Canonical...`
- 内部坐标系
- `Asset...`
- 资产坐标系
- `Local...`
- 仅当这里明确表示“对象自身局部轴”时才允许使用
正负轴命名示例:
- `LocalPositiveX`
- `LocalNegativeY`
- `HostPositiveZ`
- `CanonicalPositiveX`
- `AssetPositiveY`
方向/向量命名示例:
- `hostForward`
- `hostUp`
- `canonicalForward`
- `assetUp`
- `localXAxis`
- `localZAxis`
禁止继续使用这类脱离坐标系语义的名字:
- `PositiveX`
- `NegativeZ`
- `xAxis`
- `upAxis`
- `forwardAxis`
除非变量名里已经明确出现了所属坐标系前缀。
目标是做到:只看名字,就能知道这个方向、轴、向量到底属于宿主、内部、资产,还是对象自身局部轴。
### 4.1.3 对象局部轴业务映射
以后关于 `Local...`、前进轴、up 轴、side 轴,统一优先使用这个说法:
- **对象局部轴业务映射**
- 它不是第四套全局坐标系
- 也不是几何天然自带的“真理”
- 它表示:在当前业务链路里,我们准备把对象自身哪根局部轴解释成:
- `forward`
- `up`
- `side`
这个概念的边界必须非常明确:
- 它是**业务解释层**
- 不是宿主坐标系
- 不是内部 `Canonical Space`
- 不是插件资源资产坐标系
- 它不能脱离上下文单独存在
- `up` 往往要结合宿主 `up` 语义来解释
- `forward` 往往取决于:
- fragment 代表姿态解释
- 资源默认轴约定
- 用户选择
- 当前路径类型
为什么必须保留这层概念:
- 路径姿态求解必须回答:
- 对象哪根局部轴去对齐路径 `forward`
- 对象哪根局部轴去对齐目标 `up`
- 如果没有这层映射,下面这些都无法稳定定义:
- `Ground / Hoisting / Rail` 起点姿态
- 逐帧姿态
- `Rail` 角度修正
- 通行空间尺寸投影
- 终点原始姿态保持
- `Rail` 平移模式的终点原位搬运
当前项目里的硬约束:
- 不要把“对象局部轴业务映射”写成“对象自身天然就有 forward/up 定义”
- 对真实物体:
- 不能简单把 `ModelItem.Transform` 当成可靠的局部轴真值
- 应优先通过 fragment 代表姿态 + `Fragment默认Up` 去解释
- 对虚拟物体:
- 对象局部轴业务映射通常来自明确的资产轴约定
- 如果只是讨论对象自身局部轴语义,允许继续使用 `Local...` 命名
- 但文档、日志、设计讨论里优先说“对象局部轴业务映射”
### 4.1.1 Quaternion / Rotation3D 固定定义
这是项目级硬约束,不允许在任何修复中重新猜测、重新验证或临时改口:
- Navisworks `Rotation3D(double, double, double, double)` 的参数顺序固定为:
- `x, y, z, w`
- `Rotation3D.A/B/C/D` 与 quaternion 分量的对应关系固定为:
- `A = x`
- `B = y`
- `C = z`
- `D = w`
- 当代码里已经拿到 quaternion `(qx, qy, qz, qw)` 时,唯一允许的构造方式是:
```csharp
var rotation = new Rotation3D(qx, qy, qz, qw);
```
- 严禁再写成:
```csharp
var rotation = new Rotation3D(qw, qx, qy, qz); // 错误
```
- 以后遇到姿态问题,禁止再把故障归因到 “Navisworks 四元数分量顺序可能又不一样了”。
- 如果问题涉及真实物体或 fragment 参考姿态的轴语义,优先检查:
- fragment 三轴的业务解释是否正确
- 参考姿态是否应该用显式三轴而不是只传 quaternion
- 宿主坐标系 / 内部坐标系 / 资产坐标系 是否被混用
### 4.2 真实物体 vs 虚拟物体
这是当前架构里最容易被改坏的地方。
#### 真实物体
- 没有独立资产坐标系
- 可以视为直接生活在宿主坐标系里
- 角度调整对话框里的 `X/Y/Z`,对真实物体应按**宿主坐标系**消费
#### 真实物体参考姿态
- 真实物体不要再直接依赖 `ModelItem.Transform.Factor().Rotation`
- 对很多 Revit 导入件,这个值是单位旋转,不代表真实显示姿态
- 真实物体参考姿态应优先来自 fragment 代表姿态
- `Fragment默认Up` 的用途是:
- 把 fragment 参考框架解释成当前宿主坐标语义下的真实姿态
- 不是仅仅挑一个“竖直候选轴”
- 在真实物体链路里必须先完成“fragment 姿态解释”,再谈:
- 前进方向
- 路径对齐
- 角度调整
- `_trackedRotation` 对真实物体必须优先跟踪“解释后的真实参考姿态”,不能回退成 `Transform` 的单位旋转
#### 虚拟物体
- 有明确资产坐标系
- 当前依赖 `unit_cube.nwc` 资源
- 资源必须满足:
- 几何中心在原点
- 原点处 `BoundingBox.Center == (0,0,0)`
- 如果生产环境里虚拟物体固定偏移,先检查部署的 `unit_cube.nwc` 是否为最新资源,不要先怀疑代码
### 4.3 路径姿态链
#### Ground / Hoisting
- 走平面姿态链
- 已禁止偷偷退回旧 `yaw` 方案
- 起点、逐帧、终点、通行空间,必须共享同一套尺寸语义
- `Ground + 真实物体` 的业务跟踪点固定为**原始包围盒中心**
- 旋转后的实时 `BoundingBox.Center` 只允许用于:
- 诊断旋转后漂移
- 计算当帧补偿
- 校验补偿结果
- 不允许把实时 `BoundingBox.Center` 直接当成:
- 起点目标跟踪点
- 逐帧业务跟踪点
- 碰撞记录主语义跟踪点
- 如果 `Ground` 出现“起点正确但越走越偏”或“起点偏了但拐弯后偏差反而减小”,优先检查是否把实时包围盒中心误当成了业务跟踪点
#### Rail
- 不能像平面路径那样在宿主空间随意补旋转
- 必须并入 `canonical -> rail pose`
- `Rail 0°` 基线必须稳定,不能被角度修正逻辑污染
- `Rail` 真实物体不应再退回默认 `PositiveX / PositiveY`
- 三类路径都必须先复用同一个“对象姿态解释层”:
- `Ground / Hoisting`
- `forward = 路径方向`
- `up = 宿主 up`
- `Rail`
- `forward = rail 切向`
- `up = rail 法向 / preferred normal`
- 统一的是“对象参考姿态来源与解释方式”,不是把三类路径都强行改成同一个 `up`
### 4.4 通行空间与物体姿态的关系
通行空间、起点落位、逐帧位置、终点诊断,必须尽量共用同一套尺寸/法线语义。
典型错误信号:
- 通行空间正确,但物体陷入地面
- 物体姿态正确,但通行空间轴搞反
- `YUp``Y/Z` 表现互换
遇到这类问题,优先检查:
- 是否一条链用了“原始高度”
- 另一条链用了“旋转后法线尺寸”
- `Rail` 真实物体尤其要检查:
- 起点/逐帧中心偏移是否仍在用原始 `objectHeight`
- 通行空间是否仍在用旧 `RailAssetConvention`
- 通行空间、路径偏移、最终姿态是否共享同一套“最终姿态尺寸语义”
### 4.5 终端安装仿真
当前稳定链路是:
1. 捕获终点箱体
2. 分析端面3点
3. 选择安装点(当前已是 2 点确定安装面)
4. 生成辅助线 / 安装面 / 安装点
5. 取起点并生成路径
当前 UI 上:
- 终端安装仿真是独立区块
- 不再夹在路径编辑中间
- 安装方式选择只保留一处
---
## 5. 开发原则
### 5.1 彻底禁止 fallback
这是当前项目的第一编码原则,优先级高于其他“先跑起来”的考虑。
不允许出现以下行为:
- 新姿态链失败时,静默退回旧姿态链
- 新变换链失败时,静默退回旧变换链
- 正确姿势/正确位置/正确尺寸语义拿不到时,用“差不多”的旧值、缓存值、默认值顶上
- 只打印一条 warning然后继续使用错误语义把流程跑完
尤其禁止这类做法:
- 偷偷退回旧 `yaw`
- 偷偷用硬编码 `Z-up`
- 偷偷在错误时给默认值掩盖问题
- 偷偷在新链失败时自动掉回旧链
- 读不到当前实际几何旋转时,回退到 `_trackedRotation`
- 读不到当前真实姿态时,回退到 `referenceRotation`
- 读不到当前显示姿态时,回退到 `ModelItem.Transform`
正确做法只有两种:
1. 在进入新链前把前置条件补齐
2. 直接暴露失败并修根因
不允许把“旧链兜底”当成正式实现的一部分。
### 5.2 不向后兼容
项目只针对 Navisworks 2026。不要写旧版本兼容代码。
### 5.3 临时补丁不是正式实现
为定位问题临时加入的:
- 强制刷新
- 额外同步
- 再调一次方法
- UI 和稀泥补丁
如果最后证明它不是真正根因,修完后必须删掉,不能残留在正式代码里。
### 5.4 优先复用现有工具
尤其优先看:
- `UnitsConverter`
- `GeometryHelper`
- `LogManager`
- `HostCoordinateAdapter`
- `Canonical*` 姿态工具
- `ModelItemTransformHelper`
- `RailPathPoseHelper`
不要在业务层手搓一套新的矩阵、坐标变换或尺寸投影公式。
### 5.5 测试优先于猜测
对几何/旋转/坐标问题,优先顺序应是:
1. 看日志
2. 日志不够就补日志
3. 先补单元测试
4. 再改代码
不要在没有锁住语义之前反复试错改实现。
---
## 6. 单位原则
所有路径计算、网格计算、包络尺寸、位移偏移,内部一律使用**模型单位**。
命名规则:
- 米单位:变量名以 `InMeters` 结尾
- 模型单位:变量名不加后缀
不要混用。
优先使用:
- `UnitsConverter.GetMetersToUnitsConversionFactor()`
- `UnitsConverter.GetUnitsToMetersConversionFactor()`
- `UnitsConverter.ConvertToMeters(...)`
- `UnitsConverter.ConvertFromMeters(...)`
---
## 7. 常见问题的排查入口
### 7.1 虚拟物体固定偏差
先查:
1. 部署目录下的 `resources\\unit_cube.nwc`
2. 原点处 `BoundingBox.Center` 是否是 `(0,0,0)`
3. 再查起点/归位代码
### 7.2 真实物体旋转轴不对
先区分:
- 目标姿态算错
- 还是 Navisworks 应用姿态错
优先看:
- `[动画姿态入口]`
- `[模型增量姿态]`
### 7.3 吊装路径不显示
优先检查渲染链里是否有退化段/零长度段导致整条渲染失败。
### 7.4 “设为终点直接结束”后列表还是空
优先看 `UIStateManager` 队列消费是否吃掉了后续 UI 事件,不要先怀疑坐标系。
---
## 8. 资源与部署注意事项
### 8.1 虚拟物体资源
当前部署脚本会部署:
- `resources\\unit_cube.nwc`
- `resources\\unit_cylinder.nwc`
虚拟物体和参考杆问题,必须同时检查:
- 仓库里的资源
- `bin\\x64\\Release\\resources`
- 插件部署目录下的 `resources`
### 8.2 deploy-plugin.bat 当前规则
- 走白名单部署
- 不部署测试 DLL
- 不部署 Navisworks 自带 API DLL
不要把它改回“复制所有 dll”。
---
## 9. 推荐阅读顺序
新会话接手本项目时,推荐顺序:
1. 本文件 `AGENTS.md`
2. `doc/working/current-engineering-state.md`
3. `doc/design/2026/coordinate-system-canonical-space-design.md`
4. `doc/design/2026/NavisworksAPI使用方法.md`
5. 相关专项 skill
- `.agents/skills/nw-api/SKILL.md`
- `.agents/skills/geometry-transform/SKILL.md`
---
## 10. 当前对子代理和 skill 的约定
仓库中已经有几何/变换专项 skill
- `.agents/skills/geometry-transform/SKILL.md`
它负责沉淀以下内容:
- 坐标系变换
- 物体姿态
- 物体位移与归位
- 虚拟物体资源定位
- 通行空间几何
- Navisworks 变换 API 使用规则
后续凡是几何/旋转/位移问题,优先沿这套 skill 与工具链继续维护,不要重新发明一套术语和流程。
---
## 11. 最后的硬约束
1. 不要混淆宿主坐标系、内部坐标系、资产坐标系
2. 不要在宿主空间随意补旋转,先判断是否应并入现有姿态链
3. 不要让虚拟物体和真实物体共享含糊的状态分支
4. 不要让通行空间和真实物体使用两套不同的尺寸语义
5. 不要把临时补丁留在正式实现里
6. 不要绕过测试直接改几何核心逻辑
如果你要改动:
- `PathAnimationManager`
- `VirtualObjectManager`
- `PathPointRenderPlugin`
- `AnimationControlViewModel`
- `HostCoordinateAdapter`
- `Canonical*PoseBuilder`
- `ModelItemTransformHelper`
请默认这是高风险改动,先补测试,再动实现。

File diff suppressed because it is too large Load Diff

113
CLAUDE.md
View File

@ -1,113 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
NavisworksTransport is a Navisworks 2017 plugin (v0.1.8) for logistics path planning and transportation conflict detection in 3D building models. The plugin enables route optimization, collision detection, and animated object movement along defined paths.
## Build Commands
- **Build**: `compile.bat` - Automatically detects MSBuild (VS 2017/2019/2022) or falls back to `dotnet build`
- **Target**: .NET Framework 4.6.2, x64 platform
- **Output**: Direct deployment to `%PROGRAMFILES%\Autodesk\Navisworks Manage 2017\Plugins\NavisworksTransportPlugin\`
## Architecture Overview
### Core Plugin Structure
- **MainPlugin.cs**: Primary AddInPlugin entry point with ribbon UI
- **PathClickToolPlugin.cs**: ToolPlugin for 3D mouse interaction
- **PathPointRenderPlugin.cs**: RenderPlugin for 3D visualization
### Manager Components
- **PathPlanningManager.cs**: Central path planning and route management logic
- **PathAnimationManager.cs**: TimeLiner integration for object movement animation
- **CoordinateConverter.cs**: 2D map overlay to 3D world coordinate conversion
- **CategoryAttributeManager.cs**: COM API wrapper for logistics attribute management
- **VisibilityManager.cs**: Layer visibility and model filtering control
- **ModelSplitterManager.cs**: Model layer separation and export functionality
### Data and Utilities
- **PathPlanningModels.cs**: Core data structures (PathEditState, PathRoute, PathPoint)
- **PathDataManager.cs**: Serialization and persistence using Newtonsoft.Json
- **GeometryExtractor.cs**: 3D geometry analysis and bounding box calculations
- **LogManager.cs**: Centralized logging with global exception handling
### UI Components
- **LogisticsPropertyEditDialog.cs**: Property editing interface
- **ModelSplitterDialog.cs**: Model splitting configuration UI
## Key Technical Details
### Navisworks API Integration
- Uses dual API approach: Native API (`Autodesk.Navisworks.Api`) + COM API (`Autodesk.Navisworks.ComApi`)
- COM API required for attribute persistence and TimeLiner operations
- Plugin types: AddInPlugin (main), ToolPlugin (interaction), RenderPlugin (visualization)
### Exception Handling
Global exception handling implemented in MainPlugin with:
- AppDomain.CurrentDomain.UnhandledException
- Application.ThreadException
- Automatic recovery and user-friendly error reporting
### Coordinate Systems
- Supports 2D map overlay on 3D models with dynamic zoom/pan
- Margin-based boundary calculations for click precision
- Transform chains for coordinate conversion between spaces
### Logistics Categories
Eight predefined logistics element types:
- 门 (Doors), 电梯 (Elevators), 楼梯 (Stairs), 通道 (Channels)
- 障碍物 (Obstacles), 装卸区 (Loading Zones), 停车区 (Parking), 检查点 (Checkpoints)
## Development Guidelines
### Language Preference
- **使用中文进行所有交流和代码注释**
- 与用户交流时优先使用中文
- 代码注释和文档说明使用中文
### File Organization
- Core managers handle specific functionality areas
- Models file contains shared data structures
- UI dialogs are separate form classes
- Utilities (logging, geometry, data) are standalone classes
### Plugin Registration Pattern
```csharp
[Plugin("NavisworksTransport.PluginName", "YourDeveloperID")]
[AddInPlugin(AddInLocation.AddIn)]
```
### Error Handling Best Practices
- Use LogManager for consistent logging
- Implement try-catch blocks around Navisworks API calls
- **写任何与Navisworks相关的代码都要查在doc/navisworks_api目录下的官方API文档和示例代码**
- Provide meaningful error messages to users
- Use COM API error codes for troubleshooting
### Dependencies
- **System.Windows.Forms**: UI dialogs and controls
- **System.Drawing**: Graphics and coordinate operations
## Testing and Deployment
- Manual testing required through Navisworks Manage 2017
- Plugin automatically deploys to Navisworks plugin directory during build
- Restart Navisworks after compilation to load new plugin version
- Use LogManager output for debugging and troubleshooting

View File

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">x64</Platform>
<ProjectGuid>{7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>NavisworksTransport.UnitTests</RootNamespace>
<AssemblyName>NavisworksTransport.UnitTests</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x64</PlatformTarget>
<LangVersion>7.3</LangVersion>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>bin\x64\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x64</PlatformTarget>
<LangVersion>7.3</LangVersion>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<ItemGroup>
<Reference Include="Autodesk.Navisworks.Api">
<HintPath>..\..\..\..\Program Files\Autodesk\Navisworks Manage 2026\Autodesk.Navisworks.Api.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework">
<HintPath>packages\MSTest.TestFramework.3.0.4\lib\net462\Microsoft.VisualStudio.TestPlatform.TestFramework.dll</HintPath>
</Reference>
<Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions">
<HintPath>packages\MSTest.TestFramework.3.0.4\lib\net462\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Numerics" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="UnitTests\Core\PathHelperTests.cs" />
<Compile Include="UnitTests\Core\SelectionClipBoxLockStateTests.cs" />
<Compile Include="UnitTests\Core\PathPlanningManagerHoistingCompletionTests.cs" />
<Compile Include="UnitTests\Core\PathPersistenceTests.cs" />
<Compile Include="UnitTests\Core\PathRouteCloneTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\AutoPathPlanningCoordinateSemanticsTests.cs" />
<Compile Include="UnitTests\Integration\AutoPathGridGenerationAutomationTests.cs" />
<Compile Include="UnitTests\Integration\NavisworksTestAutomationClient.cs" />
<Compile Include="UnitTests\Integration\VirtualCollisionAutomationTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\HostCoordinateAdapterTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\CanonicalRailPoseBuilderTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\CanonicalPlanarPoseBuilderTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\ObjectSpaceOrientationHelperTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\ObjectStartPlacementRequestTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\CanonicalRailOffsetResolverTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\CanonicalTrackedPositionResolverTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\GroundPathObjectLiftOffsetTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\GroundPassageSpaceOffsetTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\FragmentRepresentativePoseHelperTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\HoistingCoordinateHelperTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\HoistingRealObjectPoseHelperTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\ModelAxisConventionTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\ProjectReferenceFrameTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\RealObjectPlanarPoseSolverTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\RealObjectProjectedExtentResolverTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\RealObjectReferencePoseResolverTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\RealObjectRailAxisConventionResolverTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\RealObjectRailExtentResolverTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\RotatedObjectExtentHelperTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\PathTargetFrameResolverTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\AssemblyEndFaceAnalyzerTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\AssemblyInstallationReferenceBuilderTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\RailAssemblyWorkflowContextTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\ViewpointHelperTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\PathPointVisualizationTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\FragmentDefaultUpContextTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\VirtualGroundPoseCharacterizationTests.cs" />
<Compile Include="UnitTests\Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="TransportPlugin.csproj">
<Project>{1A0124F6-3DEB-4153-8760-F568AD9393EE}</Project>
<Name>TransportPlugin</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -1,25 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36203.30
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NavisworksTransportPlugin", "NavisworksTransportPlugin.csproj", "{1A0124F6-3DEB-4153-8760-F568AD9393EE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1A0124F6-3DEB-4153-8760-F568AD9393EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1A0124F6-3DEB-4153-8760-F568AD9393EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1A0124F6-3DEB-4153-8760-F568AD9393EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1A0124F6-3DEB-4153-8760-F568AD9393EE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {EB39ED46-3E01-481E-AE51-425495B56837}
EndGlobalSection
EndGlobal

View File

@ -1,101 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{1A0124F6-3DEB-4153-8760-F568AD9393EE}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>NavisworksTransport</RootNamespace>
<AssemblyName>NavisworksTransportPlugin</AssemblyName>
<TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\..\..\..\Program Files\Autodesk\Navisworks Manage 2017\Plugins\NavisworksTransportPlugin\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Autodesk.Navisworks.Api">
<HintPath>..\..\..\..\Program Files\Autodesk\Navisworks Manage 2017\Autodesk.Navisworks.Api.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Autodesk.Navisworks.ComApi">
<HintPath>..\..\..\..\Program Files\Autodesk\Navisworks Manage 2017\Autodesk.Navisworks.ComApi.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Autodesk.Navisworks.Interop.ComApi">
<HintPath>..\..\..\..\Program Files\Autodesk\Navisworks Manage 2017\Autodesk.Navisworks.Interop.ComApi.dll</HintPath>
<EmbedInteropTypes>False</EmbedInteropTypes>
<Private>False</Private>
</Reference>
<Reference Include="Autodesk.Navisworks.Interop.ComApiAutomation">
<HintPath>..\..\..\..\Program Files\Autodesk\Navisworks Manage 2017\Autodesk.Navisworks.Interop.ComApiAutomation.dll</HintPath>
<EmbedInteropTypes>False</EmbedInteropTypes>
<Private>False</Private>
</Reference>
<Reference Include="Autodesk.Navisworks.Timeliner">
<HintPath>..\..\..\..\Program Files\Autodesk\Navisworks Manage 2017\Autodesk.Navisworks.Timeliner.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Autodesk.Navisworks.Clash">
<HintPath>..\..\..\..\Program Files\Autodesk\Navisworks Manage 2017\Autodesk.Navisworks.Clash.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="src\MainPlugin.cs" />
<Compile Include="src\CategoryAttributeManager.cs" />
<Compile Include="src\VisibilityManager.cs" />
<Compile Include="src\CoordinateConverter.cs" />
<Compile Include="src\PathDataManager.cs" />
<Compile Include="src\PathPlanningManager.cs" />
<Compile Include="src\PathPlanningModels.cs" />
<Compile Include="src\GeometryExtractor.cs" />
<Compile Include="src\LogManager.cs" />
<Compile Include="src\PathClickToolPlugin.cs" />
<Compile Include="src\PathPointRenderPlugin.cs" />
<Compile Include="src\PathAnimationManager.cs" />
<Compile Include="src\ClashDetectiveIntegration.cs" />
<Compile Include="src\ClashDetectiveIntegrationTest.cs" />
<Compile Include="src\ModelSplitterManager.cs" />
<Compile Include="src\FloorDetector.cs" />
<Compile Include="src\AttributeGrouper.cs" />
<Compile Include="src\NavisworksFileExporter.cs" />
<Compile Include="src\TimeLinerIntegrationManager.cs" />
<Compile Include="src\ModelSplitterDialog.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="src\LogisticsPropertyEditDialog.cs">
<SubType>Form</SubType>
</Compile>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -5,12 +5,12 @@ using System.Runtime.InteropServices;
// 有关程序集的一般信息由以下
// 控制。更改这些特性值可修改
// 与程序集关联的信息。
[assembly: AssemblyTitle("NavisworksTransportPlugin")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyTitle("TransportPlugin")]
[assembly: AssemblyDescription("Navisworks物流运输路径规划插件")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("NavisworksTransportPlugin")]
[assembly: AssemblyCopyright("Copyright © 2025")]
[assembly: AssemblyProduct("TransportPlugin")]
[assembly: AssemblyCopyright("Copyright © 2024")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
@ -29,5 +29,7 @@ using System.Runtime.InteropServices;
// 生成号
// 修订号
//
// 主版本和次版本手动维护Build和Revision在编译时自动更新
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: InternalsVisibleTo("NavisworksTransport.UnitTests")]

63
Properties/Resources.Designer.cs generated Normal file
View File

@ -0,0 +1,63 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 此代码由工具生成。
// 运行时版本:4.0.30319.42000
//
// 对此文件的更改可能会导致不正确的行为,并且如果
// 重新生成代码,这些更改将会丢失。
// </auto-generated>
//------------------------------------------------------------------------------
namespace NavisworksTransport.Properties {
using System;
/// <summary>
/// 一个强类型的资源类,用于查找本地化的字符串等。
/// </summary>
// 此类是由 StronglyTypedResourceBuilder
// 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
// 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
// (以 /str 作为命令选项),或重新生成 VS 项目。
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// 返回此类使用的缓存的 ResourceManager 实例。
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("NavisworksTransport.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// 重写当前线程的 CurrentUICulture 属性,对
/// 使用此强类型资源类的所有资源查找执行重写。
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
}
}

101
Properties/Resources.resx Normal file
View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -1,78 +1,60 @@
# NavisworksTransport
Navisworks 2017运输冲突检测插件专用于物流路径规划
Navisworks Manage 2026 物流路径规划与运输冲突检测插件
## 功能特性
### 已实现功能(第一阶段)
- ✅ **类别属性分配**:为模型项目添加物流类别属性(门、电梯、楼梯、通道、障碍物)
- ✅ **批量处理**:支持同时为多个选中项目设置属性
- ✅ **用户友好界面**:简洁的按钮式对话框
- ✅ **COM API集成**使用Navisworks COM API确保属性正确添加
### 计划功能(后续阶段)
- 🔄 模型分层转换和可见性控制
- 🔄 导航地图构建
- 🔄 A*路径规划算法
- 🔄 动态碰撞检测
- 🔄 动画和时间线集成
- 🔄 DELMIA数据导出
- ✅ **物流属性管理**:类别属性分配(门、电梯、楼梯、通道、障碍物等)、自定义类别
- ✅ **路径规划**A*自动寻路、曲线化路径、空轨路径、吊装路径
- ✅ **动画仿真**虚拟车辆动画、物体沿路径转向、TimeLiner集成
- ✅ **碰撞检测**ClashDetective集成、预计算分析、批处理队列
- ✅ **可视化**:通行空间、网格可视化、路径剖面盒、视角聚焦
- ✅ **数据管理**SQLite数据库、历史记录、导入导出(JSON/CSV/XML/DELMIA)
## 系统要求
- Windows 7 或更高版本
- Navisworks Manage 2017
- .NET Framework 4.6.2
- Windows 10/11
- Navisworks Manage 2026
- .NET Framework 4.8
## 安装说明
## 构建与安装
1. 编译项目生成NavisworksTransportPlugin.dll
2. 插件会自动安装到Navisworks插件目录
`[Navisworks安装路径]\Plugins\NavisworksTransportPlugin\`
3. 重启Navisworks即可在"附加模块"选项卡中找到插件
```bash
# 构建
./compile.bat
# 部署到Navisworks插件目录
./deploy-plugin.bat
```
插件将自动安装到:`%PROGRAMDATA%\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin\`
## 使用方法
1. 在Navisworks中选择要设置属性的模型项目
2. 点击"附加模块"选项卡中的"Transport Plugin"
3. 在弹出窗口中点击相应的类别按钮
4. 在属性面板中查看添加的"物流分类"属性
1. 在Navisworks中打开BIM模型
2. 点击"附加模块"选项卡中的"TransportPlugin"
3. 使用物流分类功能设置模型属性
4. 创建路径并执行碰撞检测
详细使用说明请参阅:[使用说明文档](doc/working/使用说明.md)
## 开发文档
- [开发任务文档](doc/working/类别属性功能开发任务.md)
- [设计方案](doc/design/Navisworks%20物流路径规划插件快速开发方案动态碰撞检测与动画集成_.md)
- [需求文档](doc/requirement/user_requiement.md)
详细说明请参阅:[AGENTS.md](AGENTS.md)
## 技术架构
```
NavisworksTransportPlugin/
├── MainPlugin.cs # 插件主类和用户界面
├── LogisticsCategories.cs # 物流类别定义
├── CategoryAttributeManager.cs # COM API封装和属性管理
└── Properties/
└── AssemblyInfo.cs # 程序集信息
src/
├── Core/ # 插件主类、动画、碰撞检测、配置、数据库
├── Commands/ # 命令模式实现
├── PathPlanning/ # A*算法、网格地图、路径优化
├── UI/WPF/ # WPF界面(Views/ViewModels/Converters)
└── Utils/ # 工具类
```
## 版本历史
## 版本
### v1.0 (2025-01-11)
- 实现基础的类别属性分配功能
- 支持5种预定义物流类别
- 提供批量处理能力
- 完整的错误处理和用户反馈
当前版本:**0.15.0** (2026-02-20)
## 原始需求概述
完整版本历史请参阅:[CHANGELOG.md](CHANGELOG.md)
本插件旨在简化 Navisworks Manage 中移动模型沿确定路径进行物理碰撞或干涉检测的流程。通过自动化 Animator 动画创建、Clash Detective 碰撞测试配置与运行,并提供直观的图形化碰撞结果显示,本插件将大大提高工作效率,并为用户提供一个快速验证施工物流和设备移动可行性的工具。
## 许可证
完整的目标功能包括:
- 在 Navisworks Ribbon 界面添加自定义按钮
- 用户选择一个要移动的模型
- 用户通过选择一系列模型元素来定义非直线路径点
- 插件自动在 Animator 中创建基于这些路径点的对象动画
- 插件自动配置并运行一个链接到该动画的动态碰撞测试
- 当检测到碰撞时,插件将通过颜色覆盖直观地高亮显示碰撞的物体,并弹出明确的提示信息
Copyright © 2024-2026

553
TransportPlugin.csproj Normal file
View File

@ -0,0 +1,553 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">x64</Platform>
<ProjectGuid>{1A0124F6-3DEB-4153-8760-F568AD9393EE}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>TransportPlugin</RootNamespace>
<AssemblyName>TransportPlugin</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NAVISWORKS_2026</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x64</PlatformTarget>
<LangVersion>7.3</LangVersion>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>bin\x64\Release\</OutputPath>
<DefineConstants>TRACE;NAVISWORKS_2026</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x64</PlatformTarget>
<LangVersion>7.3</LangVersion>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<ItemGroup>
<!-- Navisworks 2026 API References -->
<Reference Include="Autodesk.Navisworks.Api">
<HintPath>..\..\..\..\Program Files\Autodesk\Navisworks Manage 2026\Autodesk.Navisworks.Api.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Autodesk.Navisworks.Timeliner">
<HintPath>..\..\..\..\Program Files\Autodesk\Navisworks Manage 2026\Autodesk.Navisworks.Timeliner.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Autodesk.Navisworks.Clash">
<HintPath>..\..\..\..\Program Files\Autodesk\Navisworks Manage 2026\Autodesk.Navisworks.Clash.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Autodesk.Navisworks.Controls">
<HintPath>..\..\..\..\Program Files\Autodesk\Navisworks Manage 2026\Autodesk.Navisworks.Controls.dll</HintPath>
<Private>False</Private>
</Reference>
<!-- COM API References for 2026 -->
<Reference Include="Autodesk.Navisworks.ComApi">
<HintPath>..\..\..\..\Program Files\Autodesk\Navisworks Manage 2026\Autodesk.Navisworks.ComApi.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Autodesk.Navisworks.Interop.ComApi">
<HintPath>..\..\..\..\Program Files\Autodesk\Navisworks Manage 2026\Autodesk.Navisworks.Interop.ComApi.dll</HintPath>
<EmbedInteropTypes>False</EmbedInteropTypes>
<Private>False</Private>
</Reference>
<!-- System References -->
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Numerics" />
<Reference Include="System.Web.Extensions" />
<Reference Include="Newtonsoft.Json">
<HintPath>packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<!-- WPF References -->
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="WindowsBase" />
<Reference Include="System.Xaml" />
<Reference Include="WindowsFormsIntegration" />
<!-- RoyT.AStar NuGet Package -->
<Reference Include="Roy-T.AStar, Version=3.0.2.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>packages\RoyT.AStar.3.0.2\lib\netstandard2.0\Roy-T.AStar.dll</HintPath>
<Private>True</Private>
</Reference>
<!-- System.Data.SQLite NuGet Package -->
<Reference Include="System.Data.SQLite">
<HintPath>packages\Stub.System.Data.SQLite.Core.NetFramework.1.0.118.0\lib\net46\System.Data.SQLite.dll</HintPath>
<Private>True</Private>
</Reference>
<!-- Tomlyn TOML Parser -->
<Reference Include="Tomlyn">
<HintPath>packages\Tomlyn.0.19.0\lib\netstandard2.0\Tomlyn.dll</HintPath>
<Private>True</Private>
</Reference>
<!-- geometry4Sharp - 3D Geometry Library for Voxel Pathfinding -->
<Reference Include="geometry4Sharp">
<HintPath>packages\geometry4Sharp.1.0.0\lib\net48\geometry4Sharp.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<!-- Core - Main Plugin Files -->
<Compile Include="src\Core\MainPlugin.cs" />
<Compile Include="src\Core\PathClickToolPlugin.cs" />
<Compile Include="src\Core\PathInputMonitor.cs" />
<Compile Include="src\Core\PathPointRenderPlugin.cs" />
<!-- Core - Business Logic -->
<Compile Include="src\Core\ModelSplitterManager.cs" />
<Compile Include="src\Core\NavigationMapGenerator.cs" />
<Compile Include="src\Core\PathDataManager.cs" />
<Compile Include="src\Core\PathPlanningManager.cs" />
<Compile Include="src\Core\PathDatabase.cs" />
<Compile Include="src\Core\PathAnalysisService.cs" />
<Compile Include="src\Core\PathAnalysisEngine.cs" />
<Compile Include="src\Core\PathAnalysisReportGenerator.cs" />
<Compile Include="src\Core\PathPlanningModels.cs" />
<Compile Include="src\Core\Models\PathAnalysisModels.cs" />
<Compile Include="src\Core\Models\BatchQueueItem.cs" />
<Compile Include="src\Core\Models\CollisionDetectionConfig.cs" />
<Compile Include="src\Core\Models\TimeTagModels.cs" />
<Compile Include="src\Core\PathCurveEngine.cs" />
<Compile Include="src\Core\GridVisualization.cs" />
<Compile Include="src\Core\BatchQueueManager.cs" />
<!-- Core - Events and Interfaces -->
<Compile Include="src\Core\IPathPlanningManagerEvents.cs" />
<Compile Include="src\Core\PathPlanningManagerEventArgs.cs" />
<!-- Core - UI State Management -->
<Compile Include="src\Core\UIStateManager.cs" />
<!-- Core - Idle Event Management -->
<Compile Include="src\Core\IdleEventManager.cs" />
<!-- Core - Document State Management -->
<Compile Include="src\Core\DocumentStateManager.cs" />
<Compile Include="src\Core\AssemblyReferencePathManager.cs" />
<!-- Core - Virtual Object Management -->
<Compile Include="src\Core\VirtualObjectManager.cs" />
<!-- Core - Section Box Export -->
<Compile Include="src\Core\SectionBoxExporter.cs" />
<Compile Include="src\Utils\Assembly\AssemblyEndFaceAnalyzer.cs" />
<Compile Include="src\Utils\Assembly\AssemblyInstallationReferenceBuilder.cs" />
<!-- Core - Configuration Management -->
<Compile Include="src\Core\Config\SystemConfig.cs" />
<Compile Include="src\Core\Config\ConfigManager.cs" />
<Compile Include="src\Core\Config\CustomCategoryConfig.cs" />
<Compile Include="src\Core\Database\BackupManager.cs" />
<Compile Include="src\Core\Services\TimeTagCalculator.cs" />
<Compile Include="src\Core\Services\TimeTagService.cs" />
<Compile Include="src\Core\Services\TimeTagExporter.cs" />
<Compile Include="src\Core\Services\TimeTagTimeLinerIntegration.cs" />
<Compile Include="src\Core\Services\TestAutomationHttpService.cs" />
<!-- Commands - Command Pattern Framework (for testing) -->
<Compile Include="src\Commands\IPathPlanningCommand.cs" />
<Compile Include="src\Commands\CommandBase.cs" />
<Compile Include="src\Commands\PathPlanningResult.cs" />
<Compile Include="src\Commands\CommandManager.cs" />
<Compile Include="src\Commands\CommandExecutor.cs" />
<Compile Include="src\Commands\PathPlanningCommands.cs" />
<!-- Commands - Specific Command Implementations -->
<Compile Include="src\Commands\AutoPathPlanningCommand.cs" />
<Compile Include="src\Commands\DeletePathCommand.cs" />
<Compile Include="src\Commands\ExportPathCommand.cs" />
<Compile Include="src\Commands\ImportPathCommand.cs" />
<Compile Include="src\Commands\SetLogisticsAttributeCommand.cs" />
<Compile Include="src\Commands\StartAnimationCommand.cs" />
<Compile Include="src\Commands\GenerateCollisionReportCommand.cs" />
<Compile Include="src\Commands\TransportRibbonHandler.cs" />
<Compile Include="src\Commands\VoxelGridSDFTestCommand.cs" />
<Compile Include="src\Commands\VoxelPathFindingTestCommand.cs" />
<!-- Core - Animation System -->
<Compile Include="src\Core\Animation\PathAnimationManager.cs" />
<Compile Include="src\Core\Animation\TimeLinerIntegrationManager.cs" />
<!-- Core - Collision Detection -->
<Compile Include="src\Core\Collision\ClashDetectiveIntegration.cs" />
<Compile Include="src\Core\Collision\IBatchCollisionProcessor.cs" />
<Compile Include="src\Core\Collision\BatchCollisionProcessor.cs" />
<!-- Core - Spatial Indexing -->
<Compile Include="src\Core\Spatial\SpatialHashGrid.cs" />
<Compile Include="src\Core\Spatial\SpatialIndexManager.cs" />
<!-- Core - Properties Management -->
<Compile Include="src\Core\Properties\AttributeGrouper.cs" />
<Compile Include="src\Core\Properties\CategoryAttributeManager.cs" />
<Compile Include="src\Core\Properties\NavisworksComPropertyManager.cs" />
<Compile Include="src\Core\FloorAttributeManager.cs" />
<!-- Utilities -->
<Compile Include="src\Utils\ModelHighlightHelper.cs" />
<Compile Include="src\Utils\CollisionReportHtmlGenerator.cs" />
<!-- PathPlanning - Auto Path Planning -->
<Compile Include="src\PathPlanning\GridMap.cs" />
<Compile Include="src\PathPlanning\GridMapGenerator.cs" />
<Compile Include="src\PathPlanning\GridCellBuilder.cs" />
<Compile Include="src\PathPlanning\AutoPathFinder.cs" />
<Compile Include="src\PathPlanning\GridPoint2D.cs" />
<Compile Include="src\PathPlanning\AutoPathPlanningValidationResult.cs" />
<Compile Include="src\PathPlanning\ChannelHeightDetector.cs" />
<Compile Include="src\PathPlanning\SlopeAnalyzer.cs" />
<Compile Include="src\PathPlanning\OptimizedHeightCalculator.cs" />
<Compile Include="src\PathPlanning\ChannelBasedGridBuilder.cs" />
<Compile Include="src\PathPlanning\PathOptimizer.cs" />
<Compile Include="src\PathPlanning\TimeMarkerCalculationService.cs" />
<Compile Include="src\PathPlanning\GridMapCacheKey.cs" />
<Compile Include="src\PathPlanning\GridMapCache.cs" />
<!-- PathPlanning - Voxel 3D Path Planning (Experimental) -->
<Compile Include="src\PathPlanning\VoxelCell.cs" />
<Compile Include="src\PathPlanning\VoxelGrid.cs" />
<Compile Include="src\PathPlanning\VoxelGridGenerator.cs" />
<Compile Include="src\PathPlanning\VoxelPathFinder.cs" />
<Compile Include="src\PathPlanning\MeshSDFTester.cs" />
<Compile Include="src\PathPlanning\VoxelGridVisualizer.cs" />
<Compile Include="src\PathPlanning\RailGeometryHelper.cs" />
<Compile Include="src\PathPlanning\AerialPathGenerator.cs" />
<Compile Include="src\Commands\CreateAerialPathCommand.cs" />
<!-- UI - WPF -->
<Compile Include="src\UI\WPF\Views\LogisticsControlPanel.xaml.cs">
<DependentUpon>LogisticsControlPanel.xaml</DependentUpon>
</Compile>
<Compile Include="src\UI\WPF\Views\ModelSettingsView.xaml.cs">
<DependentUpon>ModelSettingsView.xaml</DependentUpon>
</Compile>
<Compile Include="src\UI\WPF\Views\PathEditingView.xaml.cs">
<DependentUpon>PathEditingView.xaml</DependentUpon>
</Compile>
<Compile Include="src\UI\WPF\Views\AnimationControlView.xaml.cs">
<DependentUpon>AnimationControlView.xaml</DependentUpon>
</Compile>
<Compile Include="src\UI\WPF\Views\CollisionAnalysisDialog.xaml.cs">
<DependentUpon>CollisionAnalysisDialog.xaml</DependentUpon>
</Compile>
<Compile Include="src\UI\WPF\Views\SystemManagementView.xaml.cs">
<DependentUpon>SystemManagementView.xaml</DependentUpon>
</Compile>
<Compile Include="src\UI\WPF\Views\LayerManagementView.xaml.cs">
<DependentUpon>LayerManagementView.xaml</DependentUpon>
</Compile>
<Compile Include="src\UI\WPF\Views\HelpDialog.xaml.cs">
<DependentUpon>HelpDialog.xaml</DependentUpon>
</Compile>
<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>
<Compile Include="src\UI\WPF\Views\ConfigEditorDialog.xaml.cs">
<DependentUpon>ConfigEditorDialog.xaml</DependentUpon>
</Compile>
<Compile Include="src\UI\WPF\Views\CollisionReportDialog.xaml.cs">
<DependentUpon>CollisionReportDialog.xaml</DependentUpon>
</Compile>
<Compile Include="src\UI\WPF\Controls\EventTimelineControl.xaml.cs">
<DependentUpon>EventTimelineControl.xaml</DependentUpon>
</Compile>
<Compile Include="src\UI\WPF\Views\TimeTagDialog.xaml.cs">
<DependentUpon>TimeTagDialog.xaml</DependentUpon>
</Compile>
<Compile Include="src\UI\WPF\Views\PathAnalysisDialog.xaml.cs">
<DependentUpon>PathAnalysisDialog.xaml</DependentUpon>
</Compile>
<Compile Include="src\UI\WPF\Views\GenerateNavigationMapDialog.xaml.cs">
<DependentUpon>GenerateNavigationMapDialog.xaml</DependentUpon>
</Compile>
<Compile Include="src\UI\WPF\Views\AerialHeightDialog.xaml.cs">
<DependentUpon>AerialHeightDialog.xaml</DependentUpon>
</Compile>
<Compile Include="src\UI\WPF\Views\MediaControlBar.xaml.cs">
<DependentUpon>MediaControlBar.xaml</DependentUpon>
</Compile>
<Compile Include="src\UI\WPF\Views\EditCoordinatesWindow.xaml.cs">
<DependentUpon>EditCoordinatesWindow.xaml</DependentUpon>
</Compile>
<Compile Include="src\UI\WPF\Views\ModelItemBoundsWindow.xaml.cs">
<DependentUpon>ModelItemBoundsWindow.xaml</DependentUpon>
</Compile>
<Compile Include="src\UI\WPF\Views\EditRotationWindow.xaml.cs">
<DependentUpon>EditRotationWindow.xaml</DependentUpon>
</Compile>
<Compile Include="src\UI\WPF\Views\CoordinateSystemResultDialog.xaml.cs">
<DependentUpon>CoordinateSystemResultDialog.xaml</DependentUpon>
</Compile>
<!-- UI - WPF ViewModels -->
<Compile Include="src\UI\WPF\ViewModels\ViewModelBase.cs" />
<Compile Include="src\UI\WPF\ViewModels\LogisticsControlViewModel.cs" />
<Compile Include="src\UI\WPF\ViewModels\LayerManagementViewModel.cs" />
<Compile Include="src\UI\WPF\ViewModels\ModelSettingsViewModel.cs" />
<Compile Include="src\UI\WPF\ViewModels\AnimationControlViewModel.cs" />
<Compile Include="src\UI\WPF\ViewModels\PathEditingViewModel.cs" />
<Compile Include="src\UI\WPF\ViewModels\RailAssemblyWorkflowContext.cs" />
<Compile Include="src\UI\WPF\ViewModels\SystemManagementViewModel.cs" />
<Compile Include="src\UI\WPF\ViewModels\CollisionReportViewModel.cs" />
<Compile Include="src\UI\WPF\ViewModels\PathAnalysisViewModel.cs" />
<Compile Include="src\UI\WPF\ViewModels\TimeTagViewModel.cs" />
<Compile Include="src\UI\WPF\ViewModels\BatchTaskManagementViewModel.cs" />
<!-- UI - WPF Helpers -->
<!-- UI - WPF Collections -->
<Compile Include="src\UI\WPF\Collections\ThreadSafeObservableCollection.cs" />
<!-- UI - WPF Interfaces -->
<Compile Include="src\UI\WPF\Interfaces\IPropertyChangeNotifier.cs" />
<!-- UI - WPF Services -->
<Compile Include="src\UI\WPF\Services\BindingExpressionOptimizer.cs" />
<Compile Include="src\UI\WPF\Services\SmartDataBindingOptimizer.cs" />
<!-- UI - WPF Commands and Models -->
<Compile Include="src\UI\WPF\Commands\RelayCommand.cs" />
<Compile Include="src\UI\WPF\Commands\LayerManagementCommands.cs" />
<Compile Include="src\UI\WPF\Converters\HoistingConverters.cs" />
<Compile Include="src\UI\WPF\Converters\BoolToVisibilityConverter.cs" />
<Compile Include="src\UI\WPF\Converters\BoolToBrushConverter.cs" />
<Compile Include="src\UI\WPF\Converters\BoolToOpacityConverter.cs" />
<Compile Include="src\UI\WPF\Converters\IndexConverter.cs" />
<Compile Include="src\UI\WPF\Converters\CountToVisibilityConverter.cs" />
<Compile Include="src\UI\WPF\Converters\EditableNumberConverter.cs" />
<Compile Include="src\UI\WPF\Converters\PathTypeConverter.cs" />
<Compile Include="src\UI\WPF\Converters\BatchQueueStatusConverter.cs" />
<Compile Include="src\UI\WPF\Converters\BatchQueueStatusHelper.cs" />
<Compile Include="src\UI\WPF\Converters\NullToDashConverter.cs" />
<Compile Include="src\UI\WPF\Converters\PathToImageConverter.cs" />
<Compile Include="src\UI\WPF\Models\LogisticsModel.cs" />
<Compile Include="src\UI\WPF\Models\HoistingLevelItem.cs" />
<Compile Include="src\UI\WPF\Models\PathRouteViewModel.cs" />
<Compile Include="src\UI\WPF\Models\SplitPreviewItem.cs" />
<!-- Utils -->
<Compile Include="src\Utils\BoundingBoxGeometryUtils.cs" />
<Compile Include="src\Utils\ComApiBase.cs" />
<Compile Include="src\Utils\CoordinateConverter.cs" />
<Compile Include="src\Utils\FloorDetector.cs" />
<Compile Include="src\Utils\ModelItemAnalysisHelper.cs" />
<Compile Include="src\Utils\GeometryHelper.cs" />
<Compile Include="src\Utils\GeometryCacheManager.cs" />
<Compile Include="src\Utils\DialogHelper.cs" />
<Compile Include="src\Utils\LogManager.cs" />
<Compile Include="src\Utils\NavisworksApiHelper.cs" />
<Compile Include="src\Utils\NavisworksSelectionHelper.cs" />
<Compile Include="src\Utils\NavisworksToDMesh3Converter.cs" />
<Compile Include="src\Utils\UnitsConverter.cs" />
<Compile Include="src\Utils\VersionInfo.cs" />
<!-- Coordinate System -->
<Compile Include="src\Utils\CoordinateSystem\CoordinateSystemType.cs" />
<Compile Include="src\Utils\CoordinateSystem\ICoordinateSystem.cs" />
<Compile Include="src\Utils\CoordinateSystem\CanonicalBounds3.cs" />
<Compile Include="src\Utils\CoordinateSystem\LocalAxisPoseBuilder.cs" />
<Compile Include="src\Utils\CoordinateSystem\LocalEulerRotationCorrection.cs" />
<Compile Include="src\Utils\CoordinateSystem\ObjectStartPlacementRequest.cs" />
<Compile Include="src\Utils\CoordinateSystem\CanonicalPlanarPoseBuilder.cs" />
<Compile Include="src\Utils\CoordinateSystem\CanonicalRailOffsetResolver.cs" />
<Compile Include="src\Utils\CoordinateSystem\CanonicalRailPoseBuilder.cs" />
<Compile Include="src\Utils\CoordinateSystem\CanonicalTrackedPositionResolver.cs" />
<Compile Include="src\Utils\CoordinateSystem\FragmentDefaultUpContext.cs" />
<Compile Include="src\Utils\CoordinateSystem\FragmentRepresentativePoseHelper.cs" />
<Compile Include="src\Utils\CoordinateSystem\RealObjectReferencePose.cs" />
<Compile Include="src\Utils\CoordinateSystem\RealObjectReferencePoseResolver.cs" />
<Compile Include="src\Utils\CoordinateSystem\HoistingRealObjectPoseHelper.cs" />
<Compile Include="src\Utils\CoordinateSystem\PathTargetFrame.cs" />
<Compile Include="src\Utils\CoordinateSystem\PathTargetFrameResolver.cs" />
<Compile Include="src\Utils\CoordinateSystem\RealObjectPlanarPoseSolver.cs" />
<Compile Include="src\Utils\CoordinateSystem\RealObjectProjectedExtentResolver.cs" />
<Compile Include="src\Utils\CoordinateSystem\RealObjectRailAxisConventionResolver.cs" />
<Compile Include="src\Utils\CoordinateSystem\RealObjectRailExtentResolver.cs" />
<Compile Include="src\Utils\CoordinateSystem\ObjectSpaceOrientationHelper.cs" />
<Compile Include="src\Utils\CoordinateSystem\RotatedObjectExtentHelper.cs" />
<Compile Include="src\Utils\CoordinateSystem\HostCoordinateAdapter.cs" />
<Compile Include="src\Utils\CoordinateSystem\HostPlanarGridHelper.cs" />
<Compile Include="src\Utils\CoordinateSystem\HoistingCoordinateHelper.cs" />
<Compile Include="src\Utils\CoordinateSystem\LocalAxisDirection.cs" />
<Compile Include="src\Utils\CoordinateSystem\ModelAxisConvention.cs" />
<Compile Include="src\Utils\CoordinateSystem\ProjectReferenceFrame.cs" />
<Compile Include="src\Utils\CoordinateSystem\RailLocalFrame.cs" />
<Compile Include="src\Utils\CoordinateSystem\ZUpCoordinateSystem.cs" />
<Compile Include="src\Utils\CoordinateSystem\YUpCoordinateSystem.cs" />
<Compile Include="src\Utils\CoordinateSystem\CoordinateSystemManager.cs" />
<Compile Include="src\Utils\ViewpointHelper.cs" />
<Compile Include="src\Utils\VisibilityHelper.cs" />
<Compile Include="src\Utils\NwdExportHelper.cs" />
<Compile Include="src\Utils\ModelItemTransformHelper.cs" />
<Compile Include="src\Utils\SelectionClipBoxLockState.cs" />
<Compile Include="src\Utils\CachedTriangle3D.cs" />
<Compile Include="src\Utils\ClipboardHelper.cs" />
<Compile Include="src\Utils\PathHelper.cs" />
<Compile Include="src\Utils\RailPathPoseHelper.cs" />
<Compile Include="src\Utils\CollisionSceneHelper.cs" />
<Compile Include="src\Utils\SectionClipHelper.cs" />
<!-- Assembly Info -->
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<!-- WPF XAML Files -->
<Page Include="src\UI\WPF\Views\LogisticsControlPanel.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Views\EditCoordinatesWindow.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Views\ModelItemBoundsWindow.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Views\EditRotationWindow.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Views\CoordinateSystemResultDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Views\ModelSettingsView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Views\PathEditingView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Views\AnimationControlView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Views\CollisionAnalysisDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Views\SystemManagementView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Views\LayerManagementView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Views\HelpDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Views\AboutDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Views\LogViewerDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Views\ConfigEditorDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Views\CollisionReportDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Controls\EventTimelineControl.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Views\TimeTagDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Views\PathAnalysisDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Views\GenerateNavigationMapDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Views\AerialHeightDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Views\MediaControlBar.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Views\BatchTaskManagementView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Compile Include="src\UI\WPF\Views\BatchTaskManagementView.xaml.cs">
<DependentUpon>BatchTaskManagementView.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Page Include="src\UI\WPF\Views\PathSelectionDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Compile Include="src\UI\WPF\Views\PathSelectionDialog.xaml.cs">
<DependentUpon>PathSelectionDialog.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Page Include="src\UI\WPF\Views\PathConfigDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Compile Include="src\UI\WPF\Views\PathConfigDialog.xaml.cs">
<DependentUpon>PathConfigDialog.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<!-- Shared Resource Dictionary -->
<Page Include="src\UI\WPF\Resources\NavisworksStyles.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Resources\MediaControlIcons.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Microsoft\WinFX\3.0\Microsoft.WinFX.targets" Condition="Exists('$(MSBuildExtensionsPath)\Microsoft\WinFX\3.0\Microsoft.WinFX.targets')" />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- Import NETStandard.Library targets to support .NET Standard 2.0 libraries -->
<Import Project="packages\NETStandard.Library.2.0.3\build\netstandard2.0\NETStandard.Library.targets" Condition="Exists('packages\NETStandard.Library.2.0.3\build\netstandard2.0\NETStandard.Library.targets')" />
<ItemGroup>
<!-- Configuration Template File (in resources folder) -->
<None Include="resources\default_config.toml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>resources\default_config.toml</Link>
</None>
<!-- SQLite native x64 runtime dependency -->
<None Include="packages\Stub.System.Data.SQLite.Core.NetFramework.1.0.118.0\build\net46\x64\SQLite.Interop.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>SQLite.Interop.dll</Link>
</None>
<!-- Plugin Name File (in resources folder) -->
<None Include="resources\TransportPlugin.name.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>resources\TransportPlugin.name.txt</Link>
</None>
<None Include="resources\TransportRibbon.xaml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>TransportRibbon.xaml</Link>
</None>
<None Include="resources\TransportRibbon_16.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>TransportRibbon_16.png</Link>
</None>
<None Include="resources\TransportRibbon_32.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>TransportRibbon_32.png</Link>
</None>
<!-- Unit Cube NWC Model File -->
<None Include="resources\unit_cube.nwc">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>resources\unit_cube.nwc</Link>
</None>
<!-- Unit Cylinder NWC Model File -->
<None Include="resources\unit_cylinder.nwc">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>resources\unit_cylinder.nwc</Link>
</None>
</ItemGroup>
</Project>

49
TransportPlugin.sln Normal file
View File

@ -0,0 +1,49 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36203.30
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TransportPlugin", "TransportPlugin.csproj", "{1A0124F6-3DEB-4153-8760-F568AD9393EE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NavisworksTransport.UnitTests", "NavisworksTransport.UnitTests.csproj", "{7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}"
EndProject
Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "TransportPlugin.Setup", "..\TransportPlugin.Setup\TransportPlugin.Setup.vdproj", "{E1955F72-A686-9398-1C6A-936493D9211F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1A0124F6-3DEB-4153-8760-F568AD9393EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1A0124F6-3DEB-4153-8760-F568AD9393EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1A0124F6-3DEB-4153-8760-F568AD9393EE}.Debug|x64.ActiveCfg = Debug|x64
{1A0124F6-3DEB-4153-8760-F568AD9393EE}.Debug|x64.Build.0 = Debug|x64
{1A0124F6-3DEB-4153-8760-F568AD9393EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1A0124F6-3DEB-4153-8760-F568AD9393EE}.Release|Any CPU.Build.0 = Release|Any CPU
{1A0124F6-3DEB-4153-8760-F568AD9393EE}.Release|x64.ActiveCfg = Release|x64
{1A0124F6-3DEB-4153-8760-F568AD9393EE}.Release|x64.Build.0 = Release|x64
{7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}.Debug|Any CPU.ActiveCfg = Debug|x64
{7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}.Debug|Any CPU.Build.0 = Debug|x64
{7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}.Debug|x64.ActiveCfg = Debug|x64
{7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}.Debug|x64.Build.0 = Debug|x64
{7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}.Release|Any CPU.ActiveCfg = Release|x64
{7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}.Release|Any CPU.Build.0 = Release|x64
{7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}.Release|x64.ActiveCfg = Release|x64
{7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}.Release|x64.Build.0 = Release|x64
{E1955F72-A686-9398-1C6A-936493D9211F}.Debug|Any CPU.ActiveCfg = Debug
{E1955F72-A686-9398-1C6A-936493D9211F}.Debug|x64.ActiveCfg = Debug
{E1955F72-A686-9398-1C6A-936493D9211F}.Debug|x64.Build.0 = Debug
{E1955F72-A686-9398-1C6A-936493D9211F}.Release|Any CPU.ActiveCfg = Release
{E1955F72-A686-9398-1C6A-936493D9211F}.Release|x64.ActiveCfg = Release
{E1955F72-A686-9398-1C6A-936493D9211F}.Release|x64.Build.0 = Release
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {EB39ED46-3E01-481E-AE51-425495B56837}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,135 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.GeometryAnalysis;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class AssemblyEndFaceAnalyzerTests
{
[TestMethod]
public void Analyze_RectangularFaceWithSideNoise_ShouldReturnFaceCenter()
{
var triangles = new List<AnalysisTriangle3>();
triangles.AddRange(CreateRectangleFace(-2.0, 2.0, -1.0, 1.0, 10.0));
triangles.AddRange(CreateRectangleFace(-2.0, 2.0, -1.0, 1.0, 8.0));
triangles.Add(new AnalysisTriangle3(
new Vector3(-2.0f, -1.0f, 10.0f),
new Vector3(-2.0f, -1.0f, 9.0f),
new Vector3(-2.0f, 1.0f, 9.0f)));
EndFaceAnalysisResult result = AssemblyEndFaceAnalyzer.Analyze(
triangles,
new Vector3(-1.5f, -0.7f, 10.0f),
new Vector3(1.4f, -0.5f, 10.0f),
new Vector3(1.1f, 0.8f, 10.0f));
Assert.IsTrue(result.IsReliable, result.DiagnosticMessage);
AssertPoint(result.Center, 0.0, 0.0, 10.0);
Assert.AreEqual(2, result.CandidateTriangleCount);
}
[TestMethod]
public void Analyze_SquareRingFace_ShouldReturnRingCenter()
{
var triangles = new List<AnalysisTriangle3>();
triangles.AddRange(CreateRingFace(4.0, 2.0, 5.0));
EndFaceAnalysisResult result = AssemblyEndFaceAnalyzer.Analyze(
triangles,
new Vector3(-3.5f, -3.5f, 5.0f),
new Vector3(3.5f, -3.5f, 5.0f),
new Vector3(3.5f, 3.5f, 5.0f));
Assert.IsTrue(result.IsReliable, result.DiagnosticMessage);
AssertPoint(result.Center, 0.0, 0.0, 5.0);
Assert.IsTrue(result.CandidateTriangleCount >= 8);
}
[TestMethod]
public void Analyze_NearlyCollinearSeedPoints_ShouldFail()
{
var triangles = new List<AnalysisTriangle3>();
triangles.AddRange(CreateRectangleFace(-1.0, 1.0, -1.0, 1.0, 0.0));
EndFaceAnalysisResult result = AssemblyEndFaceAnalyzer.Analyze(
triangles,
new Vector3(0.0f, 0.0f, 0.0f),
new Vector3(1.0f, 0.0f, 0.0f),
new Vector3(2.0f, 0.0f, 0.0f));
Assert.IsFalse(result.IsReliable);
}
[TestMethod]
public void OrientNormalTowardTarget_ShouldFlip_WhenNormalOpposesTargetDirection()
{
Vector3 orientedNormal = AssemblyEndFaceAnalyzer.OrientNormalTowardTarget(
new Vector3(-1f, 0f, 0f),
Vector3.Zero,
new Vector3(10f, 0f, 0f));
AssertPoint(orientedNormal, 1.0, 0.0, 0.0);
}
[TestMethod]
public void OrientNormalTowardTarget_ShouldKeepDirection_WhenNormalAlreadyFacesTarget()
{
Vector3 orientedNormal = AssemblyEndFaceAnalyzer.OrientNormalTowardTarget(
new Vector3(0f, 1f, 0f),
new Vector3(1f, 2f, 3f),
new Vector3(1f, 5f, 3f));
AssertPoint(orientedNormal, 0.0, 1.0, 0.0);
}
private static IEnumerable<AnalysisTriangle3> CreateRectangleFace(double minX, double maxX, double minY, double maxY, double z)
{
yield return new AnalysisTriangle3(
new Vector3((float)minX, (float)minY, (float)z),
new Vector3((float)maxX, (float)minY, (float)z),
new Vector3((float)maxX, (float)maxY, (float)z));
yield return new AnalysisTriangle3(
new Vector3((float)minX, (float)minY, (float)z),
new Vector3((float)maxX, (float)maxY, (float)z),
new Vector3((float)minX, (float)maxY, (float)z));
}
private static IEnumerable<AnalysisTriangle3> CreateRingFace(double outerHalfSize, double innerHalfSize, double z)
{
if (innerHalfSize >= outerHalfSize)
{
throw new ArgumentOutOfRangeException(nameof(innerHalfSize));
}
foreach (AnalysisTriangle3 triangle in CreateRectangleFace(-outerHalfSize, outerHalfSize, innerHalfSize, outerHalfSize, z))
{
yield return triangle;
}
foreach (AnalysisTriangle3 triangle in CreateRectangleFace(-outerHalfSize, outerHalfSize, -outerHalfSize, -innerHalfSize, z))
{
yield return triangle;
}
foreach (AnalysisTriangle3 triangle in CreateRectangleFace(-outerHalfSize, -innerHalfSize, -innerHalfSize, innerHalfSize, z))
{
yield return triangle;
}
foreach (AnalysisTriangle3 triangle in CreateRectangleFace(innerHalfSize, outerHalfSize, -innerHalfSize, innerHalfSize, z))
{
yield return triangle;
}
}
private static void AssertPoint(Vector3 actual, double x, double y, double z)
{
Assert.AreEqual(x, actual.X, 1e-5);
Assert.AreEqual(y, actual.Y, 1e-5);
Assert.AreEqual(z, actual.Z, 1e-5);
}
}
}

View File

@ -0,0 +1,98 @@
using System;
using System.Numerics;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.GeometryAnalysis;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class AssemblyInstallationReferenceBuilderTests
{
[TestMethod]
public void Build_ShouldCreatePlaneParallelToOpticalAxisAndProjectedAnchor()
{
Vector3 axisBase = new Vector3(0f, 0f, 0f);
Vector3 axisDirection = Vector3.UnitX;
Vector3 pickPoint = new Vector3(2f, 3f, 4f);
AssemblyInstallationReferenceResult result =
AssemblyInstallationReferenceBuilder.Build(axisBase, axisDirection, pickPoint);
Assert.AreEqual(5f, result.OffsetDistance, 1e-4f);
Assert.AreEqual(2f, result.AnchorPoint.X, 1e-4f);
Assert.AreEqual(3f, result.AnchorPoint.Y, 1e-4f);
Assert.AreEqual(4f, result.AnchorPoint.Z, 1e-4f);
Assert.AreEqual(0f, Vector3.Dot(result.PlaneNormal, result.OpticalAxisDirection), 1e-4f);
Assert.AreEqual(0f, Vector3.Dot(result.PlaneSpanDirection, result.OpticalAxisDirection), 1e-4f);
Assert.AreEqual(0f, Vector3.Dot(result.PlaneNormal, result.PlaneSpanDirection), 1e-4f);
}
[TestMethod]
public void BuildFromTwoPoints_ShouldCreatePlaneParallelToOpticalAxisAndContainBothPoints()
{
Vector3 axisBase = new Vector3(0f, 0f, 0f);
Vector3 axisDirection = Vector3.UnitX;
Vector3 pickPoint1 = new Vector3(2f, 3f, 4f);
Vector3 pickPoint2 = new Vector3(6f, 3f, 6f);
AssemblyInstallationReferenceResult result =
AssemblyInstallationReferenceBuilder.Build(axisBase, axisDirection, pickPoint1, pickPoint2);
Assert.AreEqual(3f, result.OffsetDistance, 1e-4f);
Assert.AreEqual(4f, result.AnchorPoint.X, 1e-4f);
Assert.AreEqual(3f, result.AnchorPoint.Y, 1e-4f);
Assert.AreEqual(0f, result.AnchorPoint.Z, 1e-4f);
Assert.AreEqual(0f, Vector3.Dot(result.PlaneNormal, result.OpticalAxisDirection), 1e-4f);
Assert.AreEqual(0f, Vector3.Dot(result.PlaneSpanDirection, result.OpticalAxisDirection), 1e-4f);
Assert.AreEqual(0f, Vector3.Dot(result.PlaneNormal, result.PlaneSpanDirection), 1e-4f);
Assert.AreEqual(0f, Vector3.Dot(pickPoint1 - result.AnchorPoint, result.PlaneNormal), 1e-4f);
Assert.AreEqual(0f, Vector3.Dot(pickPoint2 - result.AnchorPoint, result.PlaneNormal), 1e-4f);
}
[TestMethod]
public void BuildFromTwoPoints_ShouldUseOpticalAxisProjectionAsInstallationCenterLine()
{
Vector3 axisBase = new Vector3(0f, 0f, 0f);
Vector3 axisDirection = Vector3.UnitX;
Vector3 pickPoint1 = new Vector3(2f, 3f, 4f);
Vector3 pickPoint2 = new Vector3(6f, 3f, 6f);
AssemblyInstallationReferenceResult result =
AssemblyInstallationReferenceBuilder.Build(axisBase, axisDirection, pickPoint1, pickPoint2);
Assert.AreEqual(0f, result.InstallLineBasePoint.X, 1e-4f);
Assert.AreEqual(3f, result.InstallLineBasePoint.Y, 1e-4f);
Assert.AreEqual(0f, result.InstallLineBasePoint.Z, 1e-4f);
Assert.AreEqual(4f, result.AnchorPoint.X, 1e-4f);
Assert.AreEqual(3f, result.AnchorPoint.Y, 1e-4f);
Assert.AreEqual(0f, result.AnchorPoint.Z, 1e-4f);
}
[TestMethod]
public void BuildFromTwoPoints_WhenPlanePointsAreNearlyAxisAligned_ShouldThrow()
{
Vector3 axisBase = new Vector3(0f, 0f, 0f);
Vector3 axisDirection = Vector3.UnitX;
Vector3 pickPoint1 = new Vector3(2f, 3f, 4f);
Vector3 pickPoint2 = new Vector3(6f, 3.00001f, 4.00001f);
InvalidOperationException ex = Assert.ThrowsException<InvalidOperationException>(
() => AssemblyInstallationReferenceBuilder.Build(axisBase, axisDirection, pickPoint1, pickPoint2));
StringAssert.Contains(ex.Message, "无法确定安装参考面");
}
[TestMethod]
public void Build_WhenPickPointTooCloseToOpticalAxis_ShouldThrow()
{
Vector3 axisBase = new Vector3(0f, 0f, 0f);
Vector3 axisDirection = Vector3.UnitX;
Vector3 pickPoint = new Vector3(2f, 0f, 0f);
InvalidOperationException ex = Assert.ThrowsException<InvalidOperationException>(
() => AssemblyInstallationReferenceBuilder.Build(axisBase, axisDirection, pickPoint));
StringAssert.Contains(ex.Message, "距离光轴过小");
}
}
}

View File

@ -0,0 +1,405 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Core;
using NavisworksTransport.PathPlanning;
using NavisworksTransport.Utils.CoordinateSystem;
using System.Numerics;
using System.Reflection;
using System.Collections.Generic;
using Autodesk.Navisworks.Api;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class AutoPathPlanningCoordinateSemanticsTests
{
[TestMethod]
public void YUp_HostPlanarGridHelper_CreateHostPoint_ShouldApplyElevationOnHostY()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
Vector3 point = HostPlanarGridHelper.CreateHostPoint3(2.0, 3.0, 14.83, adapter);
AssertPoint(point, 2.0, 14.83, 3.0);
}
[TestMethod]
public void ZUp_HostPlanarGridHelper_CreateHostPoint_ShouldApplyElevationOnHostZ()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.ZUp);
Vector3 point = HostPlanarGridHelper.CreateHostPoint3(2.0, 3.0, 6.25, adapter);
AssertPoint(point, 2.0, 3.0, 6.25);
}
[TestMethod]
public void YUp_HostPlanarGridHelper_GetAndSetElevation_ShouldUseHostY()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var hostPoint = new Vector3(4.0f, 15.0f, 6.0f);
double elevation = HostPlanarGridHelper.GetElevation3(hostPoint, adapter);
Vector3 adjusted = HostPlanarGridHelper.SetElevation3(hostPoint, 20.0, adapter);
Assert.AreEqual(15.0, elevation, 1e-6);
AssertPoint(adjusted, 4.0, 20.0, 6.0);
}
[TestMethod]
public void YUp_HostPlanarGridHelper_GetHorizontalCoords_ShouldProjectToHostXZPlane()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var hostPoint = new Vector3(8.0f, 14.83f, -5.35f);
(double h1, double h2) = HostPlanarGridHelper.GetHorizontalCoords3(hostPoint, adapter);
Assert.AreEqual(8.0, h1, 1e-6);
Assert.AreEqual(-5.35, h2, 1e-6);
}
[TestMethod]
public void YUp_HostPlanarGridHelper_HorizontalRange_ShouldMatchHostXZPlane()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var hostMin = new Vector3(-10.0f, 2.0f, -30.0f);
var hostMax = new Vector3(40.0f, 12.0f, 5.0f);
(double min1, double max1, double min2, double max2) =
HostPlanarGridHelper.GetHorizontalRange3(hostMin, hostMax, adapter);
Assert.AreEqual(-10.0, min1, 1e-6);
Assert.AreEqual(40.0, max1, 1e-6);
Assert.AreEqual(-30.0, min2, 1e-6);
Assert.AreEqual(5.0, max2, 1e-6);
}
[TestMethod]
public void YUp_GridLikeWorldPointConstruction_ShouldWriteElevationToHostY()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
double originH1 = -210.0;
double originH2 = -10.0;
double cellSize = 1.0;
Vector3 worldPoint = HostPlanarGridHelper.CreateHostPoint3(
originH1 + 4 * cellSize,
originH2 + 6 * cellSize,
14.83,
adapter);
Assert.AreEqual(-206.0, worldPoint.X, 1e-6);
Assert.AreEqual(14.83, worldPoint.Y, 1e-6);
Assert.AreEqual(-4.0, worldPoint.Z, 1e-6);
}
[TestMethod]
public void YUp_GridLikeWorldToGrid_ShouldReadHostXZAsPlanarAxes()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var origin = new Vector3(-210.0f, 14.83f, -10.0f);
var endPoint = new Vector3(-169.20f, 14.83f, -5.35f);
const double cellSize = 1.0;
(double pointH1, double pointH2) = HostPlanarGridHelper.GetHorizontalCoords3(endPoint, adapter);
(double originH1, double originH2) = HostPlanarGridHelper.GetHorizontalCoords3(origin, adapter);
int gridX = (int)System.Math.Round((pointH1 - originH1) / cellSize);
int gridY = (int)System.Math.Round((pointH2 - originH2) / cellSize);
Assert.AreEqual(41, gridX);
Assert.AreEqual(5, gridY);
}
[TestMethod]
public void ZUp_GridMap_CellPlanarBounds_ShouldMatchNearestNodeRoundSemantics()
{
var bounds = GridMap.CalculateNearestNodePlanarBounds(2.0, 3.0, 1.0);
Assert.AreEqual(1.5, bounds.minH1, 1e-9);
Assert.AreEqual(2.5, bounds.maxH1, 1e-9);
Assert.AreEqual(2.5, bounds.minH2, 1e-9);
Assert.AreEqual(3.5, bounds.maxH2, 1e-9);
}
[TestMethod]
public void ZUp_GridMap_SegmentIntersection_ShouldDetectInteriorNotBoundaryTouch()
{
var bounds = GridMap.CalculateNearestNodePlanarBounds(2.0, 3.0, 1.0);
bool crossesInterior = GridMap.TryGetSegmentRectangleInteriorIntersection(
1.75,
3.0,
2.25,
3.0,
bounds.minH1,
bounds.maxH1,
bounds.minH2,
bounds.maxH2,
1.0,
out double enterT,
out double exitT);
Assert.IsTrue(crossesInterior);
Assert.IsTrue(enterT >= 0.0 && enterT <= exitT && exitT <= 1.0);
bool onlyTouchesBoundary = GridMap.TryGetSegmentRectangleInteriorIntersection(
1.5,
2.0,
1.5,
4.0,
bounds.minH1,
bounds.maxH1,
bounds.minH2,
bounds.maxH2,
1.0,
out _,
out _);
Assert.IsFalse(onlyTouchesBoundary, "贴着归属边界行走不应被当成穿过格子内部。");
}
[TestMethod]
public void YUp_GridMapGenerator_GetObstacleElevationRange_ShouldUseHostYInsteadOfWorldZ()
{
var generator = new GridMapGenerator();
var method = typeof(GridMapGenerator).GetMethod(
"GetObstacleElevationRange",
BindingFlags.Instance | BindingFlags.NonPublic,
null,
new[]
{
typeof(double), typeof(double), typeof(double),
typeof(double), typeof(double), typeof(double),
typeof(CoordinateSystemType)
},
null);
Assert.IsNotNull(method, "未找到 GetObstacleElevationRange 私有方法。");
var elevationRange = ((double min, double max))method.Invoke(generator, new object[]
{
-200.0, 10.0, -30.0,
-198.0, 30.0, -10.0,
CoordinateSystemType.YUp
});
Assert.AreEqual(10.0, elevationRange.min, 1e-6, "YUp 下障碍物最小高程应来自宿主 Y。");
Assert.AreEqual(30.0, elevationRange.max, 1e-6, "YUp 下障碍物最大高程应来自宿主 Y。");
}
[TestMethod]
public void YUp_GridMapGenerator_CalculateBoundingBoxGridCoverage_ShouldProjectToHostXZPlane()
{
var generator = new GridMapGenerator();
var method = typeof(GridMapGenerator).GetMethod(
"CalculateGridCoverageFromWorldExtents",
BindingFlags.Instance | BindingFlags.NonPublic,
null,
new[]
{
typeof(double), typeof(double), typeof(double),
typeof(double), typeof(double), typeof(double),
typeof(double), typeof(double), typeof(double),
typeof(int), typeof(int), typeof(double),
typeof(CoordinateSystemType)
},
null);
Assert.IsNotNull(method, "未找到 CalculateGridCoverageFromWorldExtents 私有方法。");
var coveredCells = (List<(int x, int y)>)method.Invoke(generator, new object[]
{
-200.0, 10.0, -30.0,
-198.0, 30.0, -10.0,
-210.0, 14.83, -80.0,
200, 200, 1.0,
CoordinateSystemType.YUp
});
Assert.AreEqual(63, coveredCells.Count, "YUp 下障碍物包围盒应按宿主 XZ 平面覆盖 3x21 个网格。");
CollectionAssert.Contains(coveredCells, (10, 50));
CollectionAssert.Contains(coveredCells, (12, 70));
}
[TestMethod]
public void YUp_GridMapGenerator_CalculateDoorOpeningCoverage_ShouldProjectToHostXZPlane()
{
var generator = new GridMapGenerator();
var method = typeof(GridMapGenerator).GetMethod(
"CalculateDoorOpeningCoverageFromWorldExtents",
BindingFlags.Instance | BindingFlags.NonPublic,
null,
new[]
{
typeof(double), typeof(double), typeof(double),
typeof(double), typeof(double), typeof(double),
typeof(double),
typeof(double), typeof(double), typeof(double),
typeof(int), typeof(int), typeof(double),
typeof(CoordinateSystemType)
},
null);
Assert.IsNotNull(method, "未找到 CalculateDoorOpeningCoverageFromWorldExtents 私有方法。");
var coveredCells = (List<(int x, int y)>)method.Invoke(generator, new object[]
{
10.0, 14.0, 20.0,
12.0, 16.0, 30.0,
4.0,
0.0, 0.0, 0.0,
100, 100, 1.0,
CoordinateSystemType.YUp
});
Assert.AreEqual(15, coveredCells.Count, "YUp 下门开口应按宿主 XZ 平面覆盖 3x5 个网格。");
CollectionAssert.Contains(coveredCells, (10, 23));
CollectionAssert.Contains(coveredCells, (12, 27));
}
[TestMethod]
public void YUp_PathPlanningManager_CalculateOptimalGridSize_ShouldUseHostXZHorizontalRange()
{
var method = typeof(PathPlanningManager).GetMethod(
"CalculateOptimalGridSizeFromBounds",
BindingFlags.Static | BindingFlags.NonPublic,
null,
new[]
{
typeof(double), typeof(double), typeof(double),
typeof(double), typeof(double), typeof(double),
typeof(CoordinateSystemType)
},
null);
Assert.IsNotNull(method, "未找到 CalculateOptimalGridSizeFromBounds 私有方法。");
var gridSize = (double)method.Invoke(null, new object[]
{
-10.0, 100.0, -300.0,
90.0, 120.0, 400.0,
CoordinateSystemType.YUp
});
Assert.AreEqual(2.0, gridSize, 1e-6, "YUp 下网格大小应基于宿主 XZ 平面最大跨度 700 计算。");
}
[TestMethod]
public void YUp_ChannelHeightDetector_GetBoundsElevationRange_ShouldUseHostY()
{
var method = typeof(ChannelHeightDetector).GetMethod(
"GetBoundsElevationRange",
BindingFlags.Static | BindingFlags.NonPublic,
null,
new[]
{
typeof(double), typeof(double), typeof(double),
typeof(double), typeof(double), typeof(double),
typeof(CoordinateSystemType)
},
null);
Assert.IsNotNull(method, "未找到 GetBoundsElevationRange 私有方法。");
var elevationRange = ((double min, double max))method.Invoke(null, new object[]
{
-10.0, 14.83, -30.0,
20.0, 18.50, 40.0,
CoordinateSystemType.YUp
});
Assert.AreEqual(14.83, elevationRange.min, 1e-6);
Assert.AreEqual(18.50, elevationRange.max, 1e-6);
}
[TestMethod]
public void YUp_ChannelHeightDetector_CreateHeightProfileSample_ShouldUseHostXZPlaneAndHostYElevation()
{
var method = typeof(ChannelHeightDetector).GetMethod(
"CreateHeightProfileSamplePoint3",
BindingFlags.Static | BindingFlags.NonPublic,
null,
new[]
{
typeof(double), typeof(double), typeof(double),
typeof(double), typeof(double), typeof(double),
typeof(CoordinateSystemType),
typeof(double)
},
null);
Assert.IsNotNull(method, "未找到 CreateHeightProfileSamplePoint3 私有方法。");
var samplePoint = (Vector3)method.Invoke(null, new object[]
{
-10.0, 14.83, -30.0,
20.0, 18.50, 40.0,
CoordinateSystemType.YUp,
0.5
});
Assert.AreEqual(5.0, samplePoint.X, 1e-6, "YUp 下采样点第一水平轴应沿宿主 X 插值。");
Assert.AreEqual(14.83, samplePoint.Y, 1e-6, "YUp 下采样点高程应落在宿主底面 Y。");
Assert.AreEqual(5.0, samplePoint.Z, 1e-6, "YUp 下采样点第二水平轴应沿宿主 Z 插值。");
}
[TestMethod]
public void YUp_ChannelHeightDetector_GetPickedSurfaceElevation_ShouldUseHostYInsteadOfWorldZ()
{
var method = typeof(ChannelHeightDetector).GetMethod(
"GetPickedSurfaceElevation",
BindingFlags.Static | BindingFlags.NonPublic,
null,
new[]
{
typeof(double), typeof(double), typeof(double),
typeof(CoordinateSystemType)
},
null);
Assert.IsNotNull(method, "未找到 GetPickedSurfaceElevation 私有方法。");
var elevation = (double)method.Invoke(null, new object[]
{
11.0, 22.0, 33.0,
CoordinateSystemType.YUp
});
Assert.AreEqual(22.0, elevation, 1e-6, "YUp 下拾取表面点的高程应来自宿主 Y。");
}
[TestMethod]
public void YUp_GridMapGenerator_ResolveBoundsMinElevation_ShouldUseHostYInsteadOfWorldZ()
{
var method = typeof(GridMapGenerator).GetMethod(
"ResolveBoundsMinElevation",
BindingFlags.Static | BindingFlags.NonPublic,
null,
new[]
{
typeof(double), typeof(double), typeof(double),
typeof(CoordinateSystemType)
},
null);
Assert.IsNotNull(method, "未找到 ResolveBoundsMinElevation 私有方法。");
var elevation = (double)method.Invoke(null, new object[]
{
0.0, 12.5, -100.0,
CoordinateSystemType.YUp
});
Assert.AreEqual(12.5, elevation, 1e-6, "YUp 下边界最小高程应来自宿主 Y而不是 bounds.Min.Z。");
}
private static void AssertPoint(Vector3 actual, double x, double y, double z)
{
Assert.AreEqual(x, actual.X, 1e-6);
Assert.AreEqual(y, actual.Y, 1e-6);
Assert.AreEqual(z, actual.Z, 1e-6);
}
}
}

View File

@ -0,0 +1,188 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
using System;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class CanonicalPlanarPoseBuilderTests
{
[TestMethod]
public void StraightForward_ZUpConvention_ShouldKeepLocalZAsWorldUp()
{
bool ok = CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
new Vector3(5, 0, 0),
Vector3.UnitZ,
ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp),
out Quaternion rotation);
Assert.IsTrue(ok);
Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(rotation);
AssertColumn(linear, 0, 1, 0, 0);
AssertColumn(linear, 1, 0, 1, 0);
AssertColumn(linear, 2, 0, 0, 1);
}
[TestMethod]
public void StraightForward_YUpConvention_ShouldMapLocalYToWorldUp()
{
bool ok = CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
new Vector3(5, 0, 0),
Vector3.UnitZ,
ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.YUp),
out Quaternion rotation);
Assert.IsTrue(ok);
Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(rotation);
AssertColumn(linear, 0, 1, 0, 0);
AssertColumn(linear, 1, 0, 0, 1);
AssertColumn(linear, 2, 0, -1, 0);
}
[TestMethod]
public void ForwardWithVerticalComponent_ShouldProjectToHorizontalPlane()
{
bool ok = CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
new Vector3(10, 0, 3),
Vector3.UnitZ,
ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp),
out Quaternion rotation);
Assert.IsTrue(ok);
Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(rotation);
Assert.AreEqual(0.0, linear.M31, 1e-6);
Assert.AreEqual(0.0, linear.M32, 1e-6);
Assert.AreEqual(1.0, linear.M33, 1e-6);
}
[TestMethod]
public void ZeroLocalEulerCorrection_ShouldMatchCurrentPlanarBaseline()
{
var convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp);
Assert.IsTrue(CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
new Vector3(5, 0, 0),
Vector3.UnitZ,
convention,
out Quaternion baselineRotation));
Assert.IsTrue(CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
new Vector3(5, 0, 0),
Vector3.UnitZ,
convention,
LocalEulerRotationCorrection.Zero,
out Quaternion correctedRotation));
AssertQuaternionEquivalent(baselineRotation, correctedRotation);
}
[TestMethod]
public void LocalEulerCorrection_ShouldAlignCorrectedLocalForwardWithPlanarForward()
{
var convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp);
var correction = new LocalEulerRotationCorrection(0.0, 0.0, 90.0);
var adapter = new HostCoordinateAdapter(CoordinateSystemType.ZUp);
Quaternion correctionQuaternion = adapter.CreateCanonicalRotationCorrection(correction);
Assert.IsTrue(CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
new Vector3(5, 0, 0),
Vector3.UnitZ,
convention,
correctionQuaternion,
out Quaternion rotation));
LocalAxisPoseBuilder.ApplyLocalPreRotation(convention, correctionQuaternion, out var correctedLocalForward, out var correctedLocalUp);
Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(rotation);
Vector3 mappedForward = TransformLocalVector(linear, correctedLocalForward);
Vector3 mappedUp = TransformLocalVector(linear, correctedLocalUp);
AssertVector(mappedForward, 1, 0, 0);
AssertVector(mappedUp, 0, 0, 1);
}
[TestMethod]
public void WorldCorrection_ShouldRotateBaselinePoseAroundHostYAxisInYUp()
{
var convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.YUp);
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
Quaternion worldCorrection = adapter.CreateCanonicalRotationCorrection(
new LocalEulerRotationCorrection(0.0, 90.0, 0.0));
Assert.IsTrue(CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
new Vector3(5, 0, 0),
Vector3.UnitZ,
convention,
out Quaternion baselineRotation));
Assert.IsTrue(CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForwardWithWorldCorrection(
new Vector3(5, 0, 0),
Vector3.UnitZ,
convention,
worldCorrection,
out Quaternion correctedRotation));
Matrix4x4 baseline = Matrix4x4.CreateFromQuaternion(baselineRotation);
AssertColumn(baseline, 0, 1, 0, 0);
AssertColumn(baseline, 1, 0, 0, 1);
AssertColumn(baseline, 2, 0, -1, 0);
Quaternion expectedRotation = Quaternion.Normalize(worldCorrection * baselineRotation);
AssertQuaternionEquivalent(expectedRotation, correctedRotation);
}
private static void AssertColumn(Matrix4x4 matrix, int column, double x, double y, double z)
{
switch (column)
{
case 0:
Assert.AreEqual(x, matrix.M11, 1e-6);
Assert.AreEqual(y, matrix.M21, 1e-6);
Assert.AreEqual(z, matrix.M31, 1e-6);
break;
case 1:
Assert.AreEqual(x, matrix.M12, 1e-6);
Assert.AreEqual(y, matrix.M22, 1e-6);
Assert.AreEqual(z, matrix.M32, 1e-6);
break;
case 2:
Assert.AreEqual(x, matrix.M13, 1e-6);
Assert.AreEqual(y, matrix.M23, 1e-6);
Assert.AreEqual(z, matrix.M33, 1e-6);
break;
default:
Assert.Fail("Only first 3 columns are valid.");
break;
}
}
private static void AssertVector(Vector3 actual, double x, double y, double z)
{
Assert.AreEqual(x, actual.X, 1e-6);
Assert.AreEqual(y, actual.Y, 1e-6);
Assert.AreEqual(z, actual.Z, 1e-6);
}
private static void AssertQuaternionEquivalent(Quaternion expected, Quaternion actual)
{
double dot = Math.Abs(
expected.X * actual.X +
expected.Y * actual.Y +
expected.Z * actual.Z +
expected.W * actual.W);
Assert.AreEqual(1.0, dot, 1e-6);
}
private static Vector3 TransformLocalVector(Matrix4x4 linear, Vector3 localVector)
{
Vector3 world = new Vector3(
linear.M11 * localVector.X + linear.M12 * localVector.Y + linear.M13 * localVector.Z,
linear.M21 * localVector.X + linear.M22 * localVector.Y + linear.M23 * localVector.Z,
linear.M31 * localVector.X + linear.M32 * localVector.Y + linear.M33 * localVector.Z);
return Vector3.Normalize(world);
}
}
}

View File

@ -0,0 +1,149 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Core;
using NavisworksTransport.Utils;
using NavisworksTransport.Utils.CoordinateSystem;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class CanonicalRailOffsetResolverTests
{
[TestMethod]
public void OverRail_ShouldOffsetTrackedCenterAlongNormalByHalfHeight()
{
PathRoute route = new PathRoute
{
PathType = PathType.Rail,
RailMountMode = RailMountMode.OverRail,
RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine
};
RailLocalFrame frame = new RailLocalFrame(Vector3.UnitX, Vector3.UnitY, Vector3.UnitZ);
Vector3 trackedCenter = CanonicalRailOffsetResolver.ResolveTrackedCenter(
route,
Vector3.Zero,
frame,
4.0);
AssertVector(trackedCenter, 0.0, 0.0, 2.0);
}
[TestMethod]
public void UnderRail_ShouldOffsetTrackedCenterOppositeToNormalByHalfHeight()
{
PathRoute route = new PathRoute
{
PathType = PathType.Rail,
RailMountMode = RailMountMode.UnderRail,
RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine
};
RailLocalFrame frame = new RailLocalFrame(Vector3.UnitX, Vector3.UnitY, Vector3.UnitZ);
Vector3 trackedCenter = CanonicalRailOffsetResolver.ResolveTrackedCenter(
route,
Vector3.Zero,
frame,
4.0);
AssertVector(trackedCenter, 0.0, 0.0, -2.0);
}
[TestMethod]
public void SlopedNormal_ShouldMoveTrackedCenterAlongRailNormalInsteadOfWorldUp()
{
PathRoute route = new PathRoute
{
PathType = PathType.Rail,
RailMountMode = RailMountMode.OverRail,
RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine
};
Vector3 normal = Vector3.Normalize(new Vector3(0f, 1f, 1f));
RailLocalFrame frame = new RailLocalFrame(Vector3.UnitX, Vector3.UnitY, normal);
Vector3 trackedCenter = CanonicalRailOffsetResolver.ResolveTrackedCenter(
route,
new Vector3(10f, 20f, 30f),
frame,
4.0);
Vector3 expected = new Vector3(10f, 20f, 30f) + normal * 2f;
AssertVector(trackedCenter, expected.X, expected.Y, expected.Z);
}
[TestMethod]
public void AnchorSemantic_ShouldOnlyUseHalfHeightOffset_WhenReferencePointIsAlreadyAnchor()
{
PathRoute route = new PathRoute
{
PathType = PathType.Rail,
RailMountMode = RailMountMode.OverRail,
RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine
};
RailLocalFrame frame = new RailLocalFrame(Vector3.UnitX, Vector3.UnitY, Vector3.UnitZ);
Vector3 trackedCenter = CanonicalRailOffsetResolver.ResolveTrackedCenter(
route,
Vector3.Zero,
frame,
4.0);
AssertVector(trackedCenter, 0.0, 0.0, 2.0);
}
[TestMethod]
public void RailNormalOffset_ShouldAddExtraDisplacementAlongRailNormal()
{
PathRoute route = new PathRoute
{
PathType = PathType.Rail,
RailMountMode = RailMountMode.OverRail,
RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine,
RailNormalOffset = 1.5
};
RailLocalFrame frame = new RailLocalFrame(Vector3.UnitX, Vector3.UnitY, Vector3.UnitZ);
Vector3 trackedCenter = CanonicalRailOffsetResolver.ResolveTrackedCenter(
route,
Vector3.Zero,
frame,
4.0);
AssertVector(trackedCenter, 0.0, 0.0, 3.5);
}
[TestMethod]
public void PreservedTrackedCenterOffset_ShouldBeMeasuredFromRailSemanticCenter()
{
Vector3 semanticTrackedCenter = new Vector3(10f, 20f, 30f);
Vector3 actualTrackedCenter = new Vector3(11.25f, 19.5f, 30.75f);
Vector3 offset = RailPathPoseHelper.CalculatePreservedTrackedCenterOffset(
actualTrackedCenter,
semanticTrackedCenter);
AssertVector(offset, 1.25, -0.5, 0.75);
}
[TestMethod]
public void ResolvePreservedTrackedCenterPosition_ShouldReuseRailSemanticCenterPlusOffset()
{
Vector3 semanticTrackedCenter = new Vector3(3f, -2f, 8f);
Vector3 preservedOffset = new Vector3(0.2f, -0.4f, 0.6f);
Vector3 preservedTrackedCenter = RailPathPoseHelper.ResolvePreservedTrackedCenterPosition(
semanticTrackedCenter,
preservedOffset);
AssertVector(preservedTrackedCenter, 3.2, -2.4, 8.6);
}
private static void AssertVector(Vector3 actual, double x, double y, double z)
{
Assert.AreEqual(x, actual.X, 1e-6);
Assert.AreEqual(y, actual.Y, 1e-6);
Assert.AreEqual(z, actual.Z, 1e-6);
}
}
}

View File

@ -0,0 +1,237 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
using System;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class CanonicalRailPoseBuilderTests
{
[TestMethod]
public void StraightCanonicalPath_ZUpConvention_ShouldProduceIdentityLikeBasis()
{
bool ok = CanonicalRailPoseBuilder.TryCreateQuaternion(
new Vector3(0, 0, 0),
new Vector3(1, 0, 0),
new Vector3(2, 0, 0),
Vector3.UnitZ,
ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp),
out Quaternion rotation);
Assert.IsTrue(ok);
Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(rotation);
AssertColumn(linear, 0, 1, 0, 0);
AssertColumn(linear, 1, 0, 1, 0);
AssertColumn(linear, 2, 0, 0, 1);
}
[TestMethod]
public void StraightCanonicalPath_YUpConvention_ShouldMapLocalYToCanonicalUp()
{
bool ok = CanonicalRailPoseBuilder.TryCreateQuaternion(
new Vector3(0, 0, 0),
new Vector3(1, 0, 0),
new Vector3(2, 0, 0),
Vector3.UnitZ,
ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.YUp),
out Quaternion rotation);
Assert.IsTrue(ok);
Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(rotation);
AssertColumn(linear, 0, 1, 0, 0);
AssertColumn(linear, 1, 0, 0, 1);
AssertColumn(linear, 2, 0, -1, 0);
}
[TestMethod]
public void SlopedPath_ShouldKeepForwardAlignedWithPathAndUpOrthogonalized()
{
bool ok = CanonicalRailPoseBuilder.TryCreateBasis(
new Vector3(0, 0, 0),
new Vector3(1, 1, 1),
new Vector3(2, 2, 2),
Vector3.UnitZ,
out Vector3 forward,
out Vector3 lateral,
out Vector3 up);
Assert.IsTrue(ok);
Assert.AreEqual(1.0, forward.Length(), 1e-6);
Assert.AreEqual(1.0, lateral.Length(), 1e-6);
Assert.AreEqual(1.0, up.Length(), 1e-6);
Assert.AreEqual(0.0, Vector3.Dot(forward, up), 1e-6);
Assert.AreEqual(0.0, Vector3.Dot(forward, lateral), 1e-6);
Assert.AreEqual(0.0, Vector3.Dot(lateral, up), 1e-6);
}
[TestMethod]
public void SlopedPath_ShouldCreateRailLocalFrameWithStableNormalCloseToCanonicalUp()
{
bool ok = CanonicalRailPoseBuilder.TryCreateLocalFrame(
new Vector3(0, 0, 0),
new Vector3(10, 0, 1),
new Vector3(20, 0, 2),
Vector3.UnitZ,
out RailLocalFrame frame);
Assert.IsTrue(ok);
Assert.IsTrue(Vector3.Dot(frame.Forward, Vector3.UnitX) > 0.99f);
Assert.IsTrue(Vector3.Dot(frame.Normal, Vector3.UnitZ) > 0.99f);
Assert.AreEqual(0.0, Vector3.Dot(frame.Forward, frame.Normal), 1e-6);
Assert.AreEqual(0.0, Vector3.Dot(frame.Forward, frame.Lateral), 1e-6);
Assert.AreEqual(0.0, Vector3.Dot(frame.Lateral, frame.Normal), 1e-6);
}
[TestMethod]
public void ZeroLocalUpRotation_ShouldMatchCurrentRailBaseline()
{
var convention = ModelAxisConvention.CreateRailAssetConvention();
Assert.IsTrue(CanonicalRailPoseBuilder.TryCreateQuaternion(
new Vector3(0, 0, 0),
new Vector3(1, 0, 0),
new Vector3(2, 0, 0),
Vector3.UnitZ,
convention,
out Quaternion baselineRotation));
Assert.IsTrue(CanonicalRailPoseBuilder.TryCreateQuaternion(
new Vector3(0, 0, 0),
new Vector3(1, 0, 0),
new Vector3(2, 0, 0),
Vector3.UnitZ,
convention,
0.0,
out Quaternion correctedRotation));
AssertQuaternionEquivalent(baselineRotation, correctedRotation);
}
[TestMethod]
public void LocalUpRotation_ShouldAlignCorrectedLocalForwardWithRailTangent()
{
var convention = ModelAxisConvention.CreateRailAssetConvention();
Assert.IsTrue(CanonicalRailPoseBuilder.TryCreateQuaternion(
new Vector3(0, 0, 0),
new Vector3(1, 0, 0),
new Vector3(2, 0, 0),
Vector3.UnitZ,
convention,
90.0,
out Quaternion rotation));
Quaternion localPreRotation = Quaternion.CreateFromAxisAngle(
Vector3.Normalize(convention.UpUnitVector),
(float)(90.0 * Math.PI / 180.0));
Vector3 correctedLocalForward = Vector3.Normalize(Vector3.Transform(convention.ForwardUnitVector, localPreRotation));
Vector3 correctedLocalUp = Vector3.Normalize(Vector3.Transform(convention.UpUnitVector, localPreRotation));
Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(rotation);
Vector3 mappedForward = TransformLocalVector(linear, correctedLocalForward);
Vector3 mappedUp = TransformLocalVector(linear, correctedLocalUp);
AssertVector(mappedForward, 1, 0, 0);
AssertVector(mappedUp, 0, 0, 1);
}
[TestMethod]
public void LocalEulerCorrection_ShouldAlignCorrectedLocalForwardWithRailTangent()
{
var convention = ModelAxisConvention.CreateRailAssetConvention();
var correction = new LocalEulerRotationCorrection(0.0, 90.0, 0.0);
var adapter = new HostCoordinateAdapter(CoordinateSystemType.ZUp);
Quaternion correctionQuaternion = adapter.CreateCanonicalRotationCorrection(correction);
Assert.IsTrue(CanonicalRailPoseBuilder.TryCreateQuaternion(
new Vector3(0, 0, 0),
new Vector3(1, 0, 0),
new Vector3(2, 0, 0),
Vector3.UnitZ,
convention,
null,
correctionQuaternion,
out Quaternion rotation));
LocalAxisPoseBuilder.ApplyLocalPreRotation(convention, correctionQuaternion, out var correctedLocalForward, out var correctedLocalUp);
Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(rotation);
Vector3 mappedForward = TransformLocalVector(linear, correctedLocalForward);
Vector3 mappedUp = TransformLocalVector(linear, correctedLocalUp);
AssertVector(mappedForward, 1, 0, 0);
AssertVector(mappedUp, 0, 0, 1);
}
[TestMethod]
public void PreferredNormal_ShouldOverrideCanonicalUpForRailFrame()
{
bool ok = CanonicalRailPoseBuilder.TryCreateLocalFrame(
new Vector3(0, 0, 0),
new Vector3(1, 0, 0),
new Vector3(2, 0, 0),
Vector3.UnitZ,
Vector3.UnitY,
out RailLocalFrame frame);
Assert.IsTrue(ok);
AssertVector(frame.Forward, 1, 0, 0);
AssertVector(frame.Normal, 0, 1, 0);
AssertVector(frame.Lateral, 0, 0, -1);
}
private static void AssertColumn(Matrix4x4 matrix, int column, double x, double y, double z)
{
switch (column)
{
case 0:
Assert.AreEqual(x, matrix.M11, 1e-6);
Assert.AreEqual(y, matrix.M21, 1e-6);
Assert.AreEqual(z, matrix.M31, 1e-6);
break;
case 1:
Assert.AreEqual(x, matrix.M12, 1e-6);
Assert.AreEqual(y, matrix.M22, 1e-6);
Assert.AreEqual(z, matrix.M32, 1e-6);
break;
case 2:
Assert.AreEqual(x, matrix.M13, 1e-6);
Assert.AreEqual(y, matrix.M23, 1e-6);
Assert.AreEqual(z, matrix.M33, 1e-6);
break;
default:
Assert.Fail("Only first 3 columns are valid.");
break;
}
}
private static void AssertVector(Vector3 actual, double x, double y, double z)
{
Assert.AreEqual(x, actual.X, 1e-6);
Assert.AreEqual(y, actual.Y, 1e-6);
Assert.AreEqual(z, actual.Z, 1e-6);
}
private static void AssertQuaternionEquivalent(Quaternion expected, Quaternion actual)
{
double dot = Math.Abs(
expected.X * actual.X +
expected.Y * actual.Y +
expected.Z * actual.Z +
expected.W * actual.W);
Assert.AreEqual(1.0, dot, 1e-6);
}
private static Vector3 TransformLocalVector(Matrix4x4 linear, Vector3 localVector)
{
Vector3 world = new Vector3(
linear.M11 * localVector.X + linear.M12 * localVector.Y + linear.M13 * localVector.Z,
linear.M21 * localVector.X + linear.M22 * localVector.Y + linear.M23 * localVector.Z,
linear.M31 * localVector.X + linear.M32 * localVector.Y + linear.M33 * localVector.Z);
return Vector3.Normalize(world);
}
}
}

View File

@ -0,0 +1,65 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class CanonicalTrackedPositionResolverTests
{
[TestMethod]
public void GroundReference_ShouldOffsetHalfHeightAlongUp()
{
Vector3 result = CanonicalTrackedPositionResolver.ResolveGroundTrackedCenter(
new Vector3(10f, 20f, 30f),
Vector3.UnitZ,
4.0);
AssertVector(result, 10.0, 20.0, 32.0);
}
[TestMethod]
public void TopSuspensionReference_ShouldOffsetNegativeHalfHeightAlongUp()
{
Vector3 result = CanonicalTrackedPositionResolver.ResolveTopSuspensionTrackedCenter(
new Vector3(10f, 20f, 30f),
Vector3.UnitZ,
4.0);
AssertVector(result, 10.0, 20.0, 28.0);
}
[TestMethod]
public void ArbitraryNormalReference_ShouldOffsetAlongProvidedNormal()
{
Vector3 normal = Vector3.Normalize(new Vector3(0f, 1f, 1f));
Vector3 result = CanonicalTrackedPositionResolver.ResolveCenterFromContactReference(
new Vector3(5f, 6f, 7f),
normal,
4.0,
0.0);
Vector3 expected = new Vector3(5f, 6f, 7f) + normal * 2f;
AssertVector(result, expected.X, expected.Y, expected.Z);
}
[TestMethod]
public void MidSurfaceFactor_ShouldProduceNoOffset()
{
Vector3 result = CanonicalTrackedPositionResolver.ResolveCenterFromContactReference(
new Vector3(1f, 2f, 3f),
Vector3.UnitZ,
8.0,
0.5);
AssertVector(result, 1.0, 2.0, 3.0);
}
private static void AssertVector(Vector3 actual, double x, double y, double z)
{
Assert.AreEqual(x, actual.X, 1e-6);
Assert.AreEqual(y, actual.Y, 1e-6);
Assert.AreEqual(z, actual.Z, 1e-6);
}
}
}

View File

@ -0,0 +1,47 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class FragmentDefaultUpContextTests
{
[TestInitialize]
public void SetUp()
{
FragmentDefaultUpContext.ResetForTests();
}
[TestMethod]
public void GetOrCreateDocumentKey_ReusesFirstStableKey_WhenDocumentMetadataTemporarilyChanges()
{
const int runtimeDocumentId = 42;
string initialKey = FragmentDefaultUpContext.GetOrCreateDocumentKey(
runtimeDocumentId,
@"C:\models\floor2.nwd",
"Floor2");
string transientKey = FragmentDefaultUpContext.GetOrCreateDocumentKey(
runtimeDocumentId,
string.Empty,
"无标题");
Assert.AreEqual(initialKey, transientKey);
Assert.AreEqual("file:C:\\models\\floor2.nwd", transientKey);
}
[TestMethod]
public void GetOrCreateDocumentKey_UsesRuntimeScopedFallback_WhenFileNameIsUnavailable()
{
const int runtimeDocumentId = 7;
string key = FragmentDefaultUpContext.GetOrCreateDocumentKey(
runtimeDocumentId,
string.Empty,
"无标题");
Assert.AreEqual("runtime:7|title:无标题", key);
}
}
}

View File

@ -0,0 +1,172 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
using System;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class FragmentRepresentativePoseHelperTests
{
[TestMethod]
public void ShouldAverageFragmentMatrices_AndIgnoreTranslation()
{
Quaternion expectedRotation = Quaternion.Normalize(
Quaternion.CreateFromAxisAngle(Vector3.UnitZ, (float)(Math.PI / 6.0)));
double[] fragmentMatrix1 = CreateFragmentMatrix(expectedRotation, new Vector3(10.0f, 20.0f, 30.0f));
double[] fragmentMatrix2 = CreateFragmentMatrix(expectedRotation, new Vector3(-3.0f, 8.0f, 1.0f));
bool ok = FragmentRepresentativePoseHelper.TryGetRepresentativeRotation(
new[] { fragmentMatrix1, fragmentMatrix2 },
out Quaternion representativeRotation);
Assert.IsTrue(ok);
AssertVector(
Vector3.Transform(Vector3.UnitX, representativeRotation),
Vector3.Transform(Vector3.UnitX, expectedRotation),
1e-4);
AssertVector(
Vector3.Transform(Vector3.UnitY, representativeRotation),
Vector3.Transform(Vector3.UnitY, expectedRotation),
1e-4);
AssertVector(
Vector3.Transform(Vector3.UnitZ, representativeRotation),
Vector3.Transform(Vector3.UnitZ, expectedRotation),
1e-4);
}
[TestMethod]
public void ShouldIgnoreQuaternionSignWhenAveragingFragments()
{
Quaternion expectedRotation = Quaternion.Normalize(
Quaternion.CreateFromAxisAngle(Vector3.UnitX, (float)(Math.PI / 4.0)));
Quaternion oppositeSignRotation = new Quaternion(
-expectedRotation.X,
-expectedRotation.Y,
-expectedRotation.Z,
-expectedRotation.W);
bool ok = FragmentRepresentativePoseHelper.TryGetRepresentativeRotation(
new[] { expectedRotation, oppositeSignRotation },
out Quaternion representativeRotation);
Assert.IsTrue(ok);
AssertVector(
Vector3.Transform(Vector3.UnitY, representativeRotation),
Vector3.Transform(Vector3.UnitY, expectedRotation),
1e-4);
AssertVector(
Vector3.Transform(Vector3.UnitZ, representativeRotation),
Vector3.Transform(Vector3.UnitZ, expectedRotation),
1e-4);
}
[TestMethod]
public void ShouldFail_WhenNoFragmentRotationsAreAvailable()
{
bool ok = FragmentRepresentativePoseHelper.TryGetRepresentativeRotation(
Array.Empty<double[]>(),
out Quaternion representativeRotation);
Assert.IsFalse(ok);
Assert.AreEqual(Quaternion.Identity, representativeRotation);
}
[TestMethod]
public void ShouldPreserveAxes_ForObservedYUpRealObjectFragmentBasis()
{
Vector3 axisX = new Vector3(1.0f, 0.0f, 0.0f);
Vector3 axisY = new Vector3(0.0f, 0.0f, 1.0f);
Vector3 axisZ = new Vector3(0.0f, -1.0f, 0.0f);
double[] fragmentMatrix =
{
axisX.X, axisX.Y, axisX.Z, 0.0,
axisY.X, axisY.Y, axisY.Z, 0.0,
axisZ.X, axisZ.Y, axisZ.Z, 0.0,
0.0, 0.0, 0.0, 1.0
};
bool ok = FragmentRepresentativePoseHelper.TryExtractRotationFromFragmentMatrix(
fragmentMatrix,
out Quaternion rotation);
Assert.IsTrue(ok);
AssertVector(Vector3.Transform(Vector3.UnitX, rotation), axisX, 1e-4);
AssertVector(Vector3.Transform(Vector3.UnitY, rotation), axisY, 1e-4);
AssertVector(Vector3.Transform(Vector3.UnitZ, rotation), axisZ, 1e-4);
}
[TestMethod]
public void RepresentativeFrame_ShouldExposeSameAxes_AsRepresentativeRotation()
{
Vector3 axisX = new Vector3(1.0f, 0.0f, 0.0f);
Vector3 axisY = new Vector3(0.0f, 0.0f, 1.0f);
Vector3 axisZ = new Vector3(0.0f, -1.0f, 0.0f);
double[] fragmentMatrix =
{
axisX.X, axisX.Y, axisX.Z, 0.0,
axisY.X, axisY.Y, axisY.Z, 0.0,
axisZ.X, axisZ.Y, axisZ.Z, 0.0,
0.0, 0.0, 0.0, 1.0
};
bool ok = FragmentRepresentativePoseHelper.TryGetRepresentativeFrame(
new[] { fragmentMatrix },
out FragmentRepresentativePoseHelper.RepresentativeFrame frame);
Assert.IsTrue(ok);
AssertVector(frame.AxisX, axisX, 1e-4);
AssertVector(frame.AxisY, axisY, 1e-4);
AssertVector(frame.AxisZ, axisZ, 1e-4);
AssertVector(Vector3.Transform(Vector3.UnitX, frame.Rotation), axisX, 1e-4);
AssertVector(Vector3.Transform(Vector3.UnitY, frame.Rotation), axisY, 1e-4);
AssertVector(Vector3.Transform(Vector3.UnitZ, frame.Rotation), axisZ, 1e-4);
}
[TestMethod]
public void TryInterpretRepresentativeFrame_YUpHost_WithFragmentDefaultUpZ_ShouldMapZToHostY()
{
var raw = new FragmentRepresentativePoseHelper.RepresentativeFrame(
new Vector3(1.0f, 0.0f, 0.0f),
new Vector3(0.0f, 0.0f, -1.0f),
new Vector3(0.0f, 1.0f, 0.0f),
Quaternion.Identity);
bool ok = FragmentRepresentativePoseHelper.TryInterpretRepresentativeFrame(
raw,
"Z",
"Y",
out var interpreted);
Assert.IsTrue(ok);
AssertVector(interpreted.AxisX, new Vector3(1.0f, 0.0f, 0.0f), 1e-4);
AssertVector(interpreted.AxisY, new Vector3(0.0f, 1.0f, 0.0f), 1e-4);
AssertVector(interpreted.AxisZ, new Vector3(0.0f, 0.0f, 1.0f), 1e-4);
}
private static double[] CreateFragmentMatrix(Quaternion rotation, Vector3 translation)
{
Vector3 axisX = Vector3.Transform(Vector3.UnitX, rotation);
Vector3 axisY = Vector3.Transform(Vector3.UnitY, rotation);
Vector3 axisZ = Vector3.Transform(Vector3.UnitZ, rotation);
return new[]
{
(double)axisX.X, (double)axisX.Y, (double)axisX.Z, 0.0,
(double)axisY.X, (double)axisY.Y, (double)axisY.Z, 0.0,
(double)axisZ.X, (double)axisZ.Y, (double)axisZ.Z, 0.0,
(double)translation.X, (double)translation.Y, (double)translation.Z, 1.0
};
}
private static void AssertVector(Vector3 actual, Vector3 expected, double tolerance)
{
Assert.AreEqual(expected.X, actual.X, tolerance);
Assert.AreEqual(expected.Y, actual.Y, tolerance);
Assert.AreEqual(expected.Z, actual.Z, tolerance);
}
}
}

View File

@ -0,0 +1,17 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Core;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class GroundPassageSpaceOffsetTests
{
[TestMethod]
public void GroundPassageSpaceVerticalOffset_ShouldStayAtHalfHeight_WhenHeightAlreadyIncludesLift()
{
double verticalOffset = PathPointRenderPlugin.ResolveGroundPathObjectSpaceVerticalOffset(5.7);
Assert.AreEqual(2.85, verticalOffset, 1e-9);
}
}
}

View File

@ -0,0 +1,42 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Core.Animation;
using NavisworksTransport.Utils.CoordinateSystem;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class GroundPathObjectLiftOffsetTests
{
[TestMethod]
public void YUp_GroundPathLift_ShouldOffsetAlongHostY()
{
Vector3 offset = PathAnimationManager.ResolveGroundPathObjectLiftOffsetVector(1.25, CoordinateSystemType.YUp);
AssertVector(offset, 0.0, 1.25, 0.0);
}
[TestMethod]
public void ZUp_GroundPathLift_ShouldOffsetAlongHostZ()
{
Vector3 offset = PathAnimationManager.ResolveGroundPathObjectLiftOffsetVector(1.25, CoordinateSystemType.ZUp);
AssertVector(offset, 0.0, 0.0, 1.25);
}
[TestMethod]
public void GroundPathLift_ShouldReturnZero_WhenLiftIsNotPositive()
{
Vector3 offset = PathAnimationManager.ResolveGroundPathObjectLiftOffsetVector(0.0, CoordinateSystemType.ZUp);
AssertVector(offset, 0.0, 0.0, 0.0);
}
private static void AssertVector(Vector3 actual, double x, double y, double z, double tolerance = 1e-6)
{
Assert.AreEqual(x, actual.X, tolerance);
Assert.AreEqual(y, actual.Y, tolerance);
Assert.AreEqual(z, actual.Z, tolerance);
}
}
}

View File

@ -0,0 +1,146 @@
using Autodesk.Navisworks.Api;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class HoistingCoordinateHelperTests
{
[TestMethod]
public void OffsetAlongUp_YUp_ShouldChangeHostYOnly()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var startPoint = new Vector3(1.0f, 2.0f, 3.0f);
Vector3 liftedPoint = HoistingCoordinateHelper.OffsetAlongUp3(startPoint, 10.0, adapter);
AssertPoint(liftedPoint, 1.0, 12.0, 3.0);
}
[TestMethod]
public void ProjectToAerialElevation_YUp_ShouldKeepGroundHorizontalCoords()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var groundPoint = new Vector3(4.0f, 5.0f, 6.0f);
var aerialReferencePoint = new Vector3(1.0f, 12.0f, 3.0f);
Vector3 projectedPoint = HoistingCoordinateHelper.ProjectToAerialElevation3(groundPoint, aerialReferencePoint, adapter);
AssertPoint(projectedPoint, 4.0, 12.0, 6.0);
}
[TestMethod]
public void RelativeHeight_YUp_ShouldUseHostYDifference()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var groundPoint = new Vector3(-10.0f, -30.0f, 8.0f);
var aerialPoint = new Vector3(-10.0f, 35.0f, 8.0f);
double relativeHeight = HoistingCoordinateHelper.GetRelativeHeight3(groundPoint, aerialPoint, adapter);
Assert.AreEqual(65.0, relativeHeight, 1e-9);
}
[TestMethod]
public void SetElevation_YUp_ShouldRestoreClickedGroundElevationWithoutChangingHorizontalProjection()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var aerialPoint = new Vector3(-180.50f, 28.17f, 14.83f);
var clickedGroundPoint = new Vector3(-180.50f, -12.40f, 14.83f);
Vector3 landingPoint = HoistingCoordinateHelper.SetElevation3(
aerialPoint,
HoistingCoordinateHelper.GetElevation(clickedGroundPoint, adapter),
adapter);
AssertPoint(landingPoint, -180.50, -12.40, 14.83);
}
[TestMethod]
public void CreateHorizontalTurnPoint_YUp_ShouldOperateInHostXZPlane()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var currentPoint = new Vector3(-168.70f, 28.17f, -30.01f);
var nextPoint = new Vector3(-180.50f, 28.17f, 14.83f);
Vector3 turnPointKeepCurrentX = HoistingCoordinateHelper.CreateHorizontalTurnPoint3(
currentPoint,
nextPoint,
keepCurrentFirstHorizontalAxis: true,
adapter: adapter);
Vector3 turnPointKeepCurrentZ = HoistingCoordinateHelper.CreateHorizontalTurnPoint3(
currentPoint,
nextPoint,
keepCurrentFirstHorizontalAxis: false,
adapter: adapter);
AssertPoint(turnPointKeepCurrentX, -168.70, 28.17, 14.83);
AssertPoint(turnPointKeepCurrentZ, -180.50, 28.17, -30.01);
}
[TestMethod]
public void GetHorizontalDirection_YUp_ShouldProjectToHostXZPlane()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var fromPoint = new Vector3(-164.81f, 45.45f, 74.69f);
var toPoint = new Vector3(-176.90f, 45.45f, 40.48f);
Vector3 horizontalDirection = HoistingCoordinateHelper.GetHorizontalDirection3(fromPoint, toPoint, adapter);
Assert.AreEqual(0.0, horizontalDirection.Y, 1e-5);
Assert.AreEqual(1.0, horizontalDirection.Length(), 1e-5);
Assert.IsTrue(horizontalDirection.X < 0.0f);
Assert.IsTrue(horizontalDirection.Z < 0.0f);
}
[TestMethod]
public void ExtendVerticalTransitionForObjectSpace_YUp_ShouldMoveLowerPointAlongHostY()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var lowerPoint = new Vector3(-191.56f, 29.05f, -2.63f);
Vector3 extendedPoint = HoistingCoordinateHelper.ExtendVerticalTransitionForObjectSpace3(
lowerPoint,
4.0,
isLowerPoint: true,
adapter: adapter);
AssertPoint(extendedPoint, -191.56, 25.05, -2.63);
}
[TestMethod]
public void ExtendVerticalTransitionForObjectSpace_YUp_ShouldNotChangeHigherPoint()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var higherPoint = new Vector3(-191.56f, 45.45f, -2.63f);
Vector3 unchangedPoint = HoistingCoordinateHelper.ExtendVerticalTransitionForObjectSpace3(
higherPoint,
4.0,
isLowerPoint: false,
adapter: adapter);
AssertPoint(unchangedPoint, -191.56, 45.45, -2.63);
}
[TestMethod]
public void OffsetAlongUp_ZUp_ShouldChangeHostZOnly()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.ZUp);
var startPoint = new Vector3(1.0f, 2.0f, 3.0f);
Vector3 liftedPoint = HoistingCoordinateHelper.OffsetAlongUp3(startPoint, 10.0, adapter);
AssertPoint(liftedPoint, 1.0, 2.0, 13.0);
}
private static void AssertPoint(Vector3 actual, double x, double y, double z)
{
Assert.AreEqual(x, actual.X, 1e-5);
Assert.AreEqual(y, actual.Y, 1e-5);
Assert.AreEqual(z, actual.Z, 1e-5);
}
}
}

View File

@ -0,0 +1,116 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class HoistingRealObjectPoseHelperTests
{
[TestMethod]
public void ShouldKeepActualUpAxis_WhenAligningHoistingForward()
{
Quaternion actualRotation = Quaternion.Normalize(new Quaternion(0.70710677f, 0.0f, 0.0f, 0.70710677f));
Vector3 hostUp = Vector3.UnitY;
Vector3 targetForward = -Vector3.UnitX;
bool ok = HoistingRealObjectPoseHelper.TryCreateRotationFromActualPose(
actualRotation,
targetForward,
hostUp,
out Quaternion resultRotation,
out LocalAxisDirection selectedDirection,
out Vector3 selectedAxisLocal,
out Vector3 projectedForward);
Assert.IsTrue(ok);
Assert.AreEqual(LocalAxisDirection.PositiveX, selectedDirection);
AssertVector(selectedAxisLocal, 1.0, 0.0, 0.0);
AssertVector(projectedForward, 1.0, 0.0, 0.0);
Vector3 transformedUp = Vector3.Normalize(Vector3.Transform(-Vector3.UnitZ, resultRotation));
AssertVector(transformedUp, 0.0, 1.0, 0.0, 1e-4);
Vector3 transformedForward = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, resultRotation));
AssertVector(transformedForward, 1.0, 0.0, 0.0, 1e-4);
}
[TestMethod]
public void ShouldChooseHorizontalAxisFromActualPose_ForHoisting()
{
Quaternion actualRotation = Quaternion.Normalize(new Quaternion(0.70710677f, 0.0f, 0.0f, 0.70710677f));
Vector3 hostUp = Vector3.UnitY;
Vector3 targetForward = Vector3.UnitX;
bool ok = HoistingRealObjectPoseHelper.TryCreateRotationFromActualPose(
actualRotation,
targetForward,
hostUp,
out Quaternion resultRotation,
out LocalAxisDirection selectedDirection,
out Vector3 selectedAxisLocal,
out Vector3 projectedForward);
Assert.IsTrue(ok);
Assert.AreEqual(LocalAxisDirection.PositiveX, selectedDirection);
AssertVector(selectedAxisLocal, 1.0, 0.0, 0.0);
AssertVector(projectedForward, 1.0, 0.0, 0.0);
Vector3 transformedUp = Vector3.Normalize(Vector3.Transform(-Vector3.UnitZ, resultRotation));
AssertVector(transformedUp, 0.0, 1.0, 0.0, 1e-4);
Vector3 transformedForward = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, resultRotation));
AssertVector(transformedForward, 1.0, 0.0, 0.0, 1e-4);
}
[TestMethod]
public void CreateRotationFromPlanarBasePose_ShouldReturnBaseRotation_WhenYawUnchanged()
{
Quaternion baseRotation = Quaternion.Normalize(
Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.31f) *
Quaternion.CreateFromAxisAngle(Vector3.UnitX, -0.42f));
Quaternion resultRotation = HoistingRealObjectPoseHelper.CreateRotationFromPlanarBasePose(
baseRotation,
baseYawRadians: 1.25,
targetYawRadians: 1.25,
hostUp: Vector3.UnitY);
AssertQuaternionEquivalent(resultRotation, baseRotation);
}
[TestMethod]
public void CreateRotationFromPlanarBasePose_ShouldPreserveTiltAndApplyDeltaYaw()
{
Quaternion baseRotation = Quaternion.Normalize(
Quaternion.CreateFromAxisAngle(Vector3.UnitY, 0.20f) *
Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.35f) *
Quaternion.CreateFromAxisAngle(Vector3.UnitX, -0.40f));
Quaternion resultRotation = HoistingRealObjectPoseHelper.CreateRotationFromPlanarBasePose(
baseRotation,
baseYawRadians: 0.0,
targetYawRadians: System.Math.PI / 2.0,
hostUp: Vector3.UnitY);
Quaternion expectedRotation = Quaternion.Normalize(
Quaternion.CreateFromAxisAngle(Vector3.UnitY, (float)(System.Math.PI / 2.0)) *
baseRotation);
AssertQuaternionEquivalent(resultRotation, expectedRotation);
}
private static void AssertVector(Vector3 actual, double x, double y, double z, double tolerance = 1e-6)
{
Assert.AreEqual(x, actual.X, tolerance);
Assert.AreEqual(y, actual.Y, tolerance);
Assert.AreEqual(z, actual.Z, tolerance);
}
private static void AssertQuaternionEquivalent(Quaternion actual, Quaternion expected, double tolerance = 1e-6)
{
float dot = Quaternion.Dot(Quaternion.Normalize(actual), Quaternion.Normalize(expected));
Assert.AreEqual(1.0, System.Math.Abs(dot), tolerance);
}
}
}

View File

@ -0,0 +1,443 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
using System.Numerics;
using System;
using Autodesk.Navisworks.Api;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class HostCoordinateAdapterTests
{
[TestMethod]
public void ZUp_PointRoundTrip_ShouldRemainUnchanged()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.ZUp);
var hostPoint = new Vector3(1.5f, -2.5f, 3.5f);
Vector3 canonicalPoint = adapter.ToCanonicalPoint3(hostPoint);
Vector3 roundTripPoint = adapter.FromCanonicalPoint3(canonicalPoint);
AssertPoint(roundTripPoint, 1.5, -2.5, 3.5);
}
[TestMethod]
public void YUp_PointAndVectorRoundTrip_ShouldMatchExpectedMapping()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var hostPoint = new Vector3(1.0f, 2.0f, 3.0f);
var hostVector = new Vector3(4.0f, 5.0f, 6.0f);
Vector3 canonicalPoint = adapter.ToCanonicalPoint3(hostPoint);
Vector3 canonicalVector = adapter.ToCanonicalVector3(hostVector);
AssertPoint(canonicalPoint, 1.0, -3.0, 2.0);
AssertVector(canonicalVector, 4.0, -6.0, 5.0);
Vector3 restoredPoint = adapter.FromCanonicalPoint3(canonicalPoint);
Vector3 restoredVector = adapter.FromCanonicalVector3(canonicalVector);
AssertPoint(restoredPoint, 1.0, 2.0, 3.0);
AssertVector(restoredVector, 4.0, 5.0, 6.0);
}
[TestMethod]
public void YUp_BoundsRoundTrip_ShouldPreserveBounds()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var hostBounds = new CanonicalBounds3(new Vector3(-2.0f, 1.0f, 3.0f), new Vector3(5.0f, 7.0f, 11.0f));
CanonicalBounds3 canonicalBounds = adapter.ToCanonicalBounds3(hostBounds);
CanonicalBounds3 restoredBounds = adapter.FromCanonicalBounds3(canonicalBounds);
AssertPoint(restoredBounds.Min, -2.0, 1.0, 3.0);
AssertPoint(restoredBounds.Max, 5.0, 7.0, 11.0);
}
[TestMethod]
public void YUp_CanonicalRotation_ShouldConvertToHostYUpBasis()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp);
Quaternion canonicalRotation = convention.CreateQuaternion(new Vector3(1, 0, 0), Vector3.UnitZ);
Matrix4x4 hostLinear = adapter.FromCanonicalLinearTransform(Matrix4x4.CreateFromQuaternion(canonicalRotation));
Assert.AreEqual(1.0, hostLinear.M11, 1e-6);
Assert.AreEqual(0.0, hostLinear.M21, 1e-6);
Assert.AreEqual(0.0, hostLinear.M31, 1e-6);
Assert.AreEqual(0.0, hostLinear.M12, 1e-6);
Assert.AreEqual(0.0, hostLinear.M22, 1e-6);
Assert.AreEqual(-1.0, hostLinear.M32, 1e-6);
Assert.AreEqual(0.0, hostLinear.M13, 1e-6);
Assert.AreEqual(1.0, hostLinear.M23, 1e-6);
Assert.AreEqual(0.0, hostLinear.M33, 1e-6);
}
[TestMethod]
public void YUp_CanonicalRotation_WithPlanarForward_ShouldProduceExpectedHostAxes()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp);
Vector3 hostForward = Vector3.Normalize(new Vector3(-0.997320f, 0.0f, 0.073164f));
Vector3 canonicalForward = adapter.ToCanonicalVector3(hostForward);
bool ok = CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
canonicalForward,
HostCoordinateAdapter.CanonicalUpVector3,
convention,
out Quaternion canonicalRotation);
Assert.IsTrue(ok);
Matrix4x4 canonicalLinear = Matrix4x4.CreateFromQuaternion(canonicalRotation);
Matrix4x4 hostLinear = adapter.FromCanonicalLinearTransform(canonicalLinear);
Quaternion hostQuaternion = Quaternion.CreateFromRotationMatrix(hostLinear);
Matrix4x4 reconstructedHostLinear = Matrix4x4.CreateFromQuaternion(hostQuaternion);
Assert.AreEqual(hostForward.X, reconstructedHostLinear.M11, 1e-3);
Assert.AreEqual(hostForward.Y, reconstructedHostLinear.M21, 1e-3);
Assert.AreEqual(hostForward.Z, reconstructedHostLinear.M31, 1e-3);
Assert.AreEqual(0.0, reconstructedHostLinear.M13, 1e-3);
Assert.AreEqual(1.0, reconstructedHostLinear.M23, 1e-3);
Assert.AreEqual(0.0, reconstructedHostLinear.M33, 1e-3);
}
[TestMethod]
public void YUp_HostLinear_FromActualGroundPathPoints_ShouldMatchExpectedHostAxes()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp);
Vector3 previousPoint = new Vector3(-181.82637032765f, 14.8333330154419f, 3.38710203888167f);
Vector3 currentPoint = new Vector3(-181.82637032765f, 14.8333330154419f, 3.38710203888167f);
Vector3 nextPoint = new Vector3(-202.828908853644f, 16.3772966612769f, 14.304117291825f);
Vector3 canonicalPrevious = adapter.ToCanonicalPoint3(previousPoint);
Vector3 canonicalCurrent = adapter.ToCanonicalPoint3(currentPoint);
Vector3 canonicalNext = adapter.ToCanonicalPoint3(nextPoint);
Vector3 canonicalForward = canonicalNext - canonicalPrevious;
Assert.IsTrue(CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
canonicalForward,
HostCoordinateAdapter.CanonicalUpVector3,
convention,
out Quaternion canonicalRotation));
Matrix4x4 canonicalLinear = Matrix4x4.CreateFromQuaternion(canonicalRotation);
Matrix4x4 hostLinear = adapter.FromCanonicalLinearTransform(canonicalLinear);
Assert.AreEqual(-0.8873, hostLinear.M11, 1e-3);
Assert.AreEqual(0.0000, hostLinear.M21, 1e-3);
Assert.AreEqual(0.4612, hostLinear.M31, 1e-3);
Assert.AreEqual(0.4612, hostLinear.M12, 1e-3);
Assert.AreEqual(0.0000, hostLinear.M22, 1e-3);
Assert.AreEqual(0.8873, hostLinear.M32, 1e-3);
Assert.AreEqual(0.0000, hostLinear.M13, 1e-3);
Assert.AreEqual(1.0000, hostLinear.M23, 1e-3);
Assert.AreEqual(0.0000, hostLinear.M33, 1e-3);
}
[TestMethod]
public void YUp_HostRotationCorrection_ShouldMapHostYAxisToCanonicalUp()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var correction = new LocalEulerRotationCorrection(0.0, 90.0, 0.0);
Quaternion canonicalCorrection = adapter.CreateCanonicalRotationCorrection(correction);
Vector3 rotatedForward = Vector3.Transform(Vector3.UnitX, canonicalCorrection);
Assert.AreEqual(0.0, rotatedForward.X, 1e-6);
Assert.AreEqual(1.0, rotatedForward.Y, 1e-6);
Assert.AreEqual(0.0, rotatedForward.Z, 1e-6);
}
[TestMethod]
public void YUp_HostRotationCorrection_ShouldKeepHostYAxisInvariantInHostSpace()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
Quaternion hostCorrection = adapter.CreateHostRotationCorrection(
new LocalEulerRotationCorrection(0.0, 90.0, 0.0));
Vector3 rotatedUp = Vector3.Transform(Vector3.UnitY, hostCorrection);
AssertVector(rotatedUp, 0.0, 1.0, 0.0);
}
[TestMethod]
public void YUp_HostRotationCorrection_X90_ShouldRotateHostZIntoHostY()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
Quaternion hostCorrection = adapter.CreateHostRotationCorrection(
new LocalEulerRotationCorrection(90.0, 0.0, 0.0));
Vector3 rotatedHostZ = Vector3.Transform(Vector3.UnitZ, hostCorrection);
AssertVector(rotatedHostZ, 0.0, -1.0, 0.0);
}
[TestMethod]
public void YUp_HostRotationCorrection_Z90_ShouldRotateHostXIntoNegativeHostY()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
Quaternion hostCorrection = adapter.CreateHostRotationCorrection(
new LocalEulerRotationCorrection(0.0, 0.0, 90.0));
Vector3 rotatedHostX = Vector3.Transform(Vector3.UnitX, hostCorrection);
AssertVector(rotatedHostX, 0.0, 1.0, 0.0);
}
[TestMethod]
public void ComposeHostQuaternion_ShouldApplyHostCorrectionAfterBaseline()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var baseline = Quaternion.Identity;
Quaternion composed = adapter.ComposeHostQuaternion(
baseline,
new LocalEulerRotationCorrection(0.0, 90.0, 0.0));
Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(composed);
Assert.AreEqual(0.0, linear.M11, 1e-6);
Assert.AreEqual(0.0, linear.M21, 1e-6);
Assert.AreEqual(-1.0, linear.M31, 1e-6);
Assert.AreEqual(0.0, linear.M12, 1e-6);
Assert.AreEqual(1.0, linear.M22, 1e-6);
Assert.AreEqual(0.0, linear.M32, 1e-6);
}
[TestMethod]
public void ZUp_HostRotationCorrection_ShouldKeepHostZAxisInvariantInHostSpace()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.ZUp);
Quaternion hostCorrection = adapter.CreateHostRotationCorrection(
new LocalEulerRotationCorrection(0.0, 0.0, 90.0));
Vector3 rotatedUp = Vector3.Transform(Vector3.UnitZ, hostCorrection);
AssertVector(rotatedUp, 0.0, 0.0, 1.0);
}
[TestMethod]
public void ZUp_ComposeHostQuaternion_ShouldApplyHostZCorrectionAfterBaseline()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.ZUp);
var baseline = Quaternion.Identity;
Quaternion composed = adapter.ComposeHostQuaternion(
baseline,
new LocalEulerRotationCorrection(0.0, 0.0, 90.0));
Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(composed);
Assert.AreEqual(0.0, linear.M11, 1e-6);
Assert.AreEqual(1.0, linear.M21, 1e-6);
Assert.AreEqual(0.0, linear.M31, 1e-6);
Assert.AreEqual(-1.0, linear.M12, 1e-6);
Assert.AreEqual(0.0, linear.M22, 1e-6);
Assert.AreEqual(0.0, linear.M32, 1e-6);
Assert.AreEqual(0.0, linear.M13, 1e-6);
Assert.AreEqual(0.0, linear.M23, 1e-6);
Assert.AreEqual(1.0, linear.M33, 1e-6);
}
[TestMethod]
public void YUp_ComposeHostQuaternion_ForGroundBaseline_ShouldRotateAxesAroundHostXAxis()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
Vector3 baselineX = new Vector3(-0.8987f, 0.0000f, -0.4386f);
Vector3 baselineY = new Vector3(0.0000f, 1.0000f, 0.0000f);
Vector3 baselineZ = new Vector3(0.4386f, 0.0000f, -0.8987f);
Quaternion baseline = CreateQuaternionFromAxes(baselineX, baselineY, baselineZ);
var correction = new LocalEulerRotationCorrection(90.0, 0.0, 0.0);
Quaternion composed = adapter.ComposeHostQuaternion(baseline, correction);
Quaternion hostCorrection = adapter.CreateHostRotationCorrection(correction);
Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(composed);
AssertAxis(linear, 1, Vector3.Transform(baselineX, hostCorrection));
AssertAxis(linear, 2, Vector3.Transform(baselineY, hostCorrection));
AssertAxis(linear, 3, Vector3.Transform(baselineZ, hostCorrection));
}
[TestMethod]
public void YUp_ComposeHostQuaternion_ForGroundBaseline_ShouldRotateAxesAroundHostYAxis()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
Vector3 baselineX = new Vector3(-0.8987f, 0.0000f, -0.4386f);
Vector3 baselineY = new Vector3(0.0000f, 1.0000f, 0.0000f);
Vector3 baselineZ = new Vector3(0.4386f, 0.0000f, -0.8987f);
Quaternion baseline = CreateQuaternionFromAxes(baselineX, baselineY, baselineZ);
var correction = new LocalEulerRotationCorrection(0.0, 90.0, 0.0);
Quaternion composed = adapter.ComposeHostQuaternion(baseline, correction);
Quaternion hostCorrection = adapter.CreateHostRotationCorrection(correction);
Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(composed);
AssertAxis(linear, 1, Vector3.Transform(baselineX, hostCorrection));
AssertAxis(linear, 2, Vector3.Transform(baselineY, hostCorrection));
AssertAxis(linear, 3, Vector3.Transform(baselineZ, hostCorrection));
}
[TestMethod]
public void YUp_ComposeHostQuaternion_ForGroundBaseline_ShouldRotateAxesAroundHostZAxis()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
Vector3 baselineX = new Vector3(-0.8987f, 0.0000f, -0.4386f);
Vector3 baselineY = new Vector3(0.0000f, 1.0000f, 0.0000f);
Vector3 baselineZ = new Vector3(0.4386f, 0.0000f, -0.8987f);
Quaternion baseline = CreateQuaternionFromAxes(baselineX, baselineY, baselineZ);
var correction = new LocalEulerRotationCorrection(0.0, 0.0, 90.0);
Quaternion composed = adapter.ComposeHostQuaternion(baseline, correction);
Quaternion hostCorrection = adapter.CreateHostRotationCorrection(correction);
Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(composed);
AssertAxis(linear, 1, Vector3.Transform(baselineX, hostCorrection));
AssertAxis(linear, 2, Vector3.Transform(baselineY, hostCorrection));
AssertAxis(linear, 3, Vector3.Transform(baselineZ, hostCorrection));
}
[TestMethod]
public void YUp_ComposeHostQuaternion_ForRailBaseline_ShouldRotateAxesAroundHostXAxis()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
Vector3 baselineX = new Vector3(-0.9900f, 0.1403f, -0.0163f);
Vector3 baselineY = new Vector3(0.1403f, 0.9901f, 0.0000f);
Vector3 baselineZ = new Vector3(0.0161f, -0.0023f, -0.9999f);
Quaternion baseline = CreateQuaternionFromAxes(baselineX, baselineY, baselineZ);
var correction = new LocalEulerRotationCorrection(90.0, 0.0, 0.0);
Quaternion composed = adapter.ComposeHostQuaternion(baseline, correction);
Quaternion hostCorrection = adapter.CreateHostRotationCorrection(correction);
Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(composed);
AssertAxis(linear, 1, Vector3.Transform(baselineX, hostCorrection));
AssertAxis(linear, 2, Vector3.Transform(baselineY, hostCorrection));
AssertAxis(linear, 3, Vector3.Transform(baselineZ, hostCorrection));
}
[TestMethod]
public void YUp_ComposeHostQuaternion_ForRailBaseline_ShouldRotateAxesAroundHostYAxis()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
Vector3 baselineX = new Vector3(-0.9900f, 0.1403f, -0.0163f);
Vector3 baselineY = new Vector3(0.1403f, 0.9901f, 0.0000f);
Vector3 baselineZ = new Vector3(0.0161f, -0.0023f, -0.9999f);
Quaternion baseline = CreateQuaternionFromAxes(baselineX, baselineY, baselineZ);
var correction = new LocalEulerRotationCorrection(0.0, 90.0, 0.0);
Quaternion composed = adapter.ComposeHostQuaternion(baseline, correction);
Quaternion hostCorrection = adapter.CreateHostRotationCorrection(correction);
Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(composed);
AssertAxis(linear, 1, Vector3.Transform(baselineX, hostCorrection));
AssertAxis(linear, 2, Vector3.Transform(baselineY, hostCorrection));
AssertAxis(linear, 3, Vector3.Transform(baselineZ, hostCorrection));
}
[TestMethod]
public void YUp_ComposeHostQuaternion_ForRailBaseline_ShouldRotateAxesAroundHostZAxis()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
Vector3 baselineX = new Vector3(-0.9900f, 0.1403f, -0.0163f);
Vector3 baselineY = new Vector3(0.1403f, 0.9901f, 0.0000f);
Vector3 baselineZ = new Vector3(0.0161f, -0.0023f, -0.9999f);
Quaternion baseline = CreateQuaternionFromAxes(baselineX, baselineY, baselineZ);
var correction = new LocalEulerRotationCorrection(0.0, 0.0, 90.0);
Quaternion composed = adapter.ComposeHostQuaternion(baseline, correction);
Quaternion hostCorrection = adapter.CreateHostRotationCorrection(correction);
Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(composed);
AssertAxis(linear, 1, Vector3.Transform(baselineX, hostCorrection));
AssertAxis(linear, 2, Vector3.Transform(baselineY, hostCorrection));
AssertAxis(linear, 3, Vector3.Transform(baselineZ, hostCorrection));
}
[TestMethod]
public void RemapHostSemanticCorrectionToLocalAxes_ShouldPermuteAnglesFromSemanticAxes()
{
LocalEulerRotationCorrection mapped = HostCoordinateAdapter.RemapHostSemanticCorrectionToLocalAxes(
new LocalEulerRotationCorrection(10.0, 20.0, 30.0),
LocalAxisDirection.PositiveY,
LocalAxisDirection.PositiveZ,
LocalAxisDirection.PositiveX);
Assert.AreEqual(30.0, mapped.XDegrees, 1e-9);
Assert.AreEqual(10.0, mapped.YDegrees, 1e-9);
Assert.AreEqual(20.0, mapped.ZDegrees, 1e-9);
}
[TestMethod]
public void RemapHostSemanticCorrectionToLocalAxes_ShouldApplySignForNegativeMappedAxes()
{
LocalEulerRotationCorrection mapped = HostCoordinateAdapter.RemapHostSemanticCorrectionToLocalAxes(
new LocalEulerRotationCorrection(10.0, 20.0, 30.0),
LocalAxisDirection.NegativeY,
LocalAxisDirection.PositiveZ,
LocalAxisDirection.NegativeX);
Assert.AreEqual(-30.0, mapped.XDegrees, 1e-9);
Assert.AreEqual(-10.0, mapped.YDegrees, 1e-9);
Assert.AreEqual(20.0, mapped.ZDegrees, 1e-9);
}
private static void AssertPoint(Vector3 point, double x, double y, double z)
{
Assert.AreEqual(x, point.X, 1e-9);
Assert.AreEqual(y, point.Y, 1e-9);
Assert.AreEqual(z, point.Z, 1e-9);
}
private static void AssertVector(Vector3 vector, double x, double y, double z, double tolerance = 1e-6)
{
Assert.AreEqual(x, vector.X, tolerance);
Assert.AreEqual(y, vector.Y, tolerance);
Assert.AreEqual(z, vector.Z, tolerance);
}
private static Quaternion CreateQuaternionFromAxes(Vector3 xAxis, Vector3 yAxis, Vector3 zAxis)
{
Matrix4x4 basis = new Matrix4x4(
xAxis.X, yAxis.X, zAxis.X, 0f,
xAxis.Y, yAxis.Y, zAxis.Y, 0f,
xAxis.Z, yAxis.Z, zAxis.Z, 0f,
0f, 0f, 0f, 1f);
return Quaternion.Normalize(Quaternion.CreateFromRotationMatrix(basis));
}
private static void AssertAxis(Matrix4x4 linear, int axisIndex, Vector3 expectedAxis, double tolerance = 1e-4)
{
Vector3 normalizedExpected = Vector3.Normalize(expectedAxis);
switch (axisIndex)
{
case 1:
Assert.AreEqual(normalizedExpected.X, linear.M11, tolerance);
Assert.AreEqual(normalizedExpected.Y, linear.M21, tolerance);
Assert.AreEqual(normalizedExpected.Z, linear.M31, tolerance);
break;
case 2:
Assert.AreEqual(normalizedExpected.X, linear.M12, tolerance);
Assert.AreEqual(normalizedExpected.Y, linear.M22, tolerance);
Assert.AreEqual(normalizedExpected.Z, linear.M32, tolerance);
break;
case 3:
Assert.AreEqual(normalizedExpected.X, linear.M13, tolerance);
Assert.AreEqual(normalizedExpected.Y, linear.M23, tolerance);
Assert.AreEqual(normalizedExpected.Z, linear.M33, tolerance);
break;
default:
Assert.Fail($"未知轴索引: {axisIndex}");
break;
}
}
}
}

View File

@ -0,0 +1,121 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class ModelAxisConventionTests
{
[TestMethod]
public void DefaultForYUp_ShouldUseXForwardAndYUp()
{
ModelAxisConvention convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.YUp);
Assert.AreEqual(LocalAxisDirection.PositiveX, convention.ForwardAxis);
Assert.AreEqual(LocalAxisDirection.PositiveY, convention.UpAxis);
}
[TestMethod]
public void DefaultForZUp_ShouldUseXForwardAndZUp()
{
ModelAxisConvention convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp);
Assert.AreEqual(LocalAxisDirection.PositiveX, convention.ForwardAxis);
Assert.AreEqual(LocalAxisDirection.PositiveZ, convention.UpAxis);
}
[TestMethod]
public void YUpConvention_CreateLinearTransform_ShouldMapLocalYToWorldUp()
{
var convention = new ModelAxisConvention(LocalAxisDirection.PositiveX, LocalAxisDirection.PositiveY);
Matrix4x4 linear = convention.CreateLinearTransform3(new Vector3(1, 0, 0), new Vector3(0, 0, 1));
AssertColumn(linear, 0, 1, 0, 0);
AssertColumn(linear, 1, 0, 0, 1);
AssertColumn(linear, 2, 0, -1, 0);
}
[TestMethod]
public void ZUpConvention_CreateLinearTransform_ShouldMapLocalZToWorldUp()
{
var convention = new ModelAxisConvention(LocalAxisDirection.PositiveX, LocalAxisDirection.PositiveZ);
Matrix4x4 linear = convention.CreateLinearTransform3(new Vector3(1, 0, 0), new Vector3(0, 0, 1));
AssertColumn(linear, 0, 1, 0, 0);
AssertColumn(linear, 1, 0, 1, 0);
AssertColumn(linear, 2, 0, 0, 1);
}
[TestMethod]
public void YUpConvention_CreateRotation_ShouldAlignLocalAxesToRequestedWorldAxes()
{
var convention = new ModelAxisConvention(LocalAxisDirection.PositiveX, LocalAxisDirection.PositiveY);
Quaternion rotation = convention.CreateQuaternion(new Vector3(0, 1, 0), new Vector3(0, 0, 1));
Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(rotation);
AssertColumn(linear, 0, 0, 1, 0);
AssertColumn(linear, 1, 0, 0, 1);
AssertColumn(linear, 2, 1, 0, 0);
}
[TestMethod]
public void ReferenceRodAssetConvention_ShouldUseXForwardAndZUp()
{
ModelAxisConvention convention = ModelAxisConvention.CreateReferenceRodAssetConvention();
Assert.AreEqual(LocalAxisDirection.PositiveX, convention.ForwardAxis);
Assert.AreEqual(LocalAxisDirection.PositiveZ, convention.UpAxis);
Assert.AreEqual(LocalAxisDirection.NegativeY, convention.SideAxis);
}
[TestMethod]
public void VirtualObjectAssetConvention_CreateScaleVector_ShouldMapLengthWidthHeightToLocalAxes()
{
ModelAxisConvention convention = ModelAxisConvention.CreateVirtualObjectAssetConvention();
var scale = convention.CreateScaleVector3(12.0, 5.0, 7.0);
Assert.AreEqual(12.0, scale.X, 1e-6);
Assert.AreEqual(5.0, scale.Y, 1e-6);
Assert.AreEqual(7.0, scale.Z, 1e-6);
}
[TestMethod]
public void YUpConvention_CreateScaleVector_ShouldMapUpSizeToLocalY()
{
ModelAxisConvention convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.YUp);
var scale = convention.CreateScaleVector3(12.0, 5.0, 7.0);
Assert.AreEqual(12.0, scale.X, 1e-6);
Assert.AreEqual(7.0, scale.Y, 1e-6);
Assert.AreEqual(5.0, scale.Z, 1e-6);
}
private static void AssertColumn(Matrix4x4 matrix, int column, double x, double y, double z)
{
switch (column)
{
case 0:
Assert.AreEqual(x, matrix.M11, 1e-6);
Assert.AreEqual(y, matrix.M21, 1e-6);
Assert.AreEqual(z, matrix.M31, 1e-6);
break;
case 1:
Assert.AreEqual(x, matrix.M12, 1e-6);
Assert.AreEqual(y, matrix.M22, 1e-6);
Assert.AreEqual(z, matrix.M32, 1e-6);
break;
case 2:
Assert.AreEqual(x, matrix.M13, 1e-6);
Assert.AreEqual(y, matrix.M23, 1e-6);
Assert.AreEqual(z, matrix.M33, 1e-6);
break;
default:
Assert.Fail("Only first 3 columns are valid.");
break;
}
}
}
}

View File

@ -0,0 +1,56 @@
using System.Numerics;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class ObjectSpaceOrientationHelperTests
{
[TestMethod]
public void CalculateAxes_ShouldHonorProvidedUpReference()
{
var segmentDirection = new Vector3(10f, 0f, 0f);
var upReference = new Vector3(0f, 1f, 0f);
var (_, up) = ObjectSpaceOrientationHelper.CalculateAxes(segmentDirection, upReference);
Assert.AreEqual(0.0, up.X, 1e-6);
Assert.AreEqual(1.0, System.Math.Abs(up.Y), 1e-6);
Assert.AreEqual(0.0, up.Z, 1e-6);
}
[TestMethod]
public void CalculateAxes_ForVerticalSegment_ShouldUseHorizontalDirectionWithProvidedUpReference()
{
var segmentDirection = new Vector3(0f, 10f, 0f);
var upReference = new Vector3(0f, 1f, 0f);
var horizontalDirection = new Vector3(1f, 0f, 0f);
var (right, up) = ObjectSpaceOrientationHelper.CalculateAxes(segmentDirection, upReference, horizontalDirection);
Assert.AreEqual(0.0, right.X, 1e-6);
Assert.AreEqual(0.0, right.Y, 1e-6);
Assert.AreEqual(1.0, right.Z, 1e-6);
Assert.AreEqual(1.0, up.X, 1e-6);
Assert.AreEqual(0.0, up.Y, 1e-6);
Assert.AreEqual(0.0, up.Z, 1e-6);
}
[TestMethod]
public void TryCalculateAxes_ShouldReturnFalse_ForZeroLengthSegment()
{
var segmentDirection = Vector3.Zero;
var upReference = new Vector3(0f, 1f, 0f);
bool ok = ObjectSpaceOrientationHelper.TryCalculateAxes(
segmentDirection,
upReference,
out var axes);
Assert.IsFalse(ok);
Assert.AreEqual(Vector3.Zero, axes.right);
Assert.AreEqual(Vector3.Zero, axes.up);
}
}
}

View File

@ -0,0 +1,44 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class ObjectStartPlacementRequestTests
{
[TestMethod]
public void TranslationOnly_ShouldPreserveInitialPoseAndClearRotationCorrection()
{
var request = ObjectStartPlacementRequest.TranslationOnly;
Assert.AreEqual(ObjectStartPlacementMode.PreserveInitialPose, request.PlacementMode);
Assert.IsTrue(request.PreserveInitialPose);
Assert.AreEqual(LocalEulerRotationCorrection.Zero, request.RotationCorrection);
Assert.AreEqual(0.0, request.VerticalLiftInMeters, 1e-9);
}
[TestMethod]
public void RotationCorrectionRequest_ShouldKeepCorrectionAndAlignToPathPose()
{
var correction = new LocalEulerRotationCorrection(15.0, 30.0, 45.0);
var request = ObjectStartPlacementRequest.CreateRotationCorrection(correction, 0.35);
Assert.AreEqual(ObjectStartPlacementMode.AlignToPathPose, request.PlacementMode);
Assert.IsFalse(request.PreserveInitialPose);
Assert.AreEqual(correction, request.RotationCorrection);
Assert.AreEqual(0.35, request.VerticalLiftInMeters, 1e-9);
}
[TestMethod]
public void TranslationOnlyRequest_ShouldKeepVerticalLift()
{
var request = ObjectStartPlacementRequest.CreateTranslationOnly(0.28);
Assert.AreEqual(ObjectStartPlacementMode.PreserveInitialPose, request.PlacementMode);
Assert.IsTrue(request.PreserveInitialPose);
Assert.AreEqual(LocalEulerRotationCorrection.Zero, request.RotationCorrection);
Assert.AreEqual(0.28, request.VerticalLiftInMeters, 1e-9);
}
}
}

View File

@ -0,0 +1,32 @@
using Autodesk.Navisworks.Api;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class PathPointVisualizationTests
{
[TestMethod]
public void ResolveVisualizationRadius_WhenVisualizationDiameterIsUnset_ShouldUseDefaultRadius()
{
var point = new PathPoint(new Point3D(1.0, 2.0, 3.0), "测试点", PathPointType.WayPoint);
double radius = point.ResolveVisualizationRadius(0.25);
Assert.AreEqual(0.25, radius, 1e-9);
}
[TestMethod]
public void ResolveVisualizationRadius_WhenVisualizationDiameterIsSpecified_ShouldUseHalfDiameter()
{
var point = new PathPoint(new Point3D(1.0, 2.0, 3.0), "测试点", PathPointType.WayPoint)
{
VisualizationDiameter = 0.10
};
double radius = point.ResolveVisualizationRadius(0.25);
Assert.AreEqual(0.05, radius, 1e-9);
}
}
}

View File

@ -0,0 +1,123 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
using System.Collections.Generic;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class PathTargetFrameResolverTests
{
[TestMethod]
public void YUp_PlanarFrame_ShouldUseHostYAsUp()
{
bool ok = PathTargetFrameResolver.TryCreatePlanarHostFrame(
new Vector3(2.0f, 0.0f, 1.0f),
CoordinateSystemType.YUp,
out PathTargetFrame frame);
Assert.IsTrue(ok);
AssertVector(frame.Up, 0.0, 1.0, 0.0);
Assert.AreEqual(0.0, Vector3.Dot(frame.Forward, frame.Up), 1e-6);
Assert.AreEqual(1.0, frame.Side.Length(), 1e-6);
}
[TestMethod]
public void ZUp_PlanarFrame_ShouldUseHostZAsUp()
{
bool ok = PathTargetFrameResolver.TryCreatePlanarHostFrame(
new Vector3(2.0f, 1.0f, 0.0f),
CoordinateSystemType.ZUp,
out PathTargetFrame frame);
Assert.IsTrue(ok);
AssertVector(frame.Up, 0.0, 0.0, 1.0);
Assert.AreEqual(0.0, Vector3.Dot(frame.Forward, frame.Up), 1e-6);
Assert.AreEqual(1.0, frame.Side.Length(), 1e-6);
}
[TestMethod]
public void Hoisting_StartForward_ShouldUseHorizontalSegmentInsteadOfInitialLift()
{
var pathPoints = new List<Vector3>
{
new Vector3(-105.992f, -28.615f, 77.043f),
new Vector3(-105.992f, 16.230f, 77.043f),
new Vector3(-229.870f, 16.230f, -32.982f)
};
bool ok = PathTargetFrameResolver.TryResolvePlanarStartHostForward(
NavisworksTransport.PathType.Hoisting,
pathPoints,
out Vector3 hostForward);
Assert.IsTrue(ok);
AssertVector(Vector3.Normalize(hostForward), -0.7477, 0.0, -0.6640, 1e-4);
}
[TestMethod]
public void Hoisting_StartFrame_ShouldUseHorizontalSegmentAndKeepHostUp()
{
var pathPoints = new List<Vector3>
{
new Vector3(-105.992f, -28.615f, 77.043f),
new Vector3(-105.992f, 16.230f, 77.043f),
new Vector3(-229.870f, 16.230f, -32.982f)
};
bool ok = PathTargetFrameResolver.TryCreatePlanarStartHostFrame(
NavisworksTransport.PathType.Hoisting,
pathPoints,
CoordinateSystemType.YUp,
out PathTargetFrame frame);
Assert.IsTrue(ok);
AssertVector(frame.Up, 0.0, 1.0, 0.0);
Assert.AreEqual(0.0, Vector3.Dot(frame.Forward, frame.Up), 1e-6);
}
[TestMethod]
public void Hoisting_StartForward_ShouldUseEditedRealPathHorizontalSegment_FromDebugLog()
{
var pathPoints = new List<Vector3>
{
new Vector3(-197.436f, 14.833f, 29.413f),
new Vector3(-197.436f, 31.240f, 29.413f),
new Vector3(-213.650f, 31.240f, 29.413f)
};
bool ok = PathTargetFrameResolver.TryResolvePlanarStartHostForward(
NavisworksTransport.PathType.Hoisting,
pathPoints,
out Vector3 hostForward);
Assert.IsTrue(ok);
AssertVector(Vector3.Normalize(hostForward), -1.0, 0.0, 0.0, 1e-6);
}
[TestMethod]
public void YUp_PlanarYaw_ShouldUseHostXZPlane_NotHostXY()
{
bool ok1 = PathTargetFrameResolver.TryResolvePlanarHostYaw(
new Vector3(1.0f, 0.0f, 0.0f),
CoordinateSystemType.YUp,
out double yaw1);
bool ok2 = PathTargetFrameResolver.TryResolvePlanarHostYaw(
new Vector3(0.0f, 0.0f, 1.0f),
CoordinateSystemType.YUp,
out double yaw2);
Assert.IsTrue(ok1);
Assert.IsTrue(ok2);
Assert.AreEqual(0.0, yaw1, 1e-6);
Assert.AreEqual(-System.Math.PI / 2.0, yaw2, 1e-6);
}
private static void AssertVector(Vector3 actual, double x, double y, double z, double tolerance = 1e-6)
{
Assert.AreEqual(x, actual.X, tolerance);
Assert.AreEqual(y, actual.Y, tolerance);
Assert.AreEqual(z, actual.Z, tolerance);
}
}
}

View File

@ -0,0 +1,38 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class ProjectReferenceFrameTests
{
[TestMethod]
public void DefaultForYUp_ShouldUseCanonicalUpAndYUpModelConvention()
{
ProjectReferenceFrame frame = ProjectReferenceFrame.CreateDefault(CoordinateSystemType.YUp);
Assert.AreEqual(0.0, frame.SphereCenterInCanonical3.X, 1e-9);
Assert.AreEqual(0.0, frame.SphereCenterInCanonical3.Y, 1e-9);
Assert.AreEqual(0.0, frame.SphereCenterInCanonical3.Z, 1e-9);
Assert.AreEqual(0.0, frame.ProjectUpInCanonical3.X, 1e-9);
Assert.AreEqual(0.0, frame.ProjectUpInCanonical3.Y, 1e-9);
Assert.AreEqual(1.0, frame.ProjectUpInCanonical3.Z, 1e-9);
Assert.AreEqual(LocalAxisDirection.PositiveX, frame.DefaultModelAxisConvention.ForwardAxis);
Assert.AreEqual(LocalAxisDirection.PositiveY, frame.DefaultModelAxisConvention.UpAxis);
}
[TestMethod]
public void Constructor_ShouldNormalizeProjectUp()
{
var frame = new ProjectReferenceFrame(
new Vector3(1, 2, 3),
new Vector3(0, 0, 10),
ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp));
Assert.AreEqual(0.0, frame.ProjectUpInCanonical3.X, 1e-9);
Assert.AreEqual(0.0, frame.ProjectUpInCanonical3.Y, 1e-9);
Assert.AreEqual(1.0, frame.ProjectUpInCanonical3.Z, 1e-9);
}
}
}

View File

@ -0,0 +1,118 @@
using Autodesk.Navisworks.Api;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.UI.WPF.ViewModels;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class RailAssemblyWorkflowContextTests
{
[TestMethod]
public void ResetEndFaceAnalysis_ClearsAnalysisStateAndSeedPoints()
{
var context = CreateContext();
context.HasEndFaceAnalysis = true;
context.EndFaceCenterPoint = new Point3D(1.0, 2.0, 3.0);
context.EndFaceNormal = new Vector3(1f, 0f, 0f);
context.EndFaceSeedPoints.Add(new Point3D(4.0, 5.0, 6.0));
context.ResetEndFaceAnalysis();
Assert.IsFalse(context.HasEndFaceAnalysis);
Assert.IsNull(context.EndFaceCenterPoint);
Assert.AreEqual(default(Vector3), context.EndFaceNormal);
Assert.AreEqual(0, context.EndFaceSeedPoints.Count);
}
[TestMethod]
public void ResetInstallationReference_RestoresDefaultOffsetAndClearsReferenceState()
{
var context = CreateContext();
context.HasInstallationReference = true;
context.InstallationPickPoint = new Point3D(1.0, 2.0, 3.0);
context.InstallationBaseAnchorPoint = new Point3D(4.0, 5.0, 6.0);
context.InstallationAnchorPoint = new Point3D(7.0, 8.0, 9.0);
context.InstallationPlaneNormal = new Vector3(0f, 1f, 0f);
context.InstallationPlaneSpanDirection = new Vector3(1f, 0f, 0f);
context.InstallationOffsetDistanceInMeters = 2.5;
context.InstallationReferenceRouteId = "route-1";
context.InstallationSeedPoints.Add(new Point3D(9.0, 8.0, 7.0));
context.AnchorVerticalOffsetInMeters = 3.5;
context.ResetInstallationReference();
Assert.IsFalse(context.HasInstallationReference);
Assert.IsNull(context.InstallationPickPoint);
Assert.IsNull(context.InstallationBaseAnchorPoint);
Assert.IsNull(context.InstallationAnchorPoint);
Assert.AreEqual(default(Vector3), context.InstallationPlaneNormal);
Assert.AreEqual(default(Vector3), context.InstallationPlaneSpanDirection);
Assert.AreEqual(0.0, context.InstallationOffsetDistanceInMeters, 1e-9);
Assert.IsNull(context.InstallationReferenceRouteId);
Assert.AreEqual(0, context.InstallationSeedPoints.Count);
Assert.AreEqual(0.25, context.AnchorVerticalOffsetInMeters, 1e-9);
}
[TestMethod]
public void ResetSession_ClearsTransientStateButPreservesConfiguration()
{
var context = CreateContext();
context.WorkflowMode = RailAssemblyWorkflowMode.EditSelectedRail;
context.TerminalObjectName = "箱体A";
context.TerminalObjectInfo = "info";
context.ReferenceRodLengthInMeters = 12.0;
context.ReferenceRodDiameterInMeters = 0.4;
context.SphereCenterX = 10.0;
context.SphereCenterY = 11.0;
context.SphereCenterZ = 12.0;
context.MountMode = RailMountMode.OverRail;
context.HasTerminalObject = true;
context.IsSelectingStartPoint = true;
context.IsSelectingEndFacePoints = true;
context.IsSelectingInstallationPoint = true;
context.StartPoint = new Point3D(1.0, 2.0, 3.0);
context.StartPointText = "(1,2,3)";
context.HasEndFaceAnalysis = true;
context.EndFaceCenterPoint = new Point3D(4.0, 5.0, 6.0);
context.HasInstallationReference = true;
context.InstallationReferenceRouteId = "route-2";
context.AnchorVerticalOffsetInMeters = 1.5;
context.EndFaceSeedPoints.Add(new Point3D(1.0, 0.0, 0.0));
context.InstallationSeedPoints.Add(new Point3D(0.0, 1.0, 0.0));
context.ResetSession();
Assert.AreEqual(RailAssemblyWorkflowMode.None, context.WorkflowMode);
Assert.IsFalse(context.HasTerminalObject);
Assert.IsFalse(context.IsSelectingStartPoint);
Assert.IsFalse(context.IsSelectingEndFacePoints);
Assert.IsFalse(context.IsSelectingInstallationPoint);
Assert.IsNull(context.TerminalObject);
Assert.IsNotNull(context.StartPoint);
Assert.AreEqual("未选择", context.TerminalObjectName);
Assert.AreEqual("请选择终点处已安装箱体", context.TerminalObjectInfo);
Assert.AreEqual("未选择", context.StartPointText);
Assert.IsFalse(context.HasEndFaceAnalysis);
Assert.IsFalse(context.HasInstallationReference);
Assert.AreEqual(0.25, context.AnchorVerticalOffsetInMeters, 1e-9);
Assert.AreEqual(12.0, context.ReferenceRodLengthInMeters, 1e-9);
Assert.AreEqual(0.4, context.ReferenceRodDiameterInMeters, 1e-9);
Assert.AreEqual(10.0, context.SphereCenterX, 1e-9);
Assert.AreEqual(11.0, context.SphereCenterY, 1e-9);
Assert.AreEqual(12.0, context.SphereCenterZ, 1e-9);
Assert.AreEqual(RailMountMode.OverRail, context.MountMode);
}
private static RailAssemblyWorkflowContext CreateContext()
{
return new RailAssemblyWorkflowContext(
defaultReferenceRodLengthInMeters: 20.0,
defaultReferenceRodDiameterInMeters: 0.3,
defaultAnchorVerticalOffsetInMeters: 0.25,
defaultSphereCenterX: 0.0,
defaultSphereCenterY: 0.0,
defaultSphereCenterZ: 0.0);
}
}
}

View File

@ -0,0 +1,36 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class RailPathPoseHelperTests
{
[TestMethod]
public void NormalizePreferredNormalToHostUpHemisphere_ShouldFlip_WhenPreferredNormalOpposesHostUp()
{
Vector3 hostPreferredNormal = new Vector3(-0.1222f, -0.9676f, 0.2211f);
Vector3 normalized = RailPathPoseHelper.NormalizePreferredNormalToHostUpHemisphere(
hostPreferredNormal,
Vector3.UnitY);
Assert.IsTrue(Vector3.Dot(normalized, Vector3.UnitY) > 0f);
Assert.AreEqual(0.1222 / 1.0008249, normalized.X, 1e-4);
Assert.AreEqual(0.9676 / 1.0008249, normalized.Y, 1e-4);
Assert.AreEqual(-0.2211 / 1.0008249, normalized.Z, 1e-4);
}
[TestMethod]
public void NormalizePreferredNormalToHostUpHemisphere_ShouldKeepDirection_WhenPreferredNormalAlreadyMatchesHostUp()
{
Vector3 hostPreferredNormal = new Vector3(0.139f, 0.954f, 0.266f);
Vector3 normalized = RailPathPoseHelper.NormalizePreferredNormalToHostUpHemisphere(
hostPreferredNormal,
Vector3.UnitY);
Assert.IsTrue(Vector3.Dot(normalized, Vector3.UnitY) > 0f);
Assert.IsTrue(Vector3.Dot(Vector3.Normalize(hostPreferredNormal), normalized) > 0.9999f);
}
}
}

View File

@ -0,0 +1,290 @@
using Autodesk.Navisworks.Api;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Core.Animation;
using NavisworksTransport.Utils.CoordinateSystem;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class RailPreservedPoseTests
{
[TestMethod]
public void ResolvePreservedPoseRotation_ShouldPreferActualGeometryRotationForRealObjects()
{
Rotation3D fallbackRotation = new Rotation3D(0.0, 0.0, 0.0, 1.0);
Rotation3D actualGeometryRotation = new Rotation3D(0.0, 0.70710678, 0.0, 0.70710678);
Rotation3D resolved = PathAnimationManager.ResolvePreservedPoseRotation(
preferActualGeometryRotation: true,
hasActualGeometryRotation: true,
actualGeometryRotation: actualGeometryRotation,
fallbackRotation: fallbackRotation);
Assert.AreEqual(actualGeometryRotation.A, resolved.A, 1e-9);
Assert.AreEqual(actualGeometryRotation.B, resolved.B, 1e-9);
Assert.AreEqual(actualGeometryRotation.C, resolved.C, 1e-9);
Assert.AreEqual(actualGeometryRotation.D, resolved.D, 1e-9);
}
[TestMethod]
public void ResolvePreservedPoseRotation_ShouldFallbackWhenActualGeometryRotationUnavailable()
{
Rotation3D fallbackRotation = new Rotation3D(0.0, 0.0, 0.38268343, 0.92387953);
Rotation3D actualGeometryRotation = new Rotation3D(0.0, 0.70710678, 0.0, 0.70710678);
Rotation3D resolved = PathAnimationManager.ResolvePreservedPoseRotation(
preferActualGeometryRotation: true,
hasActualGeometryRotation: false,
actualGeometryRotation: actualGeometryRotation,
fallbackRotation: fallbackRotation);
Assert.AreEqual(fallbackRotation.A, resolved.A, 1e-9);
Assert.AreEqual(fallbackRotation.B, resolved.B, 1e-9);
Assert.AreEqual(fallbackRotation.C, resolved.C, 1e-9);
Assert.AreEqual(fallbackRotation.D, resolved.D, 1e-9);
}
[TestMethod]
public void ShouldPreservePathRotationForFrames_ShouldEnableHoistingWhenTranslationModeHasLockedRotation()
{
bool shouldPreserve = PathAnimationManager.ShouldPreservePathRotationForFrames(
PathType.Hoisting,
ObjectStartPlacementMode.PreserveInitialPose,
hasPreservedRotation: true);
Assert.IsTrue(shouldPreserve);
}
[TestMethod]
public void ShouldPreservePathRotationForFrames_ShouldEnableRailWhenTranslationModeHasLockedRotation()
{
bool shouldPreserve = PathAnimationManager.ShouldPreservePathRotationForFrames(
PathType.Rail,
ObjectStartPlacementMode.PreserveInitialPose,
hasPreservedRotation: true);
Assert.IsTrue(shouldPreserve);
}
[TestMethod]
public void ShouldPreservePathRotationForFrames_ShouldDisableGroundEvenInTranslationMode()
{
bool shouldPreserve = PathAnimationManager.ShouldPreservePathRotationForFrames(
PathType.Ground,
ObjectStartPlacementMode.PreserveInitialPose,
hasPreservedRotation: true);
Assert.IsFalse(shouldPreserve);
}
[TestMethod]
public void ShouldPreservePathRotationForFrames_ShouldDisableWhenLockedRotationMissing()
{
bool shouldPreserve = PathAnimationManager.ShouldPreservePathRotationForFrames(
PathType.Hoisting,
ObjectStartPlacementMode.PreserveInitialPose,
hasPreservedRotation: false);
Assert.IsFalse(shouldPreserve);
}
[TestMethod]
public void ShouldAllowFragmentPlanarFallback_ShouldDisableHoisting()
{
Assert.IsFalse(PathAnimationManager.ShouldAllowFragmentPlanarFallback(PathType.Hoisting));
Assert.IsFalse(PathAnimationManager.ShouldAllowFragmentPlanarFallback(PathType.Ground));
Assert.IsTrue(PathAnimationManager.ShouldAllowFragmentPlanarFallback(PathType.Rail));
}
[TestMethod]
public void ShouldUseReferenceBasedRealObjectPlanarPose_ShouldDisableGroundAndHoisting()
{
Assert.IsFalse(PathAnimationManager.ShouldUseReferenceBasedRealObjectPlanarPose(PathType.Ground));
Assert.IsFalse(PathAnimationManager.ShouldUseReferenceBasedRealObjectPlanarPose(PathType.Hoisting));
Assert.IsTrue(PathAnimationManager.ShouldUseReferenceBasedRealObjectPlanarPose(PathType.Rail));
}
[TestMethod]
public void TryResolveDisplayedPlanarYawFromRotation_ShouldResolveYUpFromDisplayedXAxis()
{
Rotation3D rotation = new Rotation3D(
0.0,
System.Math.Sin(-System.Math.PI / 4.0),
0.0,
System.Math.Cos(-System.Math.PI / 4.0));
bool ok = PathAnimationManager.TryResolveDisplayedPlanarYawFromRotation(
rotation,
CoordinateSystemType.YUp,
out double yawRadians);
Assert.IsTrue(ok);
Assert.AreEqual(-System.Math.PI / 2.0, yawRadians, 1e-6);
}
[TestMethod]
public void ShouldUsePlanarRealObjectPureIncrementFrames_ShouldEnableOnlyForPlanarRealObjects()
{
Assert.IsTrue(PathAnimationManager.ShouldUsePlanarRealObjectPureIncrementFrames(PathType.Ground, true));
Assert.IsTrue(PathAnimationManager.ShouldUsePlanarRealObjectPureIncrementFrames(PathType.Hoisting, true));
Assert.IsFalse(PathAnimationManager.ShouldUsePlanarRealObjectPureIncrementFrames(PathType.Ground, false));
Assert.IsFalse(PathAnimationManager.ShouldUsePlanarRealObjectPureIncrementFrames(PathType.Rail, true));
}
[TestMethod]
public void ResolveAnimationFramePlaybackYawRadians_ShouldPreferPlanarHostYawWithHostCorrection()
{
var frame = new AnimationFrame
{
YawRadians = 0.25,
PlanarHostYawRadians = 0.75,
HasCustomRotation = true
};
double resolved = PathAnimationManager.ResolveAnimationFramePlaybackYawRadians(
frame,
new LocalEulerRotationCorrection(10.0, 20.0, 30.0),
CoordinateSystemType.YUp);
Assert.AreEqual(0.75 + 20.0 * System.Math.PI / 180.0, resolved, 1e-9);
}
[TestMethod]
public void ApplyRecordedPlanarRealObjectFramePose_ShouldPreserveFullRotation_ForGroundCollisionLogCase()
{
var frame = new AnimationFrame();
double yawRadians = 115.85 * System.Math.PI / 180.0;
var hostLinear = new Matrix4x4(
-0.4360f, 0.0000f, -0.8999f, 0.0f,
-0.6364f, 0.7071f, 0.3083f, 0.0f,
0.6364f, 0.7071f, -0.3083f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f);
Quaternion hostQuaternion = Quaternion.Normalize(Quaternion.CreateFromRotationMatrix(hostLinear));
var rotation = new Rotation3D(hostQuaternion.X, hostQuaternion.Y, hostQuaternion.Z, hostQuaternion.W);
PathAnimationManager.ApplyRecordedPlanarRealObjectFramePose(frame, yawRadians, rotation);
Assert.IsTrue(frame.PlanarHostYawRadians.HasValue);
Assert.AreEqual(yawRadians, frame.PlanarHostYawRadians.Value, 1e-9);
Assert.IsTrue(frame.HasCustomRotation);
Matrix3 linear = new Transform3D(frame.Rotation).Linear;
Assert.AreEqual(-0.4360, linear.Get(0, 0), 5e-4);
Assert.AreEqual(-0.6364, linear.Get(1, 0), 5e-4);
Assert.AreEqual(0.6364, linear.Get(2, 0), 5e-4);
Assert.AreEqual(0.0000, linear.Get(0, 1), 5e-4);
Assert.AreEqual(0.7071, linear.Get(1, 1), 5e-4);
Assert.AreEqual(0.7071, linear.Get(2, 1), 5e-4);
Assert.AreEqual(-0.8999, linear.Get(0, 2), 5e-4);
Assert.AreEqual(0.3083, linear.Get(1, 2), 5e-4);
Assert.AreEqual(-0.3083, linear.Get(2, 2), 5e-4);
}
[TestMethod]
public void TryResolveGroundHostPlanarYawFromFramePoints_ShouldResolveYUpHostYaw()
{
bool ok = PathAnimationManager.TryResolveGroundHostPlanarYawFromFramePoints(
new Point3D(0, 0, 0),
new Point3D(1, 0, 0),
new Point3D(1, 0, -1),
CoordinateSystemType.YUp,
out double yawRadians);
Assert.IsTrue(ok);
Assert.AreEqual(-System.Math.PI / 4.0, yawRadians, 1e-6);
}
[TestMethod]
public void ResolvePlanarHostUpCorrectionRadians_ShouldUseYDegreesForYUp()
{
double radians = PathAnimationManager.ResolvePlanarHostUpCorrectionRadians(
new LocalEulerRotationCorrection(10.0, 20.0, 30.0),
CoordinateSystemType.YUp);
Assert.AreEqual(20.0 * System.Math.PI / 180.0, radians, 1e-9);
}
[TestMethod]
public void ResolvePlanarHostUpCorrectionRadians_ShouldUseZDegreesForZUp()
{
double radians = PathAnimationManager.ResolvePlanarHostUpCorrectionRadians(
new LocalEulerRotationCorrection(10.0, 20.0, 30.0),
CoordinateSystemType.ZUp);
Assert.AreEqual(30.0 * System.Math.PI / 180.0, radians, 1e-9);
}
[TestMethod]
public void ResolveCurrentRotationBaseline_ShouldPreferActualGeometryForHoisting()
{
Rotation3D trackedRotation = new Rotation3D(0.0, 0.0, 0.0, 1.0);
Rotation3D actualGeometryRotation = new Rotation3D(0.0, 0.70710678, 0.0, 0.70710678);
Rotation3D resolved = PathAnimationManager.ResolveCurrentRotationBaseline(
PathType.Hoisting,
isRealObjectMode: true,
hasTrackedRotation: true,
trackedRotation: trackedRotation,
hasReferenceRotation: false,
referenceRotation: Rotation3D.Identity,
transformRotation: Rotation3D.Identity,
hasActualGeometryRotation: true,
actualGeometryRotation: actualGeometryRotation);
Assert.AreEqual(actualGeometryRotation.A, resolved.A, 1e-9);
Assert.AreEqual(actualGeometryRotation.B, resolved.B, 1e-9);
Assert.AreEqual(actualGeometryRotation.C, resolved.C, 1e-9);
Assert.AreEqual(actualGeometryRotation.D, resolved.D, 1e-9);
}
[TestMethod]
public void ResolveCurrentRotationBaseline_ShouldKeepTrackedRotationForGround()
{
Rotation3D trackedRotation = new Rotation3D(0.0, 0.0, 0.38268343, 0.92387953);
Rotation3D actualGeometryRotation = new Rotation3D(0.0, 0.70710678, 0.0, 0.70710678);
Rotation3D resolved = PathAnimationManager.ResolveCurrentRotationBaseline(
PathType.Ground,
isRealObjectMode: true,
hasTrackedRotation: true,
trackedRotation: trackedRotation,
hasReferenceRotation: false,
referenceRotation: Rotation3D.Identity,
transformRotation: Rotation3D.Identity,
hasActualGeometryRotation: true,
actualGeometryRotation: actualGeometryRotation);
Assert.AreEqual(trackedRotation.A, resolved.A, 1e-9);
Assert.AreEqual(trackedRotation.B, resolved.B, 1e-9);
Assert.AreEqual(trackedRotation.C, resolved.C, 1e-9);
Assert.AreEqual(trackedRotation.D, resolved.D, 1e-9);
}
[TestMethod]
public void ShouldUseRealObjectOverrideRotation_ShouldEnableForRealObjects()
{
Assert.IsTrue(PathAnimationManager.ShouldUseRealObjectOverrideRotation(true));
Assert.IsFalse(PathAnimationManager.ShouldUseRealObjectOverrideRotation(false));
}
[TestMethod]
public void ShouldUseOriginalBoundingBoxCenterForBusinessTrackedPosition_ShouldEnableOnlyForGroundRealObjects()
{
Assert.IsTrue(PathAnimationManager.ShouldUseOriginalBoundingBoxCenterForBusinessTrackedPosition(
PathType.Ground,
isRealObjectMode: true));
Assert.IsFalse(PathAnimationManager.ShouldUseOriginalBoundingBoxCenterForBusinessTrackedPosition(
PathType.Ground,
isRealObjectMode: false));
Assert.IsFalse(PathAnimationManager.ShouldUseOriginalBoundingBoxCenterForBusinessTrackedPosition(
PathType.Hoisting,
isRealObjectMode: true));
Assert.IsFalse(PathAnimationManager.ShouldUseOriginalBoundingBoxCenterForBusinessTrackedPosition(
PathType.Rail,
isRealObjectMode: true));
}
}
}

View File

@ -0,0 +1,162 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
using System;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class RealObjectPlanarPoseSolverTests
{
[TestMethod]
public void ShouldChooseClosestCandidateAxis_FromRotatedReferencePose()
{
Quaternion referenceRotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, (float)(Math.PI / 6.0));
Vector3 desiredForward = new Vector3(1.0f, 0.2f, 0.1f);
Vector3 desiredUp = Vector3.UnitZ;
bool ok = RealObjectPlanarPoseSolver.TryCreatePlanarPoseFromReferencePose(
referenceRotation,
desiredForward,
desiredUp,
out RealObjectPlanarPoseSolution solution);
Assert.IsTrue(ok);
AssertVector(solution.SelectedReferenceAxisLocal, 1.0, 0.0, 0.0);
Vector3 transformedSelectedAxis = Vector3.Normalize(Vector3.Transform(solution.SelectedReferenceAxisLocal, solution.BaselineRotation));
AssertVector(transformedSelectedAxis, desiredForward.X / desiredForward.Length(), desiredForward.Y / desiredForward.Length(), desiredForward.Z / desiredForward.Length(), 1e-4);
}
[TestMethod]
public void ShouldPreferNegativeAxisWhenItMatchesForwardBest()
{
bool ok = RealObjectPlanarPoseSolver.TryCreatePlanarPoseFromReferencePose(
Quaternion.Identity,
new Vector3(-0.1f, -1.0f, 0.05f),
Vector3.UnitZ,
out RealObjectPlanarPoseSolution solution);
Assert.IsTrue(ok);
AssertVector(solution.SelectedReferenceAxisLocal, 0.0, -1.0, 0.0);
}
[TestMethod]
public void ShouldRespectDesiredUpPlane_WhenDesiredForwardHasVerticalComponent()
{
Vector3 desiredForward = Vector3.Normalize(new Vector3(1.0f, 1.0f, 1.0f));
bool ok = RealObjectPlanarPoseSolver.TryCreatePlanarPoseFromReferencePose(
Quaternion.Identity,
desiredForward,
Vector3.UnitY,
out RealObjectPlanarPoseSolution solution);
Assert.IsTrue(ok);
Vector3 transformedSelectedAxis = Vector3.Normalize(Vector3.Transform(solution.SelectedReferenceAxisLocal, solution.BaselineRotation));
AssertVector(transformedSelectedAxis, desiredForward.X, desiredForward.Y, desiredForward.Z, 1e-4);
}
[TestMethod]
public void ShouldPreserveReferenceOrthogonalityByApplyingSingleRotationDelta()
{
Quaternion referenceRotation = Quaternion.Normalize(
Quaternion.CreateFromYawPitchRoll(
(float)(Math.PI / 7.0),
(float)(Math.PI / 9.0),
(float)(Math.PI / 11.0)));
Vector3 desiredForward = Vector3.Normalize(new Vector3(-0.3f, 0.8f, 0.52f));
bool ok = RealObjectPlanarPoseSolver.TryCreatePlanarPoseFromReferencePose(
referenceRotation,
desiredForward,
Vector3.UnitZ,
out RealObjectPlanarPoseSolution solution);
Assert.IsTrue(ok);
Vector3 transformedX = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, solution.BaselineRotation));
Vector3 transformedY = Vector3.Normalize(Vector3.Transform(Vector3.UnitY, solution.BaselineRotation));
Vector3 transformedZ = Vector3.Normalize(Vector3.Transform(Vector3.UnitZ, solution.BaselineRotation));
Assert.AreEqual(1.0, transformedX.Length(), 1e-4);
Assert.AreEqual(1.0, transformedY.Length(), 1e-4);
Assert.AreEqual(1.0, transformedZ.Length(), 1e-4);
Assert.AreEqual(0.0, Vector3.Dot(transformedX, transformedY), 1e-4);
Assert.AreEqual(0.0, Vector3.Dot(transformedY, transformedZ), 1e-4);
Assert.AreEqual(0.0, Vector3.Dot(transformedZ, transformedX), 1e-4);
}
[TestMethod]
public void FixedReferenceAxis_ShouldKeepAxisFamilyAcrossTurn()
{
Quaternion referenceRotation = Quaternion.CreateFromAxisAngle(Vector3.UnitY, (float)Math.PI);
Vector3 desiredForwardStart = Vector3.Normalize(new Vector3(-0.95f, 0.0f, 0.31f));
Vector3 desiredForwardTurn = Vector3.Normalize(new Vector3(0.22f, 0.0f, 0.97f));
Vector3 desiredUp = Vector3.UnitY;
bool startOk = RealObjectPlanarPoseSolver.TryCreatePlanarPoseFromReferencePose(
referenceRotation,
desiredForwardStart,
desiredUp,
out RealObjectPlanarPoseSolution startSolution);
Assert.IsTrue(startOk);
bool turnOk = RealObjectPlanarPoseSolver.TryCreatePlanarPoseFromReferencePose(
referenceRotation,
desiredForwardTurn,
desiredUp,
startSolution.SelectedReferenceAxisDirection,
out RealObjectPlanarPoseSolution turnSolution);
Assert.IsTrue(turnOk);
Assert.AreEqual(startSolution.SelectedReferenceAxisDirection, turnSolution.SelectedReferenceAxisDirection);
Vector3 transformedSelectedAxis = Vector3.Normalize(
Vector3.Transform(turnSolution.SelectedReferenceAxisLocal, turnSolution.BaselineRotation));
AssertVector(
transformedSelectedAxis,
desiredForwardTurn.X,
desiredForwardTurn.Y,
desiredForwardTurn.Z,
1e-4);
}
[TestMethod]
public void FixedPositiveX_ShouldNotSwitchToZ_WhenPathLeansTowardZ()
{
Quaternion referenceRotation = Quaternion.Identity;
Vector3 desiredForward = Vector3.Normalize(new Vector3(-0.690f, 0.0f, 0.724f));
Vector3 desiredUp = Vector3.UnitY;
bool ok = RealObjectPlanarPoseSolver.TryCreatePlanarPoseFromReferencePose(
referenceRotation,
desiredForward,
desiredUp,
LocalAxisDirection.PositiveX,
out RealObjectPlanarPoseSolution solution);
Assert.IsTrue(ok);
Assert.AreEqual(LocalAxisDirection.PositiveX, solution.SelectedReferenceAxisDirection);
AssertVector(solution.SelectedReferenceAxisLocal, 1.0, 0.0, 0.0);
Vector3 transformedSelectedAxis = Vector3.Normalize(
Vector3.Transform(solution.SelectedReferenceAxisLocal, solution.BaselineRotation));
AssertVector(
transformedSelectedAxis,
desiredForward.X,
desiredForward.Y,
desiredForward.Z,
1e-4);
}
private static void AssertVector(Vector3 vector, double x, double y, double z, double tolerance = 1e-6)
{
Assert.AreEqual(x, vector.X, tolerance);
Assert.AreEqual(y, vector.Y, tolerance);
Assert.AreEqual(z, vector.Z, tolerance);
}
}
}

View File

@ -0,0 +1,145 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class RealObjectProjectedExtentResolverTests
{
[TestMethod]
public void Ground_DualAxisCorrection_ShouldProjectAgainstFinalPose()
{
var convention = new ModelAxisConvention(LocalAxisDirection.PositiveX, LocalAxisDirection.PositiveY);
Vector3 targetForward = Vector3.Normalize(new Vector3(-0.8987f, 0.0f, 0.4386f));
Vector3 targetUp = Vector3.UnitY;
Quaternion baseline = Quaternion.Normalize(Quaternion.CreateFromRotationMatrix(new Matrix4x4(
-0.8987f, 0.0000f, 0.4386f, 0f,
0.0000f, 1.0000f, 0.0000f, 0f,
-0.4386f, 0.0000f,-0.8987f, 0f,
0f, 0f, 0f, 1f)));
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
Quaternion final = adapter.ComposeHostQuaternion(
baseline,
new LocalEulerRotationCorrection(90.0, 0.0, 90.0));
var extents = RealObjectProjectedExtentResolver.CalculateProjectedSemanticExtents(
convention,
forwardSize: 6.0,
sideSize: 2.0,
upSize: 4.0,
baseline,
final,
targetForward,
targetUp);
Vector3 localSize = convention.CreateScaleVector3(6.0, 2.0, 4.0);
Vector3 rotatedLocalX = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, final));
Vector3 rotatedLocalY = Vector3.Normalize(Vector3.Transform(Vector3.UnitY, final));
Vector3 rotatedLocalZ = Vector3.Normalize(Vector3.Transform(Vector3.UnitZ, final));
Vector3 targetSide = Vector3.Normalize(Vector3.Cross(targetForward, targetUp));
double expectedForward = ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetForward);
double expectedSide = ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetSide);
double expectedUp = ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetUp);
Assert.AreEqual(expectedForward, extents.forwardExtent, 1e-5);
Assert.AreEqual(expectedSide, extents.sideExtent, 1e-5);
Assert.AreEqual(expectedUp, extents.upExtent, 1e-5);
Assert.AreNotEqual(4.0, extents.upExtent, 1e-3, "双轴旋转后Ground 的法线尺寸不应停留在旧高度。");
}
[TestMethod]
public void Ground_DisplayedHostAxesProjection_ShouldKeepXAxisLengthAndSwapXZAsAxesRotate()
{
Vector3 targetForward = Vector3.UnitX;
Vector3 targetUp = Vector3.UnitY;
var noCorrection = RealObjectProjectedExtentResolver.CalculateProjectedDisplayedExtents(
displayedAxisXSize: 6.0,
displayedAxisYSize: 2.0,
displayedAxisZSize: 4.0,
displayedHostAxisX: Vector3.UnitX,
displayedHostAxisY: Vector3.UnitZ,
displayedHostAxisZ: -Vector3.UnitY,
targetForward,
targetUp);
Assert.AreEqual(6.0, noCorrection.forwardExtent, 1e-5);
Assert.AreEqual(2.0, noCorrection.sideExtent, 1e-5);
Assert.AreEqual(4.0, noCorrection.upExtent, 1e-5);
var xRotated = RealObjectProjectedExtentResolver.CalculateProjectedDisplayedExtents(
displayedAxisXSize: 6.0,
displayedAxisYSize: 2.0,
displayedAxisZSize: 4.0,
displayedHostAxisX: Vector3.UnitX,
displayedHostAxisY: Vector3.UnitY,
displayedHostAxisZ: Vector3.UnitZ,
targetForward,
targetUp);
Assert.AreEqual(6.0, xRotated.forwardExtent, 1e-5);
Assert.AreEqual(4.0, xRotated.sideExtent, 1e-5);
Assert.AreEqual(2.0, xRotated.upExtent, 1e-5);
var zRotated = RealObjectProjectedExtentResolver.CalculateProjectedDisplayedExtents(
displayedAxisXSize: 6.0,
displayedAxisYSize: 2.0,
displayedAxisZSize: 4.0,
displayedHostAxisX: -Vector3.UnitZ,
displayedHostAxisY: Vector3.UnitX,
displayedHostAxisZ: -Vector3.UnitY,
targetForward,
targetUp);
Assert.AreEqual(2.0, zRotated.forwardExtent, 1e-5);
Assert.AreEqual(6.0, zRotated.sideExtent, 1e-5);
Assert.AreEqual(4.0, zRotated.upExtent, 1e-5);
}
[TestMethod]
public void Ground_DisplayedHostAxesProjection_ShouldIgnoreVerticalComponentInForward()
{
Vector3 targetUp = Vector3.UnitY;
Vector3 planarForward = Vector3.Normalize(new Vector3(1f, 0f, 1f));
Vector3 slopedForward = Vector3.Normalize(new Vector3(1f, 2f, 1f));
var planar = RealObjectProjectedExtentResolver.CalculateProjectedDisplayedExtents(
displayedAxisXSize: 6.0,
displayedAxisYSize: 2.0,
displayedAxisZSize: 4.0,
displayedHostAxisX: Vector3.UnitX,
displayedHostAxisY: Vector3.UnitZ,
displayedHostAxisZ: -Vector3.UnitY,
planarForward,
targetUp);
var sloped = RealObjectProjectedExtentResolver.CalculateProjectedDisplayedExtents(
displayedAxisXSize: 6.0,
displayedAxisYSize: 2.0,
displayedAxisZSize: 4.0,
displayedHostAxisX: Vector3.UnitX,
displayedHostAxisY: Vector3.UnitZ,
displayedHostAxisZ: -Vector3.UnitY,
slopedForward,
targetUp);
Assert.AreNotEqual(planar.forwardExtent, sloped.forwardExtent, 1e-3, "原始带竖直分量的路径方向会污染 Ground 尺寸投影。");
}
private static double ProjectExtent(
Vector3 localSize,
Vector3 rotatedLocalX,
Vector3 rotatedLocalY,
Vector3 rotatedLocalZ,
Vector3 targetAxis)
{
return System.Math.Abs(Vector3.Dot(rotatedLocalX, targetAxis)) * localSize.X +
System.Math.Abs(Vector3.Dot(rotatedLocalY, targetAxis)) * localSize.Y +
System.Math.Abs(Vector3.Dot(rotatedLocalZ, targetAxis)) * localSize.Z;
}
}
}

View File

@ -0,0 +1,166 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class RealObjectRailAxisConventionResolverTests
{
[TestMethod]
public void YUp_InterpretedReferencePose_ShouldChoosePositiveXAndPositiveYForRail()
{
Vector3 referenceAxisX = new Vector3(-1.0f, 0.0f, 0.0f);
Vector3 referenceAxisY = new Vector3(0.0f, 1.0f, 0.0f);
Vector3 referenceAxisZ = new Vector3(0.0f, 0.0f, -1.0f);
Vector3 desiredForward = Vector3.Normalize(new Vector3(-0.8987f, 0.0f, 0.4386f));
Vector3 desiredUp = Vector3.UnitY;
bool ok = RealObjectRailAxisConventionResolver.TryResolve(
referenceAxisX,
referenceAxisY,
referenceAxisZ,
LocalAxisDirection.PositiveY,
desiredForward,
CoordinateSystemType.YUp,
out ModelAxisConvention convention,
out LocalAxisDirection selectedForwardAxis,
out Vector3 selectedForwardWorldAxis,
out LocalAxisDirection selectedUpAxis,
out Vector3 selectedUpWorldAxis);
Assert.IsTrue(ok);
Assert.AreEqual(LocalAxisDirection.PositiveX, selectedForwardAxis);
Assert.AreEqual(LocalAxisDirection.PositiveX, convention.ForwardAxis);
Assert.AreEqual(LocalAxisDirection.PositiveY, convention.UpAxis);
Assert.AreEqual(LocalAxisDirection.PositiveY, selectedUpAxis);
AssertVector(selectedForwardWorldAxis, -1.0, 0.0, 0.0);
AssertVector(selectedUpWorldAxis, 0.0, 1.0, 0.0);
}
[TestMethod]
public void ZUp_InterpretedReferencePose_ShouldChoosePositiveXAndPositiveZForRail()
{
Vector3 referenceAxisX = new Vector3(-1.0f, 0.0f, 0.0f);
Vector3 referenceAxisY = new Vector3(0.0f, 1.0f, 0.0f);
Vector3 referenceAxisZ = new Vector3(0.0f, 0.0f, 1.0f);
Vector3 desiredForward = Vector3.Normalize(new Vector3(-0.9f, 0.3f, 0.0f));
Vector3 desiredUp = Vector3.UnitZ;
bool ok = RealObjectRailAxisConventionResolver.TryResolve(
referenceAxisX,
referenceAxisY,
referenceAxisZ,
LocalAxisDirection.PositiveZ,
desiredForward,
CoordinateSystemType.ZUp,
out ModelAxisConvention convention,
out LocalAxisDirection selectedForwardAxis,
out _,
out LocalAxisDirection selectedUpAxis,
out Vector3 selectedUpWorldAxis);
Assert.IsTrue(ok);
Assert.AreEqual(LocalAxisDirection.PositiveX, selectedForwardAxis);
Assert.AreEqual(LocalAxisDirection.PositiveX, convention.ForwardAxis);
Assert.AreEqual(LocalAxisDirection.PositiveZ, convention.UpAxis);
Assert.AreEqual(LocalAxisDirection.PositiveZ, selectedUpAxis);
AssertVector(selectedUpWorldAxis, 0.0, 0.0, 1.0);
}
[TestMethod]
public void YUp_ShouldNotSelectYAxisFamilyAsForwardCandidate()
{
Vector3 referenceAxisX = new Vector3(1.0f, 0.0f, 0.0f);
Vector3 referenceAxisY = new Vector3(0.0f, 1.0f, 0.0f);
Vector3 referenceAxisZ = new Vector3(0.0f, 0.0f, 1.0f);
Vector3 desiredForward = Vector3.Normalize(new Vector3(0.0f, 1.0f, 0.01f));
Vector3 desiredUp = Vector3.UnitY;
bool ok = RealObjectRailAxisConventionResolver.TryResolve(
referenceAxisX,
referenceAxisY,
referenceAxisZ,
LocalAxisDirection.PositiveY,
desiredForward,
CoordinateSystemType.YUp,
out _,
out LocalAxisDirection selectedForwardAxis,
out _,
out _,
out _);
Assert.IsTrue(ok);
Assert.AreNotEqual(LocalAxisDirection.PositiveY, selectedForwardAxis);
Assert.AreNotEqual(LocalAxisDirection.NegativeY, selectedForwardAxis);
}
[TestMethod]
public void YUp_WhenRemainingZAxisBestMatchesDesiredUp_ShouldChooseZAsUp()
{
Vector3 referenceAxisX = new Vector3(-1.0f, 0.0f, 0.0f);
Vector3 referenceAxisY = new Vector3(0.0f, 0.0f, 1.0f);
Vector3 referenceAxisZ = new Vector3(0.0f, 1.0f, 0.0f);
Vector3 desiredForward = new Vector3(-1.0f, 0.0f, 0.0f);
Vector3 desiredUp = new Vector3(0.0f, 1.0f, 0.0f);
bool ok = RealObjectRailAxisConventionResolver.TryResolve(
referenceAxisX,
referenceAxisY,
referenceAxisZ,
LocalAxisDirection.PositiveZ,
desiredForward,
CoordinateSystemType.YUp,
out ModelAxisConvention convention,
out LocalAxisDirection selectedForwardAxis,
out Vector3 selectedForwardWorldAxis,
out LocalAxisDirection selectedUpAxis,
out Vector3 selectedUpWorldAxis);
Assert.IsTrue(ok);
Assert.AreEqual(LocalAxisDirection.PositiveX, selectedForwardAxis);
Assert.AreEqual(LocalAxisDirection.PositiveZ, selectedUpAxis);
Assert.AreEqual(LocalAxisDirection.PositiveX, convention.ForwardAxis);
Assert.AreEqual(LocalAxisDirection.PositiveZ, convention.UpAxis);
AssertVector(selectedForwardWorldAxis, -1.0, 0.0, 0.0);
AssertVector(selectedUpWorldAxis, 0.0, 1.0, 0.0);
}
[TestMethod]
public void YUp_ShouldUseMappedHostUpLocalAxis_AndSelectForwardFromRemainingAxes()
{
Vector3 referenceAxisX = new Vector3(0.0f, 0.0f, 1.0f);
Vector3 referenceAxisY = new Vector3(0.0f, 1.0f, 0.0f);
Vector3 referenceAxisZ = new Vector3(-1.0f, 0.0f, 0.0f);
Vector3 desiredForward = Vector3.Normalize(new Vector3(0.99f, -0.14f, -0.02f));
bool ok = RealObjectRailAxisConventionResolver.TryResolve(
referenceAxisX,
referenceAxisY,
referenceAxisZ,
LocalAxisDirection.PositiveZ,
desiredForward,
CoordinateSystemType.YUp,
out ModelAxisConvention convention,
out LocalAxisDirection selectedForwardAxis,
out Vector3 selectedForwardWorldAxis,
out LocalAxisDirection selectedUpAxis,
out Vector3 selectedUpWorldAxis);
Assert.IsTrue(ok);
Assert.AreEqual(LocalAxisDirection.PositiveZ, selectedUpAxis);
Assert.AreEqual(LocalAxisDirection.PositiveZ, convention.UpAxis);
Assert.AreEqual(LocalAxisDirection.NegativeY, selectedForwardAxis);
Assert.AreEqual(LocalAxisDirection.NegativeY, convention.ForwardAxis);
AssertVector(selectedUpWorldAxis, -1.0, 0.0, 0.0);
AssertVector(selectedForwardWorldAxis, 0.0, -1.0, 0.0);
}
private static void AssertVector(Vector3 actual, double x, double y, double z, double tolerance = 1e-6)
{
Assert.AreEqual(x, actual.X, tolerance);
Assert.AreEqual(y, actual.Y, tolerance);
Assert.AreEqual(z, actual.Z, tolerance);
}
}
}

View File

@ -0,0 +1,145 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
using System;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class RealObjectRailExtentResolverTests
{
[TestMethod]
public void YUp_ZeroCorrection_ShouldKeepResolvedRailUpExtent()
{
Vector3 referenceAxisX = new Vector3(-1.0f, 0.0f, 0.0f);
Vector3 referenceAxisY = new Vector3(0.0f, 1.0f, 0.0f);
Vector3 referenceAxisZ = new Vector3(0.0f, 0.0f, -1.0f);
Vector3 desiredForward = Vector3.Normalize(new Vector3(-0.8987f, 0.0f, 0.4386f));
Quaternion baseline = Quaternion.Identity;
bool ok = RealObjectRailExtentResolver.TryResolveProjectedSemanticExtents(
referenceAxisX,
referenceAxisY,
referenceAxisZ,
LocalAxisDirection.PositiveY,
desiredForward,
CoordinateSystemType.YUp,
6.0,
2.0,
4.0,
baseline,
baseline,
out ModelAxisConvention convention,
out var extents);
Assert.IsTrue(ok);
Assert.AreEqual(LocalAxisDirection.PositiveX, convention.ForwardAxis);
Assert.AreEqual(LocalAxisDirection.PositiveY, convention.UpAxis);
Assert.AreEqual(6.0, extents.forwardExtent, 1e-6);
Assert.AreEqual(2.0, extents.sideExtent, 1e-6);
Assert.AreEqual(4.0, extents.upExtent, 1e-6);
}
[TestMethod]
public void YUp_HostY90_ShouldKeepResolvedRailUpExtentAndSwapForwardSide()
{
Vector3 referenceAxisX = new Vector3(-1.0f, 0.0f, 0.0f);
Vector3 referenceAxisY = new Vector3(0.0f, 1.0f, 0.0f);
Vector3 referenceAxisZ = new Vector3(0.0f, 0.0f, -1.0f);
Vector3 desiredForward = Vector3.Normalize(new Vector3(-0.8987f, 0.0f, 0.4386f));
Quaternion baseline = Quaternion.Identity;
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
Quaternion final = adapter.ComposeHostQuaternion(
baseline,
new LocalEulerRotationCorrection(0.0, 90.0, 0.0));
bool ok = RealObjectRailExtentResolver.TryResolveProjectedSemanticExtents(
referenceAxisX,
referenceAxisY,
referenceAxisZ,
LocalAxisDirection.PositiveY,
desiredForward,
CoordinateSystemType.YUp,
6.0,
2.0,
4.0,
baseline,
final,
out _,
out var extents);
Assert.IsTrue(ok);
Assert.AreEqual(2.0, extents.forwardExtent, 1e-6);
Assert.AreEqual(6.0, extents.sideExtent, 1e-6);
Assert.AreEqual(4.0, extents.upExtent, 1e-6);
}
[TestMethod]
public void YUp_DualAxisCorrection_WithRailBaseline_ShouldProjectAgainstFinalRailPose()
{
Vector3 referenceAxisX = new Vector3(-1.0f, 0.0f, 0.0f);
Vector3 referenceAxisY = new Vector3(0.0f, 1.0f, 0.0f);
Vector3 referenceAxisZ = new Vector3(0.0f, 0.0f, -1.0f);
Vector3 desiredForward = Vector3.Normalize(new Vector3(-0.8987f, 0.0f, 0.4386f));
Quaternion baseline = Quaternion.Normalize(Quaternion.CreateFromRotationMatrix(new Matrix4x4(
0.9900f, -0.1403f, -0.0161f, 0f,
0.1403f, 0.9901f, -0.0023f, 0f,
0.0163f, 0.0000f, 0.9999f, 0f,
0f, 0f, 0f, 1f)));
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
Quaternion final = adapter.ComposeHostQuaternion(
baseline,
new LocalEulerRotationCorrection(0.0, 90.0, 90.0));
bool ok = RealObjectRailExtentResolver.TryResolveProjectedSemanticExtents(
referenceAxisX,
referenceAxisY,
referenceAxisZ,
LocalAxisDirection.PositiveY,
desiredForward,
CoordinateSystemType.YUp,
6.0,
2.0,
4.0,
baseline,
final,
out ModelAxisConvention convention,
out var extents);
Assert.IsTrue(ok);
Assert.AreEqual(LocalAxisDirection.PositiveX, convention.ForwardAxis);
Assert.AreEqual(LocalAxisDirection.PositiveY, convention.UpAxis);
Vector3 localSize = convention.CreateScaleVector3(6.0, 2.0, 4.0);
Vector3 rotatedLocalX = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, final));
Vector3 rotatedLocalY = Vector3.Normalize(Vector3.Transform(Vector3.UnitY, final));
Vector3 rotatedLocalZ = Vector3.Normalize(Vector3.Transform(Vector3.UnitZ, final));
Vector3 targetForward = Vector3.Normalize(Vector3.Transform(convention.ForwardUnitVector, baseline));
Vector3 targetUp = Vector3.Normalize(Vector3.Transform(convention.UpUnitVector, baseline));
Vector3 targetSide = Vector3.Normalize(Vector3.Cross(targetForward, targetUp));
double expectedForward = ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetForward);
double expectedSide = ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetSide);
double expectedUp = ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetUp);
Assert.AreEqual(expectedForward, extents.forwardExtent, 1e-5);
Assert.AreEqual(expectedSide, extents.sideExtent, 1e-5);
Assert.AreEqual(expectedUp, extents.upExtent, 1e-5);
Assert.AreNotEqual(4.0, extents.upExtent, 1e-3, "双轴旋转后,法向尺寸不应仍停留在单轴结果。");
}
private static double ProjectExtent(
Vector3 localSize,
Vector3 rotatedLocalX,
Vector3 rotatedLocalY,
Vector3 rotatedLocalZ,
Vector3 targetAxis)
{
return Math.Abs(Vector3.Dot(rotatedLocalX, targetAxis)) * localSize.X +
Math.Abs(Vector3.Dot(rotatedLocalY, targetAxis)) * localSize.Y +
Math.Abs(Vector3.Dot(rotatedLocalZ, targetAxis)) * localSize.Z;
}
}
}

View File

@ -0,0 +1,160 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
using System.Collections.Generic;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class RealObjectReferencePoseResolverTests
{
[TestMethod]
public void YUp_HostWithFragmentDefaultZ_ShouldInterpretRepresentativeFrameToHostSemantics()
{
var fragmentMatrices = new List<double[]>
{
CreateMatrix(
axisX: new Vector3(1.0f, 0.0f, 0.0f),
axisY: new Vector3(0.0f, 1.0f, 0.0f),
axisZ: new Vector3(0.0f, 0.0f, 1.0f))
};
bool ok = RealObjectReferencePoseResolver.TryResolveFromFragmentMatrices(
fragmentMatrices,
fragmentDefaultUpAxis: "Z",
hostUpAxis: "Y",
out RealObjectReferencePose pose);
Assert.IsTrue(ok);
AssertVector(pose.RawAxisX, 1.0, 0.0, 0.0);
AssertVector(pose.RawAxisY, 0.0, 1.0, 0.0);
AssertVector(pose.RawAxisZ, 0.0, 0.0, 1.0);
AssertVector(pose.AxisX, 1.0, 0.0, 0.0);
AssertVector(pose.AxisY, 0.0, 0.0, 1.0);
AssertVector(pose.AxisZ, 0.0, -1.0, 0.0);
Assert.AreEqual(LocalAxisDirection.PositiveX, pose.HostWorldXAxisLocalAxis);
Assert.AreEqual(LocalAxisDirection.PositiveY, pose.HostWorldYAxisLocalAxis);
Assert.AreEqual(LocalAxisDirection.PositiveZ, pose.HostWorldZAxisLocalAxis);
Assert.AreEqual(LocalAxisDirection.PositiveX, pose.HostSemanticXAxisLocalAxis);
Assert.AreEqual(LocalAxisDirection.PositiveZ, pose.HostSemanticYAxisLocalAxis);
Assert.AreEqual(LocalAxisDirection.NegativeY, pose.HostSemanticZAxisLocalAxis);
Assert.AreEqual(LocalAxisDirection.PositiveZ, pose.HostUpLocalAxis);
}
[TestMethod]
public void YUp_WorldAxisMapping_ShouldFollowRawRepresentativeFrame()
{
var fragmentMatrices = new List<double[]>
{
CreateMatrix(
axisX: new Vector3(0.0f, 0.0f, 1.0f),
axisY: new Vector3(1.0f, 0.0f, 0.0f),
axisZ: new Vector3(0.0f, 1.0f, 0.0f))
};
bool ok = RealObjectReferencePoseResolver.TryResolveFromFragmentMatrices(
fragmentMatrices,
fragmentDefaultUpAxis: "Z",
hostUpAxis: "Y",
out RealObjectReferencePose pose);
Assert.IsTrue(ok);
Assert.AreEqual(LocalAxisDirection.PositiveY, pose.HostWorldXAxisLocalAxis);
Assert.AreEqual(LocalAxisDirection.PositiveZ, pose.HostWorldYAxisLocalAxis);
Assert.AreEqual(LocalAxisDirection.PositiveX, pose.HostWorldZAxisLocalAxis);
Assert.AreEqual(LocalAxisDirection.PositiveX, pose.HostSemanticXAxisLocalAxis);
Assert.AreEqual(LocalAxisDirection.PositiveZ, pose.HostSemanticYAxisLocalAxis);
Assert.AreEqual(LocalAxisDirection.NegativeY, pose.HostSemanticZAxisLocalAxis);
}
[TestMethod]
public void DetectDefaultUpAxis_ShouldReturnZ_WhenRepresentativeZMatchesHostUp()
{
var fragmentMatrices = new List<double[]>
{
CreateMatrix(
axisX: new Vector3(1.0f, 0.0f, 0.0f),
axisY: new Vector3(0.0f, 0.0f, -1.0f),
axisZ: new Vector3(0.0f, 1.0f, 0.0f))
};
bool ok = RealObjectReferencePoseResolver.TryDetectDefaultUpAxis(
fragmentMatrices,
"Y",
out string detectedAxis,
out float yAlignment,
out float zAlignment);
Assert.IsTrue(ok);
Assert.AreEqual("Z", detectedAxis);
Assert.AreEqual(0.0, yAlignment, 1e-6);
Assert.AreEqual(1.0, zAlignment, 1e-6);
}
[TestMethod]
public void YUp_WorldAxisMapping_ShouldAcceptSlightlyRotatedRawFragmentFrame()
{
var fragmentMatrices = new List<double[]>
{
CreateMatrix(
axisX: Vector3.Normalize(new Vector3(0.9980f, 0.0638f, 0.0f)),
axisY: new Vector3(0.0f, 0.0f, -1.0f),
axisZ: Vector3.Normalize(new Vector3(-0.0638f, 0.9980f, 0.0f)))
};
bool ok = RealObjectReferencePoseResolver.TryResolveFromFragmentMatrices(
fragmentMatrices,
fragmentDefaultUpAxis: "Y",
hostUpAxis: "Y",
out RealObjectReferencePose pose);
Assert.IsTrue(ok);
Assert.AreEqual(LocalAxisDirection.PositiveX, pose.HostWorldXAxisLocalAxis);
Assert.AreEqual(LocalAxisDirection.PositiveZ, pose.HostWorldYAxisLocalAxis);
Assert.AreEqual(LocalAxisDirection.NegativeY, pose.HostWorldZAxisLocalAxis);
Assert.AreEqual(LocalAxisDirection.PositiveX, pose.HostSemanticXAxisLocalAxis);
Assert.AreEqual(LocalAxisDirection.PositiveY, pose.HostSemanticYAxisLocalAxis);
Assert.AreEqual(LocalAxisDirection.PositiveZ, pose.HostSemanticZAxisLocalAxis);
Assert.AreEqual(LocalAxisDirection.PositiveY, pose.HostUpLocalAxis);
}
[TestMethod]
public void YUp_WorldAxisMapping_ShouldRejectAmbiguousRawFragmentFrame()
{
var fragmentMatrices = new List<double[]>
{
CreateMatrix(
axisX: Vector3.Normalize(new Vector3(0.80f, 0.60f, 0.0f)),
axisY: new Vector3(0.0f, 0.0f, -1.0f),
axisZ: Vector3.Normalize(new Vector3(-0.60f, 0.80f, 0.0f)))
};
bool ok = RealObjectReferencePoseResolver.TryResolveFromFragmentMatrices(
fragmentMatrices,
fragmentDefaultUpAxis: "Y",
hostUpAxis: "Y",
out RealObjectReferencePose pose);
Assert.IsFalse(ok);
Assert.IsNull(pose);
}
private static double[] CreateMatrix(Vector3 axisX, Vector3 axisY, Vector3 axisZ)
{
return new double[]
{
axisX.X, axisX.Y, axisX.Z, 0.0,
axisY.X, axisY.Y, axisY.Z, 0.0,
axisZ.X, axisZ.Y, axisZ.Z, 0.0,
0.0, 0.0, 0.0, 1.0
};
}
private static void AssertVector(Vector3 actual, double x, double y, double z, double tolerance = 1e-6)
{
Assert.AreEqual(x, actual.X, tolerance);
Assert.AreEqual(y, actual.Y, tolerance);
Assert.AreEqual(z, actual.Z, tolerance);
}
}
}

View File

@ -0,0 +1,322 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
using System;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class RotatedObjectExtentHelperTests
{
[TestMethod]
public void YUp_HostY90_ForRealObject_ShouldKeepUpExtentUnchanged()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.YUp);
Quaternion correction = adapter.CreateHostRotationCorrection(
new LocalEulerRotationCorrection(0.0, 90.0, 0.0));
var result = RotatedObjectExtentHelper.CalculateProjectedSemanticExtents(
convention,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
correctionQuaternion: correction);
Assert.AreEqual(4.0, result.forwardExtent, 1e-6);
Assert.AreEqual(6.0, result.sideExtent, 1e-6);
Assert.AreEqual(2.0, result.upExtent, 1e-6);
}
[TestMethod]
public void YUp_HostZ90_ForRealObject_ShouldPromoteForwardSizeToUpExtent()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.YUp);
Quaternion correction = adapter.CreateHostRotationCorrection(
new LocalEulerRotationCorrection(0.0, 0.0, 90.0));
var result = RotatedObjectExtentHelper.CalculateProjectedSemanticExtents(
convention,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
correctionQuaternion: correction);
Assert.AreEqual(2.0, result.forwardExtent, 1e-6);
Assert.AreEqual(4.0, result.sideExtent, 1e-6);
Assert.AreEqual(6.0, result.upExtent, 1e-6);
}
[TestMethod]
public void ZUp_HostY90_ShouldPromoteForwardSizeToUpExtent()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.ZUp);
var convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp);
Quaternion correction = adapter.CreateCanonicalRotationCorrection(
new LocalEulerRotationCorrection(0.0, 90.0, 0.0));
var result = RotatedObjectExtentHelper.CalculateProjectedSemanticExtents(
convention,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
correctionQuaternion: correction);
Assert.AreEqual(2.0, result.forwardExtent, 1e-6);
Assert.AreEqual(4.0, result.sideExtent, 1e-6);
Assert.AreEqual(6.0, result.upExtent, 1e-6);
}
[TestMethod]
public void ZUp_HostZ90_ShouldKeepUpExtentUnchanged()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.ZUp);
var convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp);
Quaternion correction = adapter.CreateCanonicalRotationCorrection(
new LocalEulerRotationCorrection(0.0, 0.0, 90.0));
var result = RotatedObjectExtentHelper.CalculateProjectedSemanticExtents(
convention,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
correctionQuaternion: correction);
Assert.AreEqual(4.0, result.forwardExtent, 1e-6);
Assert.AreEqual(6.0, result.sideExtent, 1e-6);
Assert.AreEqual(2.0, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostY90_ShouldSwapForwardAndSide()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(0.0, 90.0, 0.0));
Assert.AreEqual(4.0, result.forwardExtent, 1e-6);
Assert.AreEqual(6.0, result.sideExtent, 1e-6);
Assert.AreEqual(2.0, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostY45_ShouldBlendForwardAndSide_AndKeepUp()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(0.0, 45.0, 0.0));
double expectedPlanar = 5.0 * Math.Sqrt(2.0);
Assert.AreEqual(expectedPlanar, result.forwardExtent, 1e-6);
Assert.AreEqual(expectedPlanar, result.sideExtent, 1e-6);
Assert.AreEqual(2.0, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostY135_ShouldBlendForwardAndSide_AndKeepUp()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(0.0, 135.0, 0.0));
double expectedPlanar = 5.0 * Math.Sqrt(2.0);
Assert.AreEqual(expectedPlanar, result.forwardExtent, 1e-6);
Assert.AreEqual(expectedPlanar, result.sideExtent, 1e-6);
Assert.AreEqual(2.0, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostY180_ShouldKeepSemanticExtents()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(0.0, 180.0, 0.0));
Assert.AreEqual(6.0, result.forwardExtent, 1e-6);
Assert.AreEqual(4.0, result.sideExtent, 1e-6);
Assert.AreEqual(2.0, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostY270_ShouldSwapForwardAndSide()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(0.0, 270.0, 0.0));
Assert.AreEqual(4.0, result.forwardExtent, 1e-6);
Assert.AreEqual(6.0, result.sideExtent, 1e-6);
Assert.AreEqual(2.0, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostX90_ShouldKeepForward_AndSwapSideWithUp()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(90.0, 0.0, 0.0));
Assert.AreEqual(6.0, result.forwardExtent, 1e-6);
Assert.AreEqual(2.0, result.sideExtent, 1e-6);
Assert.AreEqual(4.0, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostX45_ShouldKeepForward_AndBlendSideWithUp()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(45.0, 0.0, 0.0));
double expectedBlend = 3.0 * Math.Sqrt(2.0);
Assert.AreEqual(6.0, result.forwardExtent, 1e-6);
Assert.AreEqual(expectedBlend, result.sideExtent, 1e-6);
Assert.AreEqual(expectedBlend, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostX135_ShouldKeepForward_AndBlendSideWithUp()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(135.0, 0.0, 0.0));
double expectedBlend = 3.0 * Math.Sqrt(2.0);
Assert.AreEqual(6.0, result.forwardExtent, 1e-6);
Assert.AreEqual(expectedBlend, result.sideExtent, 1e-6);
Assert.AreEqual(expectedBlend, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostX180_ShouldKeepSemanticExtents()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(180.0, 0.0, 0.0));
Assert.AreEqual(6.0, result.forwardExtent, 1e-6);
Assert.AreEqual(4.0, result.sideExtent, 1e-6);
Assert.AreEqual(2.0, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostX270_ShouldKeepForward_AndSwapSideWithUp()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(270.0, 0.0, 0.0));
Assert.AreEqual(6.0, result.forwardExtent, 1e-6);
Assert.AreEqual(2.0, result.sideExtent, 1e-6);
Assert.AreEqual(4.0, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostZ90_ShouldPromoteUpToForward_AndKeepSide()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(0.0, 0.0, 90.0));
Assert.AreEqual(2.0, result.forwardExtent, 1e-6);
Assert.AreEqual(4.0, result.sideExtent, 1e-6);
Assert.AreEqual(6.0, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostZ45_ShouldBlendForwardWithUp_AndKeepSide()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(0.0, 0.0, 45.0));
double expectedBlend = 4.0 * Math.Sqrt(2.0);
Assert.AreEqual(expectedBlend, result.forwardExtent, 1e-6);
Assert.AreEqual(4.0, result.sideExtent, 1e-6);
Assert.AreEqual(expectedBlend, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostZ135_ShouldBlendForwardWithUp_AndKeepSide()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(0.0, 0.0, 135.0));
double expectedBlend = 4.0 * Math.Sqrt(2.0);
Assert.AreEqual(expectedBlend, result.forwardExtent, 1e-6);
Assert.AreEqual(4.0, result.sideExtent, 1e-6);
Assert.AreEqual(expectedBlend, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostZ180_ShouldKeepSemanticExtents()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(0.0, 0.0, 180.0));
Assert.AreEqual(6.0, result.forwardExtent, 1e-6);
Assert.AreEqual(4.0, result.sideExtent, 1e-6);
Assert.AreEqual(2.0, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostZ270_ShouldPromoteUpToForward_AndKeepSide()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(0.0, 0.0, 270.0));
Assert.AreEqual(2.0, result.forwardExtent, 1e-6);
Assert.AreEqual(4.0, result.sideExtent, 1e-6);
Assert.AreEqual(6.0, result.upExtent, 1e-6);
}
}
}

View File

@ -0,0 +1,85 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils;
using System.Numerics;
using NavisworksTransport.Core.Config;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class ViewpointHelperTests
{
[TestMethod]
public void ResolvePathViewpointProfile_ShouldReturnExpectedDefaults()
{
var config = ConfigManager.Instance.Current.PathEditing;
var ground = ViewpointHelper.ResolvePathViewpointProfile(ViewpointHelper.ViewpointStrategy.PathGroundSelection);
var hoisting = ViewpointHelper.ResolvePathViewpointProfile(ViewpointHelper.ViewpointStrategy.PathHoistingSelection);
var rail = ViewpointHelper.ResolvePathViewpointProfile(ViewpointHelper.ViewpointStrategy.PathRailSelection);
Assert.AreEqual(12.0, ground.CameraDistanceMeters, 1e-9);
Assert.AreEqual(90.0, ground.ElevationDegrees, 1e-9);
Assert.AreEqual(config.HoistingViewDistanceMeters, hoisting.CameraDistanceMeters, 1e-9);
Assert.AreEqual(config.HoistingViewElevationDegrees, hoisting.ElevationDegrees, 1e-9);
Assert.AreEqual(ViewpointHelper.PathCameraHorizontalMode.Side, hoisting.HorizontalMode);
Assert.AreEqual(config.RailViewDistanceMeters, rail.CameraDistanceMeters, 1e-9);
Assert.AreEqual(config.RailViewElevationDegrees, rail.ElevationDegrees, 1e-9);
Assert.AreEqual(ViewpointHelper.PathCameraHorizontalMode.Side, rail.HorizontalMode);
}
[TestMethod]
public void ResolveCameraOffsetDirection_Hoisting_ShouldLookFromSideAndSlightlyAbove()
{
Vector3 hostUp = Vector3.UnitZ;
Vector3 forward = Vector3.UnitX;
var profile = ViewpointHelper.ResolvePathViewpointProfile(ViewpointHelper.ViewpointStrategy.PathHoistingSelection);
Vector3 offset = ViewpointHelper.ResolveCameraOffsetDirection(profile, hostUp, forward);
Assert.AreEqual(0.0f, offset.X, 1e-5f);
Assert.IsTrue(offset.Y > 0.0f);
Assert.IsTrue(offset.Z > 0.0f);
}
[TestMethod]
public void ResolveCameraOffsetDirection_Rail_ShouldLookFromSideAndSlightlyAbove()
{
Vector3 hostUp = Vector3.UnitZ;
Vector3 forward = Vector3.UnitX;
var profile = ViewpointHelper.ResolvePathViewpointProfile(ViewpointHelper.ViewpointStrategy.PathRailSelection);
Vector3 offset = ViewpointHelper.ResolveCameraOffsetDirection(profile, hostUp, forward);
Assert.AreEqual(0.0f, offset.X, 1e-5f);
Assert.IsTrue(offset.Y > 0.0f);
Assert.IsTrue(offset.Z > 0.0f);
}
[TestMethod]
public void ResolveHorizontalPathForward_ShouldFallbackWhenPathIsVertical()
{
Vector3 hostUp = Vector3.UnitZ;
Vector3 rawVertical = Vector3.UnitZ;
Vector3 fallback = Vector3.UnitY;
Vector3 resolved = ViewpointHelper.ResolveHorizontalPathForward(rawVertical, hostUp, fallback);
Assert.AreEqual(0.0f, resolved.Z, 1e-5f);
Assert.AreEqual(1.0f, resolved.Y, 1e-5f);
}
[TestMethod]
public void ResolveFocusViewpointProfile_ShouldCentralizeNamedFocusStrategies()
{
var modelFocus = ViewpointHelper.ResolveFocusViewpointProfile(ViewpointHelper.ViewpointStrategy.ModelFocus);
var collision = ViewpointHelper.ResolveFocusViewpointProfile(ViewpointHelper.ViewpointStrategy.CollisionCloseUp);
Assert.AreEqual(60.0, modelFocus.ViewAngleDegrees, 1e-9);
Assert.AreEqual(0.25, modelFocus.TargetViewRatio, 1e-9);
Assert.AreEqual(60.0, collision.ViewAngleDegrees, 1e-9);
Assert.AreEqual(0.25, collision.TargetViewRatio, 1e-9);
}
}
}

View File

@ -0,0 +1,120 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
using System;
using System.Numerics;
using Autodesk.Navisworks.Api;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class VirtualGroundPoseCharacterizationTests
{
[TestMethod]
public void YUp_GroundVirtualPose_WithYUpVirtualAsset_ShouldUseHostYAsUp()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.YUp);
Vector3 hostForward = Vector3.Normalize(new Vector3(-0.8987f, 0.0f, 0.4386f));
Vector3 canonicalForward = adapter.ToCanonicalVector3(hostForward);
bool ok = CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
canonicalForward,
HostCoordinateAdapter.CanonicalUpVector3,
convention,
out Quaternion canonicalRotation);
Assert.IsTrue(ok);
Matrix4x4 hostLinear = adapter.FromCanonicalLinearTransform(
Matrix4x4.CreateFromQuaternion(canonicalRotation));
AssertColumn(hostLinear, 0, -0.8987, 0.0000, 0.4386);
AssertColumn(hostLinear, 1, 0.0000, 1.0000, 0.0000);
AssertColumn(hostLinear, 2, -0.4386, 0.0000, -0.8987);
}
[TestMethod]
public void YUp_GroundVirtualPose_WithYUpVirtualAsset_ShouldMatchHostYUpDefaultPlanarConvention()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
Vector3 hostForward = Vector3.Normalize(new Vector3(-0.8987f, 0.0f, 0.4386f));
Vector3 canonicalForward = adapter.ToCanonicalVector3(hostForward);
Assert.IsTrue(CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
canonicalForward,
HostCoordinateAdapter.CanonicalUpVector3,
ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.YUp),
out Quaternion assetRotation));
Assert.IsTrue(CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
canonicalForward,
HostCoordinateAdapter.CanonicalUpVector3,
ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.YUp),
out Quaternion hostDefaultRotation));
Matrix4x4 assetHostLinear = adapter.FromCanonicalLinearTransform(
Matrix4x4.CreateFromQuaternion(assetRotation));
Matrix4x4 hostDefaultLinear = adapter.FromCanonicalLinearTransform(
Matrix4x4.CreateFromQuaternion(hostDefaultRotation));
Assert.AreEqual(1.0, assetHostLinear.M22, 1e-3, "YUp 虚拟物体资源应把 local Y 对到宿主 up。");
Assert.AreEqual(hostDefaultLinear.M11, assetHostLinear.M11, 1e-3);
Assert.AreEqual(hostDefaultLinear.M21, assetHostLinear.M21, 1e-3);
Assert.AreEqual(hostDefaultLinear.M31, assetHostLinear.M31, 1e-3);
Assert.AreEqual(hostDefaultLinear.M12, assetHostLinear.M12, 1e-3);
Assert.AreEqual(hostDefaultLinear.M22, assetHostLinear.M22, 1e-3);
Assert.AreEqual(hostDefaultLinear.M32, assetHostLinear.M32, 1e-3);
Assert.AreEqual(hostDefaultLinear.M13, assetHostLinear.M13, 1e-3);
Assert.AreEqual(hostDefaultLinear.M23, assetHostLinear.M23, 1e-3);
Assert.AreEqual(hostDefaultLinear.M33, assetHostLinear.M33, 1e-3);
}
[TestMethod]
public void VirtualObjectReferencePose_ShouldMatchSelectedVirtualAssetConvention()
{
var yUpAdapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var zUpAdapter = new HostCoordinateAdapter(CoordinateSystemType.ZUp);
var yUpConvention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.YUp);
var zUpConvention = ModelAxisConvention.CreateVirtualObjectAssetConvention();
Matrix4x4 yUpLinear = Matrix4x4.CreateFromQuaternion(
yUpConvention.CreateQuaternion(new Vector3(1, 0, 0), yUpAdapter.HostUpVector3));
Matrix4x4 zUpLinear = Matrix4x4.CreateFromQuaternion(
zUpConvention.CreateQuaternion(new Vector3(1, 0, 0), zUpAdapter.HostUpVector3));
AssertColumn(yUpLinear, 0, 1.0, 0.0, 0.0);
AssertColumn(yUpLinear, 1, 0.0, 1.0, 0.0);
AssertColumn(yUpLinear, 2, 0.0, 0.0, 1.0);
AssertColumn(zUpLinear, 0, 1.0, 0.0, 0.0);
AssertColumn(zUpLinear, 1, 0.0, 1.0, 0.0);
AssertColumn(zUpLinear, 2, 0.0, 0.0, 1.0);
}
private static void AssertColumn(Matrix4x4 matrix, int column, double x, double y, double z)
{
switch (column)
{
case 0:
Assert.AreEqual(x, matrix.M11, 1e-3);
Assert.AreEqual(y, matrix.M21, 1e-3);
Assert.AreEqual(z, matrix.M31, 1e-3);
break;
case 1:
Assert.AreEqual(x, matrix.M12, 1e-3);
Assert.AreEqual(y, matrix.M22, 1e-3);
Assert.AreEqual(z, matrix.M32, 1e-3);
break;
case 2:
Assert.AreEqual(x, matrix.M13, 1e-3);
Assert.AreEqual(y, matrix.M23, 1e-3);
Assert.AreEqual(z, matrix.M33, 1e-3);
break;
default:
Assert.Fail("Only first 3 columns are valid.");
break;
}
}
}
}

View File

@ -0,0 +1,100 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Core;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class VirtualObjectModelTransformTests
{
[TestMethod]
public void BuildModelTransformPreservingScale_ShouldKeepExistingScaleAndReplaceRotationTranslation()
{
const double scaleX = 4.9213;
const double scaleY = 3.2808;
const double scaleZ = 6.5617;
var targetRotation = new QuaternionData(0.0, 0.7071068, 0.0, 0.7071068);
VirtualObjectManager.ModelTransformComponents result = VirtualObjectManager.CreateModelTransformPreservingScale(
scaleX,
scaleY,
scaleZ,
-177.129,
18.114,
-2.793,
targetRotation.ToRotation3D());
Assert.AreEqual(scaleX, result.ScaleX, 1e-4);
Assert.AreEqual(scaleY, result.ScaleY, 1e-4);
Assert.AreEqual(scaleZ, result.ScaleZ, 1e-4);
AssertRotationEquivalent(targetRotation, result.Rotation);
Assert.AreEqual(-177.129, result.TranslationX, 1e-6);
Assert.AreEqual(18.114, result.TranslationY, 1e-6);
Assert.AreEqual(-2.793, result.TranslationZ, 1e-6);
}
[TestMethod]
public void BuildModelTransformPreservingScale_ResetPose_ShouldOnlyClearRotationAndTranslation()
{
VirtualObjectManager.ModelTransformComponents result = VirtualObjectManager.CreateModelTransformPreservingScale(
12.0,
5.0,
7.0,
0.0,
0.0,
0.0,
QuaternionData.Identity.ToRotation3D());
Assert.AreEqual(12.0, result.ScaleX, 1e-6);
Assert.AreEqual(5.0, result.ScaleY, 1e-6);
Assert.AreEqual(7.0, result.ScaleZ, 1e-6);
AssertRotationEquivalent(QuaternionData.Identity, result.Rotation);
Assert.AreEqual(0.0, result.TranslationX, 1e-6);
Assert.AreEqual(0.0, result.TranslationY, 1e-6);
Assert.AreEqual(0.0, result.TranslationZ, 1e-6);
}
private static void AssertRotationEquivalent(QuaternionData expected, Autodesk.Navisworks.Api.Rotation3D actual)
{
bool same =
NearlyEqual(expected.X, actual.A) &&
NearlyEqual(expected.Y, actual.B) &&
NearlyEqual(expected.Z, actual.C) &&
NearlyEqual(expected.W, actual.D);
bool negatedSame =
NearlyEqual(expected.X, -actual.A) &&
NearlyEqual(expected.Y, -actual.B) &&
NearlyEqual(expected.Z, -actual.C) &&
NearlyEqual(expected.W, -actual.D);
Assert.IsTrue(same || negatedSame, "Rotation3D quaternion should match target rotation up to sign.");
}
private static bool NearlyEqual(double left, double right)
{
return System.Math.Abs(left - right) < 1e-6;
}
private struct QuaternionData
{
public QuaternionData(double x, double y, double z, double w)
{
X = x;
Y = y;
Z = z;
W = w;
}
public double X { get; }
public double Y { get; }
public double Z { get; }
public double W { get; }
public Autodesk.Navisworks.Api.Rotation3D ToRotation3D()
{
return new Autodesk.Navisworks.Api.Rotation3D(X, Y, Z, W);
}
public static QuaternionData Identity => new QuaternionData(0, 0, 0, 1);
}
}
}

View File

@ -0,0 +1,38 @@
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils;
namespace NavisworksTransport.UnitTests.Core
{
[TestClass]
public class PathHelperTests
{
[TestMethod]
public void BuildDuplicatedPathName_ShouldRebuildTrailingTimestamp()
{
var now = new DateTime(2026, 3, 28, 9, 45, 12);
var duplicatedName = PathHelper.BuildDuplicatedPathName("人工_0328_081530", now);
Assert.AreEqual("副本_人工_0328_094512", duplicatedName);
}
[TestMethod]
public void BuildDuplicatedPathName_ShouldKeepPlainNameWithoutTimestamp()
{
var duplicatedName = PathHelper.BuildDuplicatedPathName("我的路径");
Assert.AreEqual("副本_我的路径", duplicatedName);
}
[TestMethod]
public void GenerateCollisionReportFileName_ShouldUseUnifiedChinesePrefixAndTimestamp()
{
var now = new DateTime(2026, 4, 9, 23, 55, 1);
var fileName = PathHelper.GenerateCollisionReportFileName("人工_0409_235000", "html", now);
Assert.AreEqual("碰撞检测报告_人工_0409_235000_20260409_235501.html", fileName);
}
}
}

View File

@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using System.IO;
using Autodesk.Navisworks.Api;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NavisworksTransport.UnitTests.Core
{
[TestClass]
public class PathPersistenceTests
{
[TestMethod]
public void PathDataManager_ShouldImport_RailPreferredNormal_FromJson()
{
var tempDir = Path.Combine(Path.GetTempPath(), "NavisworksTransportTests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
var filePath = Path.Combine(tempDir, "route.json");
try
{
File.WriteAllText(filePath,
@"{
""PathPlanningData"": {
""version"": ""1.0"",
""generator"": ""test"",
""timestamp"": ""2026-03-25T00:00:00"",
""ProjectInfo"": {
""name"": ""test"",
""description"": ""test"",
""units"": ""meters"",
""coordinateSystem"": ""Global""
},
""Routes"": [
{
""id"": ""route-1"",
""name"": ""rail-test"",
""description"": """",
""pathType"": ""Rail"",
""railMountMode"": ""OverRail"",
""railPathDefinitionMode"": ""RailCenterLine"",
""railNormalOffset"": 0.25,
""railPreferredNormal"": { ""x"": -0.2, ""y"": 0.5, ""z"": 0.8 },
""totalLength"": 10.0,
""objectLimits"": { ""maxLength"": 0, ""maxWidth"": 0, ""maxHeight"": 0, ""safetyMargin"": 0 },
""gridSize"": 1.0,
""liftHeight"": 0.0,
""created"": ""2026-03-25T00:00:00"",
""points"": [
{ ""id"": ""p1"", ""name"": ""start"", ""type"": ""StartPoint"", ""index"": 0, ""x"": 0, ""y"": 0, ""z"": 0, ""created"": ""2026-03-25T00:00:00"" },
{ ""id"": ""p2"", ""name"": ""end"", ""type"": ""EndPoint"", ""index"": 1, ""x"": 10, ""y"": 0, ""z"": 0, ""created"": ""2026-03-25T00:00:00"" }
]
}
]
}
}");
var manager = new PathDataManager();
var importedRoutes = manager.ImportFromJson(filePath);
Assert.AreEqual(1, importedRoutes.Count);
Assert.IsNotNull(importedRoutes[0].RailPreferredNormal);
Assert.AreEqual(0.25, importedRoutes[0].RailNormalOffset, 1e-6);
}
finally
{
SafeDelete(tempDir);
}
}
[TestMethod]
public void PathDataManager_ShouldImport_RailPreferredNormal_FromXml()
{
var tempDir = Path.Combine(Path.GetTempPath(), "NavisworksTransportTests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
var filePath = Path.Combine(tempDir, "route.xml");
try
{
File.WriteAllText(filePath,
@"<?xml version=""1.0"" encoding=""UTF-8""?>
<PathPlanningData xmlns=""http://www.3ds.com/delmia/pathplanning"" version=""1.0"" generator=""test"" timestamp=""2026-03-25T00:00:00"">
<ProjectInfo name=""test"" description=""test"" units=""meters"" coordinateSystem=""Global"" />
<Routes>
<Route id=""route-1"" name=""rail-test"" description="""" pathType=""Rail"" railMountMode=""OverRail"" railPathDefinitionMode=""RailCenterLine"" railNormalOffset=""0.25"" railPreferredNormalX=""0.0"" railPreferredNormalY=""1.0"" railPreferredNormalZ=""-0.5"" totalLength=""10.0"" maxObjectLength=""0.0"" maxObjectWidth=""0.0"" maxObjectHeight=""0.0"" safetyMargin=""0.0"" gridSize=""1.0"" liftHeight=""0.0"" created=""2026-03-25T00:00:00"">
<Points>
<Point id=""p1"" name=""start"" type=""StartPoint"" index=""0"" x=""0"" y=""0"" z=""0"" created=""2026-03-25T00:00:00"" />
<Point id=""p2"" name=""end"" type=""EndPoint"" index=""1"" x=""10"" y=""0"" z=""0"" created=""2026-03-25T00:00:00"" />
</Points>
</Route>
</Routes>
</PathPlanningData>");
var manager = new PathDataManager();
var importedRoutes = manager.ImportFromXml(filePath);
Assert.AreEqual(1, importedRoutes.Count);
Assert.IsNotNull(importedRoutes[0].RailPreferredNormal);
Assert.AreEqual(0.25, importedRoutes[0].RailNormalOffset, 1e-6);
}
finally
{
SafeDelete(tempDir);
}
}
private static void SafeDelete(string directory)
{
if (!Directory.Exists(directory))
{
return;
}
try
{
Directory.Delete(directory, true);
}
catch
{
}
}
}
}

View File

@ -0,0 +1,47 @@
using System.Collections.Generic;
using Autodesk.Navisworks.Api;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NavisworksTransport.UnitTests.Core
{
[TestClass]
public class PathPlanningManagerHoistingCompletionTests
{
[TestMethod]
public void ReuseLastHoistingPointAsDescendPoint_ShouldPreserveGeometryAndAvoidDuplicateSegment_FromRealLogCase()
{
var originalLastAerialPointPosition = new Point3D(-132.36, -16.89, -6.95);
var pathPoints = new List<PathPoint>
{
new PathPoint(new Point3D(-140.00, -30.01, -6.95), "起吊点", PathPointType.StartPoint) { Index = 0, Direction = HoistingPointDirection.Vertical },
new PathPoint(new Point3D(-140.00, -16.89, -6.95), "提升点", PathPointType.WayPoint) { Index = 1, Direction = HoistingPointDirection.Vertical },
new PathPoint(new Point3D(-150.00, -16.89, -6.95), "路径点7", PathPointType.WayPoint) { Index = 2, Direction = HoistingPointDirection.Longitudinal },
new PathPoint(originalLastAerialPointPosition, "路径点8", PathPointType.WayPoint) { Index = 3, Direction = HoistingPointDirection.Longitudinal }
};
var finalGroundPoint = new Point3D(-132.36, -30.01, -6.95);
var originalLastPoint = pathPoints[3];
PathPoint descendPoint = PathPlanningManager.ReuseLastHoistingPointAsDescendPoint(pathPoints);
var endPoint = new PathPoint(finalGroundPoint, "落地点", PathPointType.EndPoint)
{
Index = pathPoints.Count,
Direction = HoistingPointDirection.Vertical
};
pathPoints.Add(endPoint);
Assert.AreEqual(5, pathPoints.Count);
Assert.AreSame(pathPoints[3], descendPoint);
Assert.AreSame(originalLastPoint, descendPoint);
Assert.AreEqual("下降点", pathPoints[3].Name);
Assert.AreEqual(PathPointType.WayPoint, pathPoints[3].Type);
Assert.AreEqual(HoistingPointDirection.Vertical, pathPoints[3].Direction);
Assert.AreSame(originalLastAerialPointPosition, pathPoints[3].Position);
Assert.AreEqual("落地点", pathPoints[4].Name);
Assert.AreEqual(PathPointType.EndPoint, pathPoints[4].Type);
Assert.AreEqual(HoistingPointDirection.Vertical, pathPoints[4].Direction);
Assert.AreSame(finalGroundPoint, pathPoints[4].Position);
}
}
}

View File

@ -0,0 +1,71 @@
using Autodesk.Navisworks.Api;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NavisworksTransport.UnitTests.Core
{
[TestClass]
public class PathRouteCloneTests
{
[TestMethod]
public void Clone_ShouldPreserveRailSpecificPropertiesAndGenerateNewIds()
{
var route = new PathRoute("原路径")
{
PathType = PathType.Rail,
RailMountMode = RailMountMode.OverRail,
RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine,
RailNormalOffset = 12.5,
RailPreferredNormal = new Point3D(0, 1, 0)
};
route.Points.Add(new PathPoint(new Point3D(1, 2, 3), "起点", PathPointType.StartPoint));
route.Points.Add(new PathPoint(new Point3D(4, 5, 6), "终点", PathPointType.EndPoint));
var clonedRoute = route.Clone();
Assert.AreEqual(PathType.Rail, clonedRoute.PathType);
Assert.AreEqual(RailMountMode.OverRail, clonedRoute.RailMountMode);
Assert.AreEqual(RailPathDefinitionMode.RailCenterLine, clonedRoute.RailPathDefinitionMode);
Assert.AreEqual(12.5, clonedRoute.RailNormalOffset, 1e-6);
Assert.IsNotNull(clonedRoute.RailPreferredNormal);
Assert.AreNotEqual(route.Id, clonedRoute.Id);
Assert.AreEqual(2, clonedRoute.Points.Count);
Assert.AreNotEqual(route.Points[0].Id, clonedRoute.Points[0].Id);
Assert.AreEqual(route.Points[0].Name, clonedRoute.Points[0].Name);
Assert.AreEqual(route.Points[0].Type, clonedRoute.Points[0].Type);
}
[TestMethod]
public void Clone_ShouldCopyGroundEdgesAndRemapPointReferences()
{
var route = new PathRoute("地面路径")
{
PathType = PathType.Ground,
TurnRadius = 3.0,
IsCurved = true
};
var startPoint = new PathPoint(new Point3D(0, 0, 0), "起点", PathPointType.StartPoint);
var endPoint = new PathPoint(new Point3D(10, 0, 0), "终点", PathPointType.EndPoint);
route.Points.Add(startPoint);
route.Points.Add(endPoint);
route.Edges.Add(new PathEdge
{
StartPointId = startPoint.Id,
EndPointId = endPoint.Id,
SegmentType = PathSegmentType.Straight,
PhysicalLength = 10.0
});
var clonedRoute = route.Clone();
Assert.AreEqual(PathType.Ground, clonedRoute.PathType);
Assert.AreEqual(1, clonedRoute.Edges.Count);
Assert.AreEqual(3.0, clonedRoute.TurnRadius, 1e-6);
Assert.IsTrue(clonedRoute.IsCurved);
Assert.AreEqual(clonedRoute.Points[0].Id, clonedRoute.Edges[0].StartPointId);
Assert.AreEqual(clonedRoute.Points[1].Id, clonedRoute.Edges[0].EndPointId);
Assert.AreNotEqual(route.Edges[0].Id, clonedRoute.Edges[0].Id);
}
}
}

View File

@ -0,0 +1,45 @@
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils;
namespace NavisworksTransport.UnitTests.Core
{
[TestClass]
public class SelectionClipBoxLockStateTests
{
[TestMethod]
public void ShouldRefreshOnSelectionChanged_ReturnsFalse_WhenSelectionClipBoxIsLocked()
{
var state = new SelectionClipBoxLockState();
state.LockSelection(new object[] { new object(), new object() });
bool shouldRefresh = state.ShouldRefreshOnSelectionChanged(isSelectionClipBoxMode: true);
Assert.IsFalse(shouldRefresh);
Assert.AreEqual(2, state.LockedSelectionCount);
}
[TestMethod]
public void ShouldRefreshOnSelectionChanged_ReturnsTrue_WhenSelectionClipBoxModeHasNoLockedSelection()
{
var state = new SelectionClipBoxLockState();
bool shouldRefresh = state.ShouldRefreshOnSelectionChanged(isSelectionClipBoxMode: true);
Assert.IsTrue(shouldRefresh);
Assert.AreEqual(0, state.LockedSelectionCount);
}
[TestMethod]
public void Clear_UnlocksSelectionClipBoxState()
{
var state = new SelectionClipBoxLockState();
state.LockSelection(new List<object> { new object() });
state.Clear();
Assert.IsFalse(state.IsLocked);
Assert.AreEqual(0, state.LockedSelectionCount);
}
}
}

View File

@ -0,0 +1,66 @@
using System;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
namespace NavisworksTransport.UnitTests.Integration
{
[TestClass]
[TestCategory("NavisworksIntegration")]
[TestCategory("NavisworksIntegration.Ground")]
public class AutoPathGridGenerationAutomationTests
{
private const string BaselineRouteName = "测试基准";
private const double BaselineObjectLengthInMeters = 0.4;
private const double BaselineObjectWidthInMeters = 0.4;
private const double BaselineSafetyMarginInMeters = 0.05;
private const double BaselineGridSizeInMeters = 0.3;
[TestMethod]
[Timeout(240000)]
public async Task GroundAutoPath_BaselineRoute_CreatesPath()
{
using (var client = new NavisworksTestAutomationClient())
{
await client.EnsureServiceReadyAsync(TimeSpan.FromSeconds(90)).ConfigureAwait(false);
JObject response = await client.RunAutoPathAsync(
"Ground",
BaselineRouteName,
BaselineObjectLengthInMeters,
BaselineObjectWidthInMeters,
BaselineSafetyMarginInMeters,
BaselineGridSizeInMeters).ConfigureAwait(false);
Assert.IsTrue(response.Value<bool>("ok"), "测试 HTTP 接口返回失败: " + (string)response["error"]);
JObject data = (JObject)response["data"];
Assert.IsNotNull(data, "缺少测试结果 data");
JObject sourceRoute = (JObject)data["sourceRoute"];
Assert.IsNotNull(sourceRoute, "缺少 sourceRoute");
Assert.AreEqual(BaselineRouteName, (string)sourceRoute["name"], "自动路径测试必须使用精确命名的测试基准路径作为起终点来源");
Assert.AreEqual("Ground", (string)sourceRoute["pathType"], "源路径类型不匹配");
JObject generatedRoute = (JObject)data["generatedRoute"];
Assert.IsNotNull(generatedRoute, "缺少 generatedRoute");
Assert.AreEqual("Ground", (string)generatedRoute["pathType"], "生成路径类型不匹配");
Assert.IsTrue((int)generatedRoute["pointCount"] >= 2, "生成路径至少应包含起点和终点");
Assert.IsTrue((double)generatedRoute["length"] > 0, "生成路径长度必须大于 0");
JObject planningParameters = (JObject)data["planningParameters"];
Assert.IsNotNull(planningParameters, "缺少 planningParameters");
Assert.AreEqual(BaselineObjectLengthInMeters, (double)planningParameters["objectLengthInMeters"], 1e-9, "物体长度参数不匹配");
Assert.AreEqual(BaselineObjectWidthInMeters, (double)planningParameters["objectWidthInMeters"], 1e-9, "物体宽度参数不匹配");
Assert.AreEqual(BaselineSafetyMarginInMeters, (double)planningParameters["safetyMarginInMeters"], 1e-9, "安全间隙参数不匹配");
Assert.AreEqual(BaselineGridSizeInMeters, (double)planningParameters["gridSizeInMeters"], 1e-9, "网格大小参数不匹配");
JObject segmentValidation = (JObject)data["segmentValidation"];
Assert.IsNotNull(segmentValidation, "缺少 segmentValidation");
Assert.AreEqual(0, (int)segmentValidation["blockedSampleCount"], "生成路径存在穿过不可通行网格的采样点");
Assert.AreEqual(0, (int)segmentValidation["blockedCellInteriorIntersectionCount"], "生成路径线段穿过了不可通行网格的实际归属区域");
Assert.AreEqual(0, (int)segmentValidation["invalidSampleCount"], "生成路径存在落在网格外的采样点");
}
}
}
}

View File

@ -0,0 +1,116 @@
using System;
using System.Globalization;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
namespace NavisworksTransport.UnitTests.Integration
{
internal sealed class NavisworksTestAutomationClient : IDisposable
{
private readonly HttpClient _httpClient;
public NavisworksTestAutomationClient()
{
_httpClient = new HttpClient
{
BaseAddress = new Uri("http://127.0.0.1:18777"),
Timeout = TimeSpan.FromSeconds(20)
};
}
public async Task EnsureServiceReadyAsync(TimeSpan timeout)
{
DateTime deadlineUtc = DateTime.UtcNow.Add(timeout);
Exception lastError = null;
while (DateTime.UtcNow < deadlineUtc)
{
try
{
JObject pingResponse = await GetJsonAsync("/api/test/ping").ConfigureAwait(false);
if (pingResponse.Value<bool?>("ok") == true)
{
return;
}
}
catch (Exception ex)
{
lastError = ex;
}
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
}
Assert.Fail(
"Navisworks 测试服务未就绪。请先用 start-navisworks.bat 启动 Navisworks并确保插件面板已加载。最后错误: {0}",
lastError?.Message ?? "unknown");
}
public async Task<JObject> RunVirtualCollisionTestAsync(string pathType, int timeoutSeconds)
{
string requestUri = string.Format(
"/api/test/run-virtual-collision-test?pathType={0}&timeoutSeconds={1}",
Uri.EscapeDataString(pathType),
timeoutSeconds);
return await PostJsonAsync(requestUri).ConfigureAwait(false);
}
public async Task<JObject> AnalyzeAutoPathGridAsync(string pathType)
{
string requestUri = string.Format(
"/api/test/analyze-auto-path-grid?pathType={0}",
Uri.EscapeDataString(pathType));
return await PostJsonAsync(requestUri).ConfigureAwait(false);
}
public async Task<JObject> RunAutoPathAsync(
string pathType,
string routeName,
double objectLengthInMeters,
double objectWidthInMeters,
double safetyMarginInMeters,
double gridSizeInMeters)
{
string requestUri = string.Format(
CultureInfo.InvariantCulture,
"/api/test/run-auto-path?pathType={0}&routeName={1}&objectLengthInMeters={2}&objectWidthInMeters={3}&safetyMarginInMeters={4}&gridSizeInMeters={5}",
Uri.EscapeDataString(pathType),
Uri.EscapeDataString(routeName),
objectLengthInMeters,
objectWidthInMeters,
safetyMarginInMeters,
gridSizeInMeters);
return await PostJsonAsync(requestUri).ConfigureAwait(false);
}
private async Task<JObject> GetJsonAsync(string requestUri)
{
using (HttpResponseMessage response = await _httpClient.GetAsync(requestUri).ConfigureAwait(false))
{
string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return JObject.Parse(content);
}
}
private async Task<JObject> PostJsonAsync(string requestUri)
{
using (HttpResponseMessage response = await _httpClient.PostAsync(requestUri, new StringContent(string.Empty)).ConfigureAwait(false))
{
string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return JObject.Parse(content);
}
}
public void Dispose()
{
_httpClient.Dispose();
}
}
}

View File

@ -0,0 +1,68 @@
using System;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
namespace NavisworksTransport.UnitTests.Integration
{
[TestClass]
[TestCategory("NavisworksIntegration")]
public class VirtualCollisionAutomationTests
{
private const int DefaultCollisionTimeoutSeconds = 240;
[TestMethod]
[Timeout(360000)]
public async Task GroundVirtualCollision_AutoTestRoute_Completes()
{
await AssertVirtualCollisionTestAsync("Ground").ConfigureAwait(false);
}
[TestMethod]
[Timeout(360000)]
public async Task HoistingVirtualCollision_AutoTestRoute_Completes()
{
await AssertVirtualCollisionTestAsync("Hoisting").ConfigureAwait(false);
}
[TestMethod]
[Timeout(360000)]
public async Task RailVirtualCollision_AutoTestRoute_Completes()
{
await AssertVirtualCollisionTestAsync("Rail").ConfigureAwait(false);
}
private static async Task AssertVirtualCollisionTestAsync(string pathType)
{
using (var client = new NavisworksTestAutomationClient())
{
await client.EnsureServiceReadyAsync(TimeSpan.FromSeconds(90)).ConfigureAwait(false);
JObject response = await client.RunVirtualCollisionTestAsync(pathType, DefaultCollisionTimeoutSeconds).ConfigureAwait(false);
Assert.IsTrue(response.Value<bool>("ok"), "测试 HTTP 接口返回失败: " + (string)response["error"]);
JObject data = (JObject)response["data"];
Assert.IsNotNull(data, "缺少测试结果 data");
Assert.AreEqual(pathType, (string)data["requestedPathType"], "请求的路径类型不匹配");
JObject route = (JObject)data["route"];
Assert.IsNotNull(route, "缺少 route");
Assert.AreEqual(pathType, (string)route["pathType"], "返回的路径类型不匹配");
Assert.IsFalse(string.IsNullOrWhiteSpace((string)route["name"]), "路径名为空");
JObject animatedObject = (JObject)data["animatedObject"];
Assert.IsNotNull(animatedObject, "缺少 animatedObject");
Assert.AreEqual("VirtualObject", (string)animatedObject["mode"], "当前集成测试应使用虚拟物体");
JObject animation = (JObject)data["animation"];
Assert.IsNotNull(animation, "缺少 animation");
Assert.AreEqual("Finished", (string)animation["currentState"], "动画未完成");
Assert.IsTrue((int)animation["totalFrames"] > 0, "动画总帧数应大于 0");
Assert.IsTrue(animation["detectionRecordId"] != null && (int)animation["detectionRecordId"] > 0, "检测记录 ID 无效");
Assert.IsNotNull(data["report"], "缺少碰撞报告");
}
}
}
}

View File

@ -0,0 +1,15 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("NavisworksTransport.UnitTests")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("NavisworksTransport.UnitTests")]
[assembly: AssemblyCopyright("Copyright © 2026")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
[assembly: Guid("8f8a2fd7-c7e5-4185-95a4-740a2bf6130f")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -0,0 +1,400 @@
using System;
using Autodesk.Navisworks.Api;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils;
namespace NavisworksTransport.UnitTests.Utils
{
/// <summary>
/// BoundingBoxGeometryUtils 的单元测试
/// 重点验证旋转后包围盒中心偏移计算的数学正确性
/// 使用大尺寸和夸张比例,确保结果清晰可见
/// </summary>
[TestClass]
public class BoundingBoxGeometryUtilsTests
{
#region CalculateRotatedBoundingBoxCenterOffset Tests -
/// <summary>
/// X方向超长的长方体类似长沙发/长桌绕Z轴旋转90度
/// 尺寸: 10000 x 100 x 50
/// 预期: X方向大偏移
/// </summary>
[TestMethod]
public void CalculateRotatedBoundingBoxCenterOffset_LongX_Rotate90AroundZ_LargeXOffset()
{
// X方向从 -5000 到 +5000总长10000
// Y方向从 -50 到 +50总长100
// Z方向从 -25 到 +25总长50
var bounds = new BoundingBox3D(
new Point3D(-5000, -50, -25),
new Point3D(5000, 50, 25));
// 原中心: (0, 0, 0)
// 8角点: (±5000, ±50, ±25)
// 绕Z轴旋转90度 (x'=-y, y'=x):
// (5000, 50) -> (-50, 5000)
// (5000, -50) -> (50, 5000)
// (-5000, 50) -> (-50, -5000)
// (-5000, -50) -> (50, -5000)
//
// 新X范围: [-50, 50]来自原Y范围
// 新Y范围: [-5000, 5000]来自原X范围
// 新中心: (0, 0, 0)
// 偏移: (0, 0, 0) - 等等这不应该是0
// 啊不对,让我重新算:
// 原中心是 (0, 0, 0)
// 旋转后各角点分布在 [-50,50]x[-5000,5000]x[-25,25]
// 新AABB中心仍然是 (0, 0, 0)
// 所以偏移应该是 0
// 等等,问题在于原中心是 (0,0,0),所以新中心也是 (0,0,0)
// 我需要测试非对称的,即原中心不在原点的情况
// 重新设计:让包围盒中心不在原点
// Min=(-1000, -50, -25), Max=(9000, 50, 25)
// 中心: (4000, 0, 0)
bounds = new BoundingBox3D(
new Point3D(-1000, -50, -25),
new Point3D(9000, 50, 25));
var rotation = new Rotation3D(new UnitVector3D(0, 0, 1), Math.PI / 2);
var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
// 原中心: (4000, 0, 0)
// 8角点相对中心: (-5000,±50,±25), (5000,±50,±25)
//
// 旋转90度后:
// (-5000, 50) -> (-50, -5000)
// (-5000, -50) -> (50, -5000)
// (5000, 50) -> (-50, 5000)
// (5000, -50) -> (50, 5000)
//
// 新X范围: [-50, 50]
// 新Y范围: [-5000, 5000]
// 新中心: (0, 0, 0)
// 偏移: (0-4000, 0-0, 0-0) = (-4000, 0, 0)
Console.WriteLine($"LongX-Rotate90: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
Assert.AreEqual(-4000, offset.X, 1, "X方向大偏移应为-4000");
Assert.AreEqual(0, offset.Y, 1, "Y偏移应为0");
Assert.AreEqual(0, offset.Z, 0.1, "Z偏移应为0");
}
/// <summary>
/// Y方向超长的长方体绕X轴旋转90度
/// 尺寸: 100 x 10000 x 50
/// </summary>
[TestMethod]
public void CalculateRotatedBoundingBoxCenterOffset_LongY_Rotate90AroundX_LargeYOffset()
{
// Y方向从 -1000 到 +9000总长10000中心在4000
// X方向从 -50 到 +50总长100
// Z方向从 -25 到 +25总长50
var bounds = new BoundingBox3D(
new Point3D(-50, -1000, -25),
new Point3D(50, 9000, 25));
// 绕X轴旋转90度 (y'=-z, z'=y)
var rotation = new Rotation3D(new UnitVector3D(1, 0, 0), Math.PI / 2);
var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
// 原中心: (0, 4000, 0)
// 8角点相对中心: (±50, -5000, ±25), (±50, 5000, ±25)
//
// 旋转90度后:
// y' = -z, z' = y
// (50, -5000, 25) -> (50, -25, -5000)
// (50, -5000, -25) -> (50, 25, -5000)
// (50, 5000, 25) -> (50, -25, 5000)
// (50, 5000, -25) -> (50, 25, 5000)
// (-50, -5000, 25) -> (-50, -25, -5000)
// (-50, -5000, -25) -> (-50, 25, -5000)
// (-50, 5000, 25) -> (-50, -25, 5000)
// (-50, 5000, -25) -> (-50, 25, 5000)
//
// 新X范围: [-50, 50]
// 新Y范围: [-25, 25]
// 新Z范围: [-5000, 5000]
// 新中心: (0, 0, 0)
// 偏移: (0-0, 0-4000, 0-0) = (0, -4000, 0)
Console.WriteLine($"LongY-Rotate90: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
Assert.AreEqual(0, offset.X, 1, "X偏移应为0");
Assert.AreEqual(-4000, offset.Y, 1, "Y方向大偏移应为-4000");
Assert.AreEqual(0, offset.Z, 0.1, "Z偏移应为0");
}
/// <summary>
/// Z方向超长的长方体绕Y轴旋转90度
/// 尺寸: 100 x 100 x 10000
/// </summary>
[TestMethod]
public void CalculateRotatedBoundingBoxCenterOffset_LongZ_Rotate90AroundY_LargeZOffset()
{
// Z方向从 -1000 到 +9000总长10000中心在4000
// X方向从 -50 到 +50总长100
// Y方向从 -50 到 +50总长100
var bounds = new BoundingBox3D(
new Point3D(-50, -50, -1000),
new Point3D(50, 50, 9000));
// 绕Y轴旋转90度 (z'=x, x'=-z) - 等等,让我确认旋转矩阵
// 绕Y轴旋转θ: x'=x*cosθ+z*sinθ, z'=-x*sinθ+z*cosθ
// 90度时: x'=z, z'=-x
var rotation = new Rotation3D(new UnitVector3D(0, 1, 0), Math.PI / 2);
var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
// 原中心: (0, 0, 4000)
// 8角点相对中心: (±50,±50,-5000), (±50,±50,5000)
//
// 旋转90度后 (x'=z, z'=-x):
// (50, 50, -5000) -> (-5000, 50, -50)
// (50, 50, 5000) -> (5000, 50, -50)
// (-50, 50, -5000) -> (-5000, 50, 50)
// (-50, 50, 5000) -> (5000, 50, 50)
// 以及Y=-50的四个点...
//
// 新X范围: [-5000, 5000]
// 新Y范围: [-50, 50]
// 新Z范围: [-50, 50]
// 新中心: (0, 0, 0)
// 偏移: (0-0, 0-0, 0-4000) = (0, 0, -4000)
Console.WriteLine($"LongZ-Rotate90: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
Assert.AreEqual(0, offset.X, 1, "X偏移应为0");
Assert.AreEqual(0, offset.Y, 0.1, "Y偏移应为0");
Assert.AreEqual(-4000, offset.Z, 1, "Z方向大偏移应为-4000");
}
/// <summary>
/// 三轴都不同比例的长方体绕Z轴旋转45度
/// 尺寸: 1000 x 200 x 100
/// </summary>
[TestMethod]
public void CalculateRotatedBoundingBoxCenterOffset_AsymmetricXYZ_Rotate45AroundZ_MultipleOffsets()
{
// X: -500 ~ +500 (中心0但非对称也可测)
// Y: -100 ~ +100 (中心0)
// Z: -50 ~ +50 (中心0)
// 等等中心0的话偏移还是0...
// 改用非中心对称的
// X: -100 ~ +900 (总长1000中心400)
// Y: -80 ~ +120 (总长200中心20)
// Z: -30 ~ +70 (总长100中心20)
var bounds = new BoundingBox3D(
new Point3D(-100, -80, -30),
new Point3D(900, 120, 70));
// 绕Z轴旋转45度
var rotation = new Rotation3D(new UnitVector3D(0, 0, 1), Math.PI / 4);
var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
// 原中心: (400, 20, 20)
//
// 8角点相对中心:
// (-500,-100,-50), (500,-100,-50), (-500,100,-50), (500,100,-50)
// (-500,-100,50), (500,-100,50), (-500,100,50), (500,100,50)
//
// 旋转45度后 (x'=(x-y)/√2, y'=(x+y)/√2):
// (-500,-100) -> (-400/√2, -600/√2) ≈ (-283, -424)
// (500,-100) -> (600/√2, 400/√2) ≈ (424, 283)
// (-500,100) -> (-600/√2, -400/√2) ≈ (-424, -283)
// (500,100) -> (400/√2, 600/√2) ≈ (283, 424)
//
// 新X范围: [-424, 424]
// 新Y范围: [-424, 424]
// 新中心: (0, 0, 20) - Z不变
// 偏移: (0-400, 0-20, 20-20) = (-400, -20, 0)
Console.WriteLine($"Asymmetric-Rotate45: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
// 允许一定误差(因为手动计算是近似值)
Assert.IsTrue(Math.Abs(offset.X - (-400)) < 50, $"X偏移应约-400实际是{offset.X:F1}");
Assert.IsTrue(Math.Abs(offset.Y - (-20)) < 50, $"Y偏移应约-20实际是{offset.Y:F1}");
Assert.AreEqual(0, offset.Z, 0.1, "Z偏移应为0");
}
/// <summary>
/// 实际沙发案例放大版
/// 模拟沙发: 长2000宽100高80中心偏移
/// </summary>
[TestMethod]
public void CalculateRotatedBoundingBoxCenterOffset_BigSofa_Rotate90AroundZ_RealisticCase()
{
// 沙发包围盒放大版
// X: -200 ~ +1800 (总长2000中心800)
// Y: -50 ~ +50 (总长100中心0)
// Z: 0 ~ +80 (总高80中心40)
var bounds = new BoundingBox3D(
new Point3D(-200, -50, 0),
new Point3D(1800, 50, 80));
var rotation = new Rotation3D(new UnitVector3D(0, 0, 1), Math.PI / 2);
var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
// 原中心: (800, 0, 40)
//
// 8角点相对中心:
// (-1000,-50,-40), (1000,-50,-40), (-1000,50,-40), (1000,50,-40)
// (-1000,-50,40), (1000,-50,40), (-1000,50,40), (1000,50,40)
//
// 旋转90度后 (x'=-y, y'=x):
// (-1000,-50) -> (50, -1000)
// (1000,-50) -> (50, 1000)
// (-1000,50) -> (-50, -1000)
// (1000,50) -> (-50, 1000)
//
// 新X范围: [-50, 50]
// 新Y范围: [-1000, 1000]
// 新中心: (0, 0, 40) - Z不变
// 偏移: (0-800, 0-0, 40-40) = (-800, 0, 0)
Console.WriteLine($"BigSofa-Rotate90: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
Assert.AreEqual(-800, offset.X, 10, "X方向偏移应为-800");
Assert.AreEqual(0, offset.Y, 10, "Y偏移应为0");
Assert.AreEqual(0, offset.Z, 0.1, "Z偏移应为0");
}
/// <summary>
/// X方向超长绕Z轴旋转45度 - 测试非90度旋转
/// 尺寸: 10000 x 100 x 50
/// </summary>
[TestMethod]
public void CalculateRotatedBoundingBoxCenterOffset_LongX_Rotate45AroundZ_DiagonalOffset()
{
// X方向从 -1000 到 +9000总长10000中心4000
// Y方向从 -50 到 +50总长100
var bounds = new BoundingBox3D(
new Point3D(-1000, -50, -25),
new Point3D(9000, 50, 25));
// 绕Z轴旋转45度
var rotation = new Rotation3D(new UnitVector3D(0, 0, 1), Math.PI / 4);
var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
// 原中心: (4000, 0, 0)
// 8角点相对中心: (-5000,±50,±25), (5000,±50,±25)
//
// 旋转45度后 (x'=(x-y)/√2, y'=(x+y)/√2):
// (-5000, 50) -> (-5050/√2, -4950/√2) ≈ (-3571, -3500)
// (-5000, -50) -> (-4950/√2, -5050/√2) ≈ (-3500, -3571)
// (5000, 50) -> (4950/√2, 5050/√2) ≈ (3500, 3571)
// (5000, -50) -> (5050/√2, 4950/√2) ≈ (3571, 3500)
//
// 新X范围: [-3571, 3571]
// 新Y范围: [-3571, 3571]
// 新中心: (0, 0, 0)
// 偏移: (0-4000, 0-0, 0-0) = (-4000, 0, 0)
Console.WriteLine($"LongX-Rotate45: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
// X方向应该有较大负偏移接近-4000
Assert.IsTrue(offset.X < -3500, $"45度旋转后X偏移应小于-3500实际是{offset.X:F1}");
Assert.IsTrue(offset.X > -4500, $"45度旋转后X偏移应大于-4500实际是{offset.X:F1}");
// Y方向偏移应该很小因为原Y中心是0
Assert.IsTrue(Math.Abs(offset.Y) < 100, $"45度旋转后Y偏移应接近0实际是{offset.Y:F1}");
Assert.AreEqual(0, offset.Z, 0.1, "Z偏移应为0");
}
/// <summary>
/// XY方向都非对称的长方体绕Z轴旋转45度 - 测试双向偏移
/// 尺寸: 8000 x 4000 x 100
/// </summary>
[TestMethod]
public void CalculateRotatedBoundingBoxCenterOffset_XYBothAsymmetric_Rotate45AroundZ_BothOffsets()
{
// X方向从 -3000 到 +5000总长8000中心2000
// Y方向从 -1000 到 +3000总长4000中心1000
var bounds = new BoundingBox3D(
new Point3D(-3000, -1000, -50),
new Point3D(5000, 3000, 50));
// 绕Z轴旋转45度
var rotation = new Rotation3D(new UnitVector3D(0, 0, 1), Math.PI / 4);
var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
// 原中心: (2000, 1000, 0)
// 8角点相对中心: (-5000,-2000,±50), (3000,-2000,±50), (-5000,2000,±50), (3000,2000,±50)
//
// 旋转45度后:
// (-5000, -2000) -> (-3000/√2, -7000/√2) ≈ (-2121, -4950)
// (3000, -2000) -> (5000/√2, 1000/√2) ≈ (3536, 707)
// (-5000, 2000) -> (-7000/√2, -3000/√2) ≈ (-4950, -2121)
// (3000, 2000) -> (1000/√2, 5000/√2) ≈ (707, 3536)
//
// 新X范围: [-4950, 3536]
// 新Y范围: [-4950, 3536]
// 新中心: ((-4950+3536)/2, (-4950+3536)/2, 0) = (-707, -707, 0)
// 偏移: (-707-2000, -707-1000, 0-0) = (-2707, -1707, 0)
Console.WriteLine($"XY-Asymmetric-Rotate45: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
// X方向应该有较大负偏移
Assert.IsTrue(offset.X < -2000, $"45度旋转后X偏移应小于-2000实际是{offset.X:F1}");
Assert.IsTrue(offset.X > -3500, $"45度旋转后X偏移应大于-3500实际是{offset.X:F1}");
// Y方向也应该有负偏移但比X小
Assert.IsTrue(offset.Y < -1000, $"45度旋转后Y偏移应小于-1000实际是{offset.Y:F1}");
Assert.IsTrue(offset.Y > -2500, $"45度旋转后Y偏移应大于-2500实际是{offset.Y:F1}");
Assert.AreEqual(0, offset.Z, 0.1, "Z偏移应为0");
}
/// <summary>
/// 零旋转验证 - 无论什么形状不旋转偏移都应为0
/// </summary>
[TestMethod]
public void CalculateRotatedBoundingBoxCenterOffset_NoRotation_ZeroOffset()
{
var bounds = new BoundingBox3D(
new Point3D(-1000, -100, -50),
new Point3D(9000, 100, 50));
var rotation = Rotation3D.Identity;
var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
Console.WriteLine($"No-Rotation: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
Assert.AreEqual(0, offset.X, 1e-6, "无旋转时X偏移应为0");
Assert.AreEqual(0, offset.Y, 1e-6, "无旋转时Y偏移应为0");
Assert.AreEqual(0, offset.Z, 1e-6, "无旋转时Z偏移应为0");
}
/// <summary>
/// 180度旋转验证 - 包围盒应该不变(只是翻转)
/// </summary>
[TestMethod]
public void CalculateRotatedBoundingBoxCenterOffset_180DegreeRotation_ZeroOffset()
{
var bounds = new BoundingBox3D(
new Point3D(-1000, -100, -50),
new Point3D(9000, 100, 50));
var rotation = new Rotation3D(new UnitVector3D(0, 0, 1), Math.PI);
var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
Console.WriteLine($"180-Rotation: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
Assert.AreEqual(0, offset.X, 1e-6, "180度旋转后X偏移应为0");
Assert.AreEqual(0, offset.Y, 1e-6, "180度旋转后Y偏移应为0");
Assert.AreEqual(0, offset.Z, 1e-6, "180度旋转后Z偏移应为0");
}
#endregion
}
}

View File

@ -1 +1,3 @@
0.2.0
# 版本号
0.15.0

28
compile.bat Normal file
View File

@ -0,0 +1,28 @@
@echo off
echo Building TransportPlugin...
REM Set MSBuild path (check Community edition first)
set MSBUILD_PATH="C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe"
REM Check if MSBuild exists, try alternative paths
if not exist %MSBUILD_PATH% (
set MSBUILD_PATH="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe"
)
if not exist %MSBUILD_PATH% (
echo MSBuild not found. Please install Visual Studio 2022 or Build Tools.
echo dotnet build is not compatible with .NET Framework WPF projects.
exit /b 1
)
echo Using MSBuild: %MSBUILD_PATH%
REM Build the project
echo Building project...
%MSBUILD_PATH% "TransportPlugin.csproj" /p:Configuration=Release /p:Platform=x64 /verbosity:minimal
:end
if %ERRORLEVEL% EQU 0 (
echo Build successful!
) else (
echo Build failed!
)

121
deploy-plugin.bat Normal file
View File

@ -0,0 +1,121 @@
@echo off
setlocal
set "TARGET_DIR=%PROGRAMDATA%\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin"
set "BUILD_DIR=%~dp0bin\x64\Release"
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"$ErrorActionPreference = 'Stop';" ^
"$targetDir = [System.IO.Path]::GetFullPath('%TARGET_DIR%');" ^
"$buildDir = [System.IO.Path]::GetFullPath('%BUILD_DIR%');" ^
"$deployDllNames = @(" ^
" 'TransportPlugin.dll'," ^
" 'geometry4Sharp.dll'," ^
" 'Newtonsoft.Json.dll'," ^
" 'Roy-T.AStar.dll'," ^
" 'SQLite.Interop.dll'," ^
" 'System.Data.SQLite.dll'," ^
" 'Tomlyn.dll'" ^
");" ^
"$deployRootFiles = @(" ^
" 'TransportRibbon.xaml'," ^
" 'TransportRibbon_16.png'," ^
" 'TransportRibbon_32.png'" ^
");" ^
"$stalePluginFiles = @(" ^
" 'Autodesk.Navisworks.Api.dll'," ^
" 'NavisworksTransport.UnitTests.dll'," ^
" 'Microsoft.VisualStudio.TestPlatform.TestFramework.dll'," ^
" 'Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll'" ^
");" ^
"function Test-FileUnlocked([string]$path) {" ^
" if (-not (Test-Path $path)) { return $true }" ^
" try {" ^
" $stream = [System.IO.File]::Open($path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None);" ^
" $stream.Close();" ^
" return $true;" ^
" } catch {" ^
" return $false;" ^
" }" ^
"}" ^
"function Wait-FileUnlocked([string]$path, [datetime]$deadline) {" ^
" while (-not (Test-FileUnlocked $path)) {" ^
" if ((Get-Date) -ge $deadline) { throw ('Target file still locked: ' + $path) }" ^
" Start-Sleep -Milliseconds 500;" ^
" }" ^
"}" ^
"" ^
"Get-Process Roamer -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue;" ^
"$deadline = (Get-Date).AddSeconds(15);" ^
"while (Get-Process Roamer -ErrorAction SilentlyContinue) {" ^
" if ((Get-Date) -ge $deadline) { throw 'Roamer.exe still running after 15 seconds.' }" ^
" Start-Sleep -Milliseconds 500;" ^
"}" ^
"$keyTargetFiles = @(" ^
" (Join-Path $targetDir 'TransportPlugin.dll')," ^
" (Join-Path $targetDir 'geometry4Sharp.dll')," ^
" (Join-Path (Join-Path $targetDir 'resources') 'unit_cube.nwc')," ^
" (Join-Path (Join-Path $targetDir 'resources') 'unit_cylinder.nwc')" ^
");" ^
"foreach ($keyFile in $keyTargetFiles) { Wait-FileUnlocked $keyFile $deadline }" ^
"" ^
"New-Item -ItemType Directory -Force -Path $targetDir | Out-Null;" ^
"foreach ($staleFileName in $stalePluginFiles) {" ^
" $stalePath = Join-Path $targetDir $staleFileName;" ^
" if (Test-Path $stalePath) { Remove-Item $stalePath -Force }" ^
"}" ^
"foreach ($dllName in $deployDllNames) {" ^
" $sourceDll = Join-Path $buildDir $dllName;" ^
" if (-not (Test-Path $sourceDll)) { throw ('Deployment source file missing: ' + $sourceDll) }" ^
" Copy-Item $sourceDll $targetDir -Force;" ^
"}" ^
"foreach ($rootFileName in $deployRootFiles) {" ^
" $sourceRootFile = Join-Path $buildDir $rootFileName;" ^
" if (Test-Path $sourceRootFile) {" ^
" Copy-Item $sourceRootFile $targetDir -Force;" ^
" foreach ($locale in @('en-US','zh-CN')) {" ^
" $localeDir = Join-Path $targetDir $locale;" ^
" New-Item -ItemType Directory -Force -Path $localeDir | Out-Null;" ^
" Copy-Item $sourceRootFile $localeDir -Force;" ^
" }" ^
" }" ^
"}" ^
"if (Test-Path (Join-Path $buildDir 'resources')) {" ^
" New-Item -ItemType Directory -Force -Path (Join-Path $targetDir 'resources') | Out-Null;" ^
" Copy-Item (Join-Path $buildDir 'resources\\*') (Join-Path $targetDir 'resources') -Recurse -Force;" ^
"}" ^
"" ^
"$sourceFiles = @();" ^
"foreach ($dllName in $deployDllNames) { $sourceFiles += Get-Item (Join-Path $buildDir $dllName) }" ^
"foreach ($rootFileName in $deployRootFiles) {" ^
" $sourceRootFile = Join-Path $buildDir $rootFileName;" ^
" if (Test-Path $sourceRootFile) { $sourceFiles += Get-Item $sourceRootFile }" ^
"}" ^
"if (Test-Path (Join-Path $buildDir 'resources')) {" ^
" $sourceFiles += Get-ChildItem (Join-Path $buildDir 'resources') -File;" ^
"}" ^
"" ^
"foreach ($sourceFile in $sourceFiles) {" ^
" $targetFile = if ($sourceFile.DirectoryName -eq $buildDir) { Join-Path $targetDir $sourceFile.Name } else { Join-Path (Join-Path $targetDir 'resources') $sourceFile.Name };" ^
" if (-not (Test-Path $targetFile)) { throw ('Deployment verification failed: missing target file ' + $targetFile) }" ^
" $targetInfo = Get-Item $targetFile;" ^
" if ($sourceFile.Length -ne $targetInfo.Length -or $sourceFile.LastWriteTime -ne $targetInfo.LastWriteTime) {" ^
" throw ('Deployment verification failed for ' + $sourceFile.Name + ': source=' + $sourceFile.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss.fff') + ' (' + $sourceFile.Length + '), target=' + $targetInfo.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss.fff') + ' (' + $targetInfo.Length + ')');" ^
" }" ^
" Write-Host ('Verified ' + $sourceFile.Name + ': ' + $targetInfo.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss.fff'));" ^
" if ($deployRootFiles -contains $sourceFile.Name) {" ^
" foreach ($locale in @('en-US','zh-CN')) {" ^
" $localeTargetFile = Join-Path (Join-Path $targetDir $locale) $sourceFile.Name;" ^
" if (-not (Test-Path $localeTargetFile)) { throw ('Deployment verification failed: missing locale target file ' + $localeTargetFile) }" ^
" $localeTargetInfo = Get-Item $localeTargetFile;" ^
" if ($sourceFile.Length -ne $localeTargetInfo.Length -or $sourceFile.LastWriteTime -ne $localeTargetInfo.LastWriteTime) {" ^
" throw ('Deployment verification failed for locale copy ' + $localeTargetFile);" ^
" }" ^
" Write-Host ('Verified ' + $locale + '\\' + $sourceFile.Name + ': ' + $localeTargetInfo.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss.fff'));" ^
" }" ^
" }" ^
"}" ^
"" ^
"Write-Host 'Plugin deployed successfully!';"
if errorlevel 1 exit /b 1
exit /b 0

View File

@ -0,0 +1,777 @@
# NavisworksTransport 系统架构设计方案
## 项目概述
NavisworksTransport是一款专为Navisworks 2026平台开发的智能物流路径规划插件旨在为建筑工程领域提供专业的BIM模型内运输路径优化、碰撞检测和施工模拟解决方案。
---
## 3.2.2.1 业务架构
### 业务目标与价值主张
NavisworksTransport插件致力于解决建筑施工过程中的物流运输规划难题通过智能化的路径规划和碰撞检测技术提升施工效率降低运输成本确保施工安全。
### 核心业务能力
#### 1. 智能路径规划服务
- **自动路径生成**: 基于A*算法的智能路径自动规划
- **手动路径编辑**: 支持用户自定义路径调整和优化
- **多楼层连接**: 跨楼层路径规划和垂直交通整合
- **路径可行性分析**: 实时路径验证和可行性评估
#### 2. 碰撞检测与冲突管理
- **实时碰撞检测**: 动态监测运输路径中的潜在冲突
- **静态障碍物识别**: 自动识别和标记固定障碍物
- **动态冲突预警**: 多对象运输时的冲突预警机制
- **碰撞报告生成**: 详细的碰撞分析报告和解决方案建议
#### 3. 动画仿真与可视化
- **物流运输模拟**: 真实的运输过程动画演示
- **时间轴精确控制**: 基于TimeLiner的精确时间控制
- **多级速度调节**: 灵活的播放速度控制
- **多对象协同**: 支持多个物流对象的协同动画
#### 4. 模型智能管理
- **楼层智能识别**: 自动识别和分类模型楼层结构
- **分层管理系统**: 基于属性的模型分层组织
- **物流类别标注**: 八大物流类别的智能标注系统
- **模型分割导出**: 按需模型分割和独立导出功能
### 业务流程设计
```mermaid
graph LR
A[选择起点] --> B[设置终点]
B --> C[自动路径规划]
C --> D[碰撞检测分析]
D --> E[路径优化调整]
E --> F[动画仿真演示]
F --> G[结果导出报告]
D --> H[发现冲突]
H --> I[冲突解决方案]
I --> E
```
### 物流分类体系
基于建筑物流的实际需求,定义八大核心物流类别:
1. **门 (Doors)**: 进出口通道管理
2. **电梯 (Elevators)**: 垂直运输通道
3. **楼梯 (Stairs)**: 人工垂直通道
4. **通道 (Channels)**: 水平运输走廊
5. **障碍物 (Obstacles)**: 固定阻碍物体
6. **装卸区 (Loading Zones)**: 材料装卸区域
7. **停车区 (Parking)**: 临时停靠区域
8. **检查点 (Checkpoints)**: 质检和验收点
---
## 3.2.2.2 应用架构
### 总体架构设计
NavisworksTransport采用分层式架构设计确保系统的可维护性、可扩展性和稳定性
```
┌─────────────────────────────────────────────────────────┐
│ 表现层 (Presentation) │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ WPF MVVM │ │ WinForms │ │ Ribbon UI │ │
│ │ 现代化界面 │ │ 传统对话框 │ │ 工具栏集成 │ │
│ └─────────────┘ └──────────────┘ └─────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ 业务逻辑层 (Business) │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ 路径规划引擎 │ │ 碰撞检测器 │ │ 动画管理器 │ │
│ │ A*算法集成 │ │ 实时冲突检测 │ │ TimeLiner集成 │ │
│ └─────────────┘ └──────────────┘ └─────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ 核心服务层 (Core) │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ 状态管理器 │ │ 事件总线 │ │ 日志服务 │ │
│ │ UI线程安全 │ │ 组件通信 │ │ 异常处理 │ │
│ └─────────────┘ └──────────────┘ └─────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ 数据访问层 (Data) │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ JSON序列化 │ │ XML导出 │ │ Navisworks API │ │
│ │ 路径数据 │ │ 配置管理 │ │ COM API集成 │ │
│ └─────────────┘ └──────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
### 三重插件架构模式
NavisworksTransport创新性地采用三重插件协同工作模式
#### 1. MainPlugin (主插件)
- **类型**: AddInPlugin
- **职责**: 主界面管理、Ribbon集成、DockPane控制
- **特点**: 插件生命周期管理、全局状态维护
#### 2. PathClickToolPlugin (交互插件)
- **类型**: ToolPlugin
- **职责**: 3D场景交互、鼠标点击事件、路径点设置
- **特点**: 实时用户交互响应、空间坐标计算
#### 3. PathPointRenderPlugin (渲染插件)
- **类型**: RenderPlugin
- **职责**: 3D路径可视化、覆盖层渲染、动画效果
- **特点**: 高性能图形渲染、实时视觉反馈
### 核心管理器组件
#### PathPlanningManager (路径规划管理器)
```csharp
public class PathPlanningManager
{
// 路径规划核心功能
public PathRoute PlanRoute(Point3D start, Point3D end);
public ValidationResult ValidatePath(PathRoute route);
public PathRoute OptimizePath(PathRoute route);
// 事件驱动架构
public event EventHandler<PathPlanningEventArgs> PathGenerated;
public event EventHandler<CollisionEventArgs> CollisionDetected;
}
```
#### LogisticsAnimationManager (动画管理器)
- **动画控制**: 播放、暂停、停止、速度调节
- **多对象协调**: 多个物流对象的同步动画
- **时间轴集成**: 与Navisworks TimeLiner深度集成
- **碰撞处理**: 动画过程中的实时碰撞检测
#### UIStateManager (UI状态管理器)
```csharp
public class UIStateManager
{
// 线程安全的UI更新
public async Task ExecuteUIUpdateAsync(Action updateAction);
// 批量UI更新优化
public void QueueUIUpdate(Action updateAction, UIUpdatePriority priority);
// 状态同步机制
public void SynchronizeViewModels();
}
```
### MVVM架构实现
采用标准MVVM模式实现UI与业务逻辑分离
- **Model**: PathRoute, PathPoint, LogisticsObject等数据模型
- **View**: WPF用户控件、窗口、对话框
- **ViewModel**: 数据绑定、命令处理、业务逻辑调用
---
## 3.2.2.3 数据架构
### 核心数据模型设计
#### 路径数据模型
```csharp
/// <summary>
/// 路径路由数据模型
/// </summary>
public class PathRoute
{
public Guid Id { get; set; } // 唯一标识
public string Name { get; set; } // 路径名称
public List<PathPoint> Points { get; set; } // 路径点集合
public PathValidationResult Validation { get; set; } // 验证结果
public DateTime CreateTime { get; set; } // 创建时间
public DateTime ModifyTime { get; set; } // 修改时间
public Dictionary<string, object> Metadata { get; set; } // 元数据扩展
// 路径统计信息
public double TotalDistance { get; set; } // 总距离
public TimeSpan EstimatedDuration { get; set; } // 预估用时
public List<CollisionInfo> Collisions { get; set; } // 碰撞信息
}
```
#### 路径点模型
```csharp
/// <summary>
/// 路径点数据模型
/// </summary>
public class PathPoint
{
public Point3D Position { get; set; } // 3D坐标
public int SequenceNumber { get; set; } // 序列号
public string Floor { get; set; } // 所属楼层
public PathPointType Type { get; set; } // 点类型
public List<string> ConnectedNodes { get; set; } // 连接节点
// 扩展属性
public double Height { get; set; } // 高度信息
public double Width { get; set; } // 通道宽度
public Dictionary<string, string> Attributes { get; set; } // 自定义属性
}
```
#### 物流对象模型
```csharp
/// <summary>
/// 物流对象数据模型
/// </summary>
public class LogisticsObject
{
public string Id { get; set; } // 对象标识
public LogisticsCategory Category { get; set; } // 物流类别
public BoundingBox3D Bounds { get; set; } // 边界框
public Transform3D Transform { get; set; } // 变换矩阵
public ModelItem NavisworksItem { get; set; } // Navisworks项引用
// 物流属性
public double Capacity { get; set; } // 容量
public double MaxSpeed { get; set; } // 最大速度
public List<string> Restrictions { get; set; } // 使用限制
public Dictionary<string, string> Properties { get; set; } // 扩展属性
}
```
### 数据持久化策略
#### 1. 主数据存储 (JSON格式)
```json
{
"projectInfo": {
"name": "物流路径规划项目",
"version": "1.0",
"createTime": "2024-01-01T00:00:00Z"
},
"pathRoutes": [
{
"id": "route-001",
"name": "主通道路径",
"points": [
{
"position": {"x": 100.0, "y": 200.0, "z": 0.0},
"sequenceNumber": 1,
"floor": "F1",
"type": "StartPoint"
}
],
"metadata": {
"totalDistance": 150.5,
"estimatedDuration": "00:05:30"
}
}
]
}
```
#### 2. 配置数据存储 (XML格式)
```xml
<Configuration>
<UserPreferences>
<DefaultSpeed>2.0</DefaultSpeed>
<CollisionTolerance>0.5</CollisionTolerance>
<AnimationFPS>30</AnimationFPS>
</UserPreferences>
<SystemSettings>
<LogLevel>Info</LogLevel>
<AutoSave>true</AutoSave>
<BackupInterval>300</BackupInterval>
</SystemSettings>
</Configuration>
```
#### 3. 缓存数据管理
- **内存缓存**: 碰撞检测结果、网格地图数据
- **会话缓存**: 用户操作历史、临时路径数据
- **持久缓存**: 楼层识别结果、模型分析数据
### 数据交换标准
#### 导入支持格式
- **JSON**: 路径数据、项目配置
- **CSV**: 批量路径点、统计数据
- **XML**: 配置文件、报告模板
#### 导出支持格式
- **NWD**: Navisworks文档格式
- **JSON**: 标准数据交换格式
- **Excel**: 统计报告和分析数据
- **PDF**: 项目报告和文档
- **CSV**: 数据分析和进一步处理
### 数据安全与完整性
#### 数据校验机制
```csharp
public class DataValidator
{
public ValidationResult ValidatePathRoute(PathRoute route)
{
// 路径完整性检查
// 坐标有效性验证
// 序列号连续性验证
// 楼层一致性检查
}
public bool VerifyDataIntegrity(string filePath)
{
// 文件完整性校验
// 数据格式验证
// 版本兼容性检查
}
}
```
#### 数据备份策略
- **自动备份**: 定时保存项目数据
- **增量备份**: 只保存变更的数据
- **版本控制**: 保留历史版本便于回滚
- **云端同步**: 支持云存储备份
---
## 3.2.2.4 技术架构
### 技术栈选型与理由
#### 开发平台选择
- **目标平台**: Navisworks 2026 (x64)
- *理由*: 最新API支持性能优化功能完整
- **运行时**: .NET Framework 4.8
- *理由*: Navisworks 2026官方支持的运行时版本
- **开发环境**: Visual Studio 2022 Community
- *理由*: 完整的.NET开发工具链优秀的调试支持
#### 核心技术选型
- **编程语言**: C# 8.0
- *理由*: 与Navisworks API完美集成丰富的语言特性
- **UI框架组合**:
- **WPF + MVVM**: 现代化用户界面,数据绑定优势
- **WinForms**: 传统对话框,快速开发
- **ElementHost**: 混合UI集成方案
#### 第三方库集成
```xml
<!-- packages.config -->
<packages>
<package id="RoyT.AStar" version="2.1.0" targetFramework="net48" />
<package id="Newtonsoft.Json" version="13.0.1" targetFramework="net48" />
<package id="System.Threading.Tasks.Extensions" version="4.5.4" targetFramework="net48" />
</packages>
```
### 关键技术实现
#### 1. 线程安全架构设计
**问题**: Navisworks API调用必须在主线程多线程UI更新容易导致崩溃
**解决方案**: UIStateManager统一线程调度
```csharp
public class UIStateManager
{
private readonly ConcurrentQueue<UIUpdateAction> _updateQueue;
private readonly DispatcherTimer _updateTimer;
public async Task ExecuteUIUpdateAsync(Action updateAction)
{
if (Application.Current.Dispatcher.CheckAccess())
{
// 已在UI线程直接执行
ExecuteWithExceptionHandling(updateAction);
}
else
{
// 切换到UI线程执行
await Application.Current.Dispatcher.InvokeAsync(() =>
{
ExecuteWithExceptionHandling(updateAction);
}, DispatcherPriority.Normal);
}
}
private void ExecuteWithExceptionHandling(Action updateAction)
{
try
{
updateAction?.Invoke();
}
catch (Exception ex)
{
GlobalExceptionHandler.HandleException(ex, "UI更新异常");
}
}
}
```
#### 2. 异步编程模式
**事件驱动异步处理**:
```csharp
public class PathPlanningManager
{
public async Task<PathRoute> PlanRouteAsync(Point3D start, Point3D end)
{
return await Task.Run(() =>
{
// CPU密集型的A*算法计算
var pathfinder = new PathFinder(gridMap);
var result = pathfinder.FindPath(start, end);
// 在UI线程更新进度
await uiStateManager.ExecuteUIUpdateAsync(() =>
{
OnPathGenerated(new PathPlanningEventArgs(result));
});
return result;
});
}
}
```
#### 3. 内存管理与性能优化
**对象池模式**:
```csharp
public class PathPointPool
{
private readonly ConcurrentQueue<PathPoint> _pool;
public PathPoint Rent()
{
if (_pool.TryDequeue(out var point))
{
return point;
}
return new PathPoint();
}
public void Return(PathPoint point)
{
point.Reset(); // 重置状态
_pool.Enqueue(point);
}
}
```
**空间索引优化**:
```csharp
public class TriangleSpatialHash
{
private readonly Dictionary<int, List<Triangle>> _spatialGrid;
private readonly double _cellSize;
public List<Triangle> GetNearbyTriangles(Point3D point, double radius)
{
var result = new List<Triangle>();
var minCell = GetCellIndex(point.X - radius, point.Y - radius);
var maxCell = GetCellIndex(point.X + radius, point.Y + radius);
for (int x = minCell.X; x <= maxCell.X; x++)
{
for (int y = minCell.Y; y <= maxCell.Y; y++)
{
var key = GetHashKey(x, y);
if (_spatialGrid.TryGetValue(key, out var triangles))
{
result.AddRange(triangles);
}
}
}
return result;
}
}
```
#### 4. API集成策略
**双API协同模式**:
```csharp
public class NavisworksIntegration
{
// Native API - 核心功能
private readonly Application _nativeApp;
// COM API - 属性持久化
private readonly ComApi.Application _comApp;
public void SetPersistentAttribute(ModelItem item, string key, string value)
{
try
{
// 使用COM API设置持久化属性
var comItem = _comApp.ActiveDocument.Models.RootItem.FindItem(item.InstanceGuid);
comItem.PropertyCategories.FindPropertyByDisplayName("User", key).Value = value;
}
catch (Exception ex)
{
// 降级到Native API内存属性
item.PropertyCategories.FindCategoryByDisplayName("User")
.Properties.FindPropertyByDisplayName(key).Value = new VariantData(value);
}
}
}
```
### 部署架构设计
#### 文件组织结构
```
%ProgramFiles%\Autodesk\Navisworks Manage 2026\Plugins\
└── TransportPlugin\
├── TransportPlugin.dll # 主程序集
├── RoyT.AStar.dll # A*算法库
├── Newtonsoft.Json.dll # JSON处理库
├── Resources\ # 资源文件
│ ├── Icons\ # 图标资源
│ ├── Templates\ # 模板文件
│ └── Localization\ # 本地化资源
├── Config\ # 配置文件
│ ├── DefaultSettings.xml # 默认设置
│ └── LoggingConfig.xml # 日志配置
└── Documentation\ # 文档
├── UserGuide.pdf # 用户指南
└── API_Reference.pdf # API参考
```
#### 安装部署流程
1. **环境检测**: 验证Navisworks 2026安装
2. **权限检查**: 确认插件目录写入权限
3. **文件部署**: 复制程序集和资源文件
4. **注册插件**: 更新Navisworks插件注册表
5. **配置初始化**: 创建默认配置文件
6. **完整性验证**: 验证安装完整性
### 安全性架构设计
#### 1. 数据安全
```csharp
public class DataSecurity
{
// 数据加密存储
public void SaveEncryptedData(string filePath, object data)
{
var json = JsonConvert.SerializeObject(data);
var encrypted = EncryptionHelper.Encrypt(json, GetMachineKey());
File.WriteAllText(filePath, encrypted);
}
// 完整性校验
public bool VerifyDataIntegrity(string filePath)
{
var hash = ComputeFileHash(filePath);
var storedHash = GetStoredHash(filePath + ".hash");
return hash == storedHash;
}
}
```
#### 2. 运行时安全
- **输入验证**: 所有用户输入严格验证
- **边界检查**: 数组访问和集合操作边界检查
- **异常处理**: 完整的异常捕获和恢复机制
- **资源管理**: 及时释放非托管资源
### 可扩展性设计
#### 插件化架构
```csharp
public interface IPathPlanningAlgorithm
{
string Name { get; }
PathRoute PlanPath(Point3D start, Point3D end, GridMap map);
}
public class AlgorithmManager
{
private readonly Dictionary<string, IPathPlanningAlgorithm> _algorithms;
public void RegisterAlgorithm(IPathPlanningAlgorithm algorithm)
{
_algorithms[algorithm.Name] = algorithm;
}
public PathRoute PlanPath(string algorithmName, Point3D start, Point3D end)
{
if (_algorithms.TryGetValue(algorithmName, out var algorithm))
{
return algorithm.PlanPath(start, end, _currentMap);
}
throw new ArgumentException($"未找到算法: {algorithmName}");
}
}
```
#### 配置驱动架构
```csharp
public class ConfigurationManager
{
public T GetConfiguration<T>(string sectionName) where T : class, new()
{
var section = _config.GetSection(sectionName);
return section.Get<T>() ?? new T();
}
public void UpdateConfiguration<T>(string sectionName, T config)
{
_config.SetSection(sectionName, config);
SaveConfiguration();
NotifyConfigurationChanged(sectionName);
}
}
```
### 监控与运维架构
#### 1. 日志系统设计
```csharp
public class LogManager
{
private static readonly ILogger _logger = LoggerFactory.CreateLogger();
public static void Info(string message, [CallerMemberName] string caller = "")
{
_logger.LogInformation($"[{caller}] {message}");
}
public static void Error(string message, Exception ex = null, [CallerMemberName] string caller = "")
{
_logger.LogError(ex, $"[{caller}] {message}");
}
// 性能监控
public static IDisposable BeginScope(string operationName)
{
return _logger.BeginScope($"Operation: {operationName}");
}
}
```
#### 2. 性能监控
```csharp
public class PerformanceMonitor
{
private readonly Dictionary<string, PerformanceCounter> _counters;
public void RecordOperation(string operation, TimeSpan duration)
{
var counter = GetOrCreateCounter(operation);
counter.Record(duration.TotalMilliseconds);
}
public PerformanceReport GenerateReport()
{
return new PerformanceReport
{
Timestamp = DateTime.Now,
MemoryUsage = GC.GetTotalMemory(false),
OperationStats = _counters.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value.GetStatistics()
)
};
}
}
```
#### 3. 错误报告系统
```csharp
public class ErrorReporting
{
public void ReportError(Exception ex, Dictionary<string, object> context)
{
var report = new ErrorReport
{
Exception = ex,
Context = context,
Environment = GetEnvironmentInfo(),
Timestamp = DateTime.Now
};
// 本地保存
SaveErrorReport(report);
// 可选:发送到服务器
if (UserConsents && IsOnline)
{
SendErrorReport(report);
}
}
}
```
---
## 架构优势与创新点
### 技术创新
1. **三重插件协同**: 创新的插件架构模式,各司其职,协同工作
2. **线程安全UI管理**: 统一的UI状态管理器解决多线程UI更新难题
3. **双API集成**: Native API与COM API协同功能完整性与兼容性并重
4. **空间索引优化**: 高效的空间数据结构,提升大模型处理性能
### 架构优势
1. **高可维护性**: 分层架构,职责清晰,便于团队协作开发
2. **强扩展性**: 插件化设计,支持功能模块热插拔
3. **高性能**: 内存管理优化,空间索引,异步处理
4. **高稳定性**: 完整的异常处理,线程安全设计,资源管理
### 商业价值
1. **降本增效**: 智能化路径规划,减少人工设计时间
2. **风险控制**: 碰撞预警机制,避免施工冲突
3. **决策支持**: 可视化动画演示,辅助方案决策
4. **标准化**: 统一的物流分类体系,规范管理流程
---
## 结论
NavisworksTransport系统架构方案充分考虑了建筑工程物流管理的实际需求结合Navisworks平台的技术特点设计了完整的四层架构体系。该方案不仅满足当前业务需求更具备良好的扩展性和维护性为未来的功能增强和技术演进提供了坚实的基础。
通过创新的三重插件协同模式、线程安全的UI管理机制、以及高效的数据处理架构NavisworksTransport将为建筑工程领域的数字化转型提供强有力的技术支撑。

View File

@ -0,0 +1,162 @@
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.patches import FancyBboxPatch
import numpy as np
# 设置中文字体 - 优先使用Microsoft YaHei
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
# 创建图形 - 调整尺寸和DPI
fig, ax = plt.subplots(1, 1, figsize=(16, 12))
# 定义颜色方案
colors = {
'presentation': '#E3F2FD', # 淡蓝色
'business': '#E8F5E8', # 淡绿色
'core': '#FFF3E0', # 淡橙色
'data': '#F3E5F5', # 淡紫色
'border': '#2C3E50', # 深灰色边框
'text': '#2C3E50' # 深蓝灰色文字
}
# 层级高度和间距 - 增加高度以容纳更多文字
layer_height = 2.2
layer_spacing = 0.4
component_width = 3.8
component_height = 1.4
component_spacing = 0.5
# 总宽度
total_width = 13
start_x = 1
# 绘制四个主要层级
# 调整层级Y位置以适应新的高度
layers = [
{
'name': '表现层 (Presentation)',
'color': colors['presentation'],
'y': 8.5,
'components': [
{'name': 'WPF MVVM\n现代化界面', 'desc': ''},
{'name': 'WinForms\n传统对话框', 'desc': ''},
{'name': 'Ribbon UI\n工具栏集成', 'desc': ''}
]
},
{
'name': '业务逻辑层 (Business)',
'color': colors['business'],
'y': 6.0,
'components': [
{'name': '路径规划引擎\nA*算法集成', 'desc': ''},
{'name': '碰撞检测器\n实时冲突检测', 'desc': ''},
{'name': '动画管理器\nTimeLiner集成', 'desc': ''}
]
},
{
'name': '核心服务层 (Core)',
'color': colors['core'],
'y': 3.5,
'components': [
{'name': '状态管理器\nUI线程安全', 'desc': ''},
{'name': '事件总线\n组件通信', 'desc': ''},
{'name': '日志服务\n异常处理', 'desc': ''}
]
},
{
'name': '数据访问层 (Data)',
'color': colors['data'],
'y': 1.0,
'components': [
{'name': 'JSON序列化\n路径数据', 'desc': ''},
{'name': 'XML导出\n配置管理', 'desc': ''},
{'name': 'Navisworks API\nCOM API集成', 'desc': ''}
]
}
]
# 绘制每个层级
for layer in layers:
# 绘制层级背景
layer_rect = FancyBboxPatch(
(start_x, layer['y']), total_width, layer_height,
boxstyle="round,pad=0.1",
facecolor=layer['color'],
edgecolor=colors['border'],
linewidth=2
)
ax.add_patch(layer_rect)
# 绘制层级标题 - 调整位置确保文字在框内正确显示
ax.text(start_x + total_width/2, layer['y'] + layer_height - 0.4,
layer['name'],
ha='center', va='center',
fontsize=15, fontweight='bold',
color=colors['text'])
# 计算组件起始位置
total_components_width = len(layer['components']) * component_width + (len(layer['components']) - 1) * component_spacing
components_start_x = start_x + (total_width - total_components_width) / 2
# 绘制组件
for i, component in enumerate(layer['components']):
comp_x = components_start_x + i * (component_width + component_spacing)
comp_y = layer['y'] + 0.2
# 绘制组件框
comp_rect = FancyBboxPatch(
(comp_x, comp_y), component_width, component_height,
boxstyle="round,pad=0.05",
facecolor='white',
edgecolor=colors['border'],
linewidth=1.5
)
ax.add_patch(comp_rect)
# 绘制组件文字 - 调整字体大小和位置
ax.text(comp_x + component_width/2, comp_y + component_height/2,
component['name'],
ha='center', va='center',
fontsize=11, fontweight='normal',
color=colors['text'],
linespacing=1.2)
# 绘制层级之间的连接线
for i in range(len(layers) - 1):
y_start = layers[i]['y']
y_end = layers[i+1]['y'] + layer_height
# 绘制多条连接线表示数据流
for j in range(3):
x_pos = start_x + total_width * (j + 1) / 4
ax.annotate('', xy=(x_pos, y_start), xytext=(x_pos, y_end),
arrowprops=dict(arrowstyle='->', color=colors['border'],
lw=1.5, alpha=0.7))
# 添加架构说明 - 调整位置和字体
ax.text(start_x + total_width + 0.5, 6.5,
'特点:\n• 分层解耦\n• 职责清晰\n• 易于维护\n• 支持扩展',
ha='left', va='center',
fontsize=12, fontweight='normal',
bbox=dict(boxstyle="round,pad=0.5", facecolor='#F8F9FA', edgecolor=colors['border']),
color=colors['text'],
linespacing=1.3)
# 设置图形属性 - 调整范围以适应新的布局
ax.set_xlim(0, 17)
ax.set_ylim(0, 12)
ax.set_aspect('equal')
ax.axis('off')
# 添加标题
plt.title('NavisworksTransport 分层架构设计',
fontsize=18, fontweight='bold', pad=20, color=colors['text'])
# 保存图形
plt.tight_layout()
plt.savefig(r'C:\Users\Tellme\apps\NavisworksTransport\doc\architecture\system_architecture.png',
dpi=300, bbox_inches='tight', facecolor='white', edgecolor='none')
print("架构图已生成: system_architecture.png")
plt.show()

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

View File

@ -0,0 +1,93 @@
# 优化选择提示信息 - 完成报告
## 优化目标
优化选择提示信息,在"已选择X个模型"后面添加节点名称,让用户更清楚当前操作的对象。
## 实现方案
### 1. 创建通用格式化函数
`LayerManagementViewModel.cs``ModelSettingsViewModel.cs` 中新增了 `FormatSelectionText` 方法:
```csharp
/// <summary>
/// 格式化选择状态文本,包含节点名称
/// </summary>
/// <param name="count">选择数量</param>
/// <param name="selectedItems">选择的项目集合</param>
/// <param name="unitName">单位名称(如"个模型"、"个节点"</param>
/// <param name="maxDisplayCount">最大显示名称数量</param>
/// <param name="maxTotalLength">最大总长度</param>
/// <returns>格式化后的选择状态文本</returns>
private string FormatSelectionText(int count, IEnumerable<ModelItem> selectedItems = null,
string unitName = "个模型", int maxDisplayCount = 3, int maxTotalLength = 80)
```
### 2. 优化显示策略
- **单选时**: 显示完整节点名称超过50字符时截断
- **多选时**: 显示前几个名称,超过一定长度或数量时用省略号
- **智能截断**: 避免信息过长影响UI显示
### 3. 更新数据结构
扩展了 `SelectNodesResult` 类,增加了 `SelectedItems` 属性来保存选择的项目信息:
```csharp
public class SelectNodesResult
{
public bool IsSuccess { get; set; }
public int Count { get; set; }
public List<ModelItem> SelectedItems { get; set; } = new List<ModelItem>();
public string ErrorMessage { get; set; }
}
```
### 4. 优化涉及的区域
#### LayerManagementViewModel.cs 优化点:
1. **节点选择区域**第801行选择节点时的状态显示
2. **楼层属性设置区域**第2314行和第2372行楼层属性相关的模型选择状态
3. **选择集保存区域**:选择集保存功能的选择状态显示
#### ModelSettingsViewModel.cs 优化点:
1. **模型选择状态显示**第329行物流属性设置相关的模型选择状态
## 优化效果示例
### 优化前:
- "已选择1个模型"
- "已选择5个模型"
### 优化后:
- "已选择1个模型: 主楼-一层-墙体-W001"
- "已选择2个模型: Wall_001, Door_002"
- "已选择5个模型: Wall_001, Door_002, Window_003..."
### 长名称处理:
- 单选超长: "已选择1个模型: 这是一个非常长的节点名称包含很多详细信息..."
- 多选智能截断: "已选择8个模型: Node1, Node2, Very_Long_Node_Name..."
## 技术细节
### 1. 线程安全处理
- 业务逻辑在后台线程执行避免UI阻塞
- 使用 `UIStateManager.ExecuteUIUpdateAsync()` 确保UI更新在主线程
### 2. 错误处理
- 保持原有的错误处理逻辑
- 当获取节点信息失败时,仍显示基本的数量信息
### 3. 性能优化
- 使用 `Take(maxDisplayCount + 1)` 限制处理的项目数量
- 智能截断避免过长字符串的处理
## 代码兼容性
- 保持向后兼容,不影响现有功能
- 编译测试通过,无破坏性更改
- 遵循现有的错误处理和日志记录模式
## 结论
成功实现了选择提示信息的优化,用户现在可以清楚地看到:
1. 选择了多少个对象
2. 选择的具体对象名称
3. 对于多选情况的智能显示
这大大提升了用户体验,让用户能够更清楚地了解当前的操作对象。

View File

@ -0,0 +1,196 @@
# 楼层属性设置功能选择同步问题修复报告
## 问题描述
在NavisworksTransport插件的分层管理功能中当用户在Navisworks中选择树节点时
- **正常现象**:选择集保存区域显示"已选择1个项目"
- **问题现象**:楼层属性设置区域的状态信息没有变化,设置按钮不能点击
## 问题根因分析
### 1. 事件订阅对比
#### 选择集保存功能(正常工作)
- **位置**`LogisticsControlPanel.xaml.cs`
- **事件订阅**`NavisApplication.ActiveDocument.CurrentSelection.Changed += OnSelectionChanged;`
- **处理逻辑**:在`OnSelectionChanged`中调用`UpdateCurrentSelectionAsync()`更新主ViewModel的选择状态
#### 楼层属性设置功能(有问题)
- **位置**`LayerManagementViewModel.cs`
- **事件订阅****没有订阅Navisworks选择变化事件**
- **初始化**:只在`InitializeAsync()`中调用一次`RefreshSelectionAsync()`
### 2. 事件处理流程差异
**正常流程(选择集保存)**
```
Navisworks选择变化
→ LogisticsControlPanel.OnSelectionChanged
→ ViewModel.UpdateCurrentSelectionAsync()
→ 更新"已选择X个项目"状态
```
**问题流程(楼层属性设置)**
```
Navisworks选择变化
→ (无事件处理)
→ 状态不更新
→ 按钮保持不可用状态
```
## 解决方案
### 1. 添加选择事件订阅
在`LayerManagementViewModel.cs`中添加选择事件处理机制:
```csharp
// 构造函数中订阅事件
SubscribeToSelectionEvents();
// 添加订阅方法
private void SubscribeToSelectionEvents()
{
try
{
if (Autodesk.Navisworks.Api.Application.ActiveDocument?.CurrentSelection != null)
{
Autodesk.Navisworks.Api.Application.ActiveDocument.CurrentSelection.Changed += OnNavisworksSelectionChanged;
LogManager.Info("[LayerManagementViewModel] 已订阅Navisworks选择变化事件");
}
}
catch (Exception ex)
{
LogManager.Error($"[LayerManagementViewModel] 订阅选择事件失败: {ex.Message}", ex);
}
}
```
### 2. 实现选择变化处理器
```csharp
private async void OnNavisworksSelectionChanged(object sender, EventArgs e)
{
try
{
// 使用UIStateManager确保在正确的线程上执行UI更新
await _uiStateManager.ExecuteUIUpdateAsync(async () =>
{
await UpdateFloorAttributeSelectionStateAsync();
});
}
catch (Exception ex)
{
LogManager.Error($"[LayerManagementViewModel] 处理选择变化事件异常: {ex.Message}", ex);
}
}
```
### 3. 专门的楼层属性状态更新方法
```csharp
private async Task UpdateFloorAttributeSelectionStateAsync()
{
try
{
// 纯业务逻辑执行(后台线程)
var result = await Task.Run(() =>
{
try
{
var document = Autodesk.Navisworks.Api.Application.ActiveDocument;
if (document?.CurrentSelection?.SelectedItems?.Count > 0)
{
var selectedCount = document.CurrentSelection.SelectedItems.Count;
return new { Success = true, Count = selectedCount, Message = $"已选择 {selectedCount} 个模型" };
}
else
{
return new { Success = true, Count = 0, Message = "请在主界面中选择需要设置的模型" };
}
}
catch (Exception ex)
{
return new { Success = false, Count = 0, Message = $"检查选择状态失败: {ex.Message}" };
}
});
// UI更新
SelectedModelsText = result.Message;
// 刷新命令状态
OnPropertyChanged(nameof(HasSelectedItems));
OnPropertyChanged(nameof(HasSelectedModels));
OnPropertyChanged(nameof(CanSetFloorAttribute));
OnPropertyChanged(nameof(CanClearFloorAttribute));
LogManager.Info($"[LayerManagementViewModel] 楼层属性选择状态已更新: {result.Message}");
}
catch (Exception ex)
{
LogManager.Error($"[LayerManagementViewModel] 更新楼层属性选择状态异常: {ex.Message}", ex);
SelectedModelsText = "检查选择状态异常";
}
}
```
### 4. 资源清理
在`Dispose()`方法中添加事件取消订阅:
```csharp
// 取消Navisworks选择变化事件订阅
UnsubscribeFromSelectionEvents();
```
## 修复验证
### 1. UI数据绑定验证
确认XAML中的绑定正确
- `SelectedModelsText` → 显示选择状态
- `CanSetFloorAttribute` → 设置按钮可用性
- `CanClearFloorAttribute` → 清除按钮可用性
### 2. 预期修复结果
修复后,当用户选择树节点时:
- ✅ 楼层属性设置区域的状态信息应该正确更新
- ✅ 设置楼层属性的按钮应该变为可点击状态
- ✅ 状态提示应该显示当前选择的对象信息
- ✅ 与选择集保存功能保持同步,无冲突
## 技术要点
### 1. 线程安全
- 使用`UIStateManager.ExecuteUIUpdateAsync()`确保UI更新在正确线程执行
- 业务逻辑在后台线程中执行避免阻塞UI
### 2. 事件处理模式
- 遵循项目的统一事件处理架构
- 业务逻辑与UI分离符合MVVM模式
### 3. 错误处理
- 完善的异常处理和日志记录
- 优雅的错误状态显示
## 影响范围
### 修改的文件
- `src/UI/WPF/ViewModels/LayerManagementViewModel.cs`
### 涉及的功能
- 楼层属性设置功能的用户体验改进
- 不影响现有选择集保存等其他功能
### 风险评估
- **低风险**修改仅限于LayerManagementViewModel内部
- **向后兼容**不改变现有API或接口
- **独立性**:与其他功能模块解耦
## 总结
此修复通过为LayerManagementViewModel添加Navisworks选择变化事件监听解决了楼层属性设置功能中状态不同步的问题。修复遵循了项目既有的架构模式确保了代码质量和系统稳定性。
修复后的楼层属性设置功能将具备与选择集保存功能相同的响应性和用户体验,提升了整体插件的一致性和可用性。

View File

@ -90,20 +90,20 @@ Navisworks作为项目审阅软件能够聚合来自多种设计软件如A
* **路径生成:** 插件将执行路径查找算法如A\*算法生成一系列3D坐标点作为路径 28。
* **模型动画化:**
* **Animator集成** Navisworks的Animator工具允许创建对象动画通过关键帧控制对象的移动和旋转 45。虽然Navisworks API对Animator的直接编程控制例如直接创建动画集和关键帧可能有限 44但可以通过以下策略实现
* **手动创建动画集(演示阶段):** 在演示中可以预先在Navisworks中为代表运输车辆的模型手动创建动画集,并将其设置为可由插件控制。
* **通过API控制对象变换** 插件可以根据计算出的路径点在TimeLiner模拟的每个时间步长通过API动态更新代表运输车辆的ModelItem的变换位置和方向从而模拟其沿路径移动 45。
* **TimeLiner集成** Navisworks的TimeLiner工具可以用于4D模拟将模型与时间表关联 6。插件可以将动画化的模型运输车辆与TimeLiner任务关联起来 45。
* **手动创建动画集(演示阶段):** 在演示中可以预先在Navisworks中为代表运输物体的模型手动创建动画集,并将其设置为可由插件控制。
* **通过API控制对象变换** 插件可以根据计算出的路径点在TimeLiner模拟的每个时间步长通过API动态更新代表运输物体的ModelItem的变换位置和方向从而模拟其沿路径移动 45。
* **TimeLiner集成** Navisworks的TimeLiner工具可以用于4D模拟将模型与时间表关联 6。插件可以将动画化的模型运输物体与TimeLiner任务关联起来 45。
* **动态碰撞检测:**
* **TimeLiner与Clash Detective联动** Navisworks Manage版本支持TimeLiner与Clash Detective的联动实现基于时间的碰撞检测 37。这是实现动态碰撞检测的关键。
* **工作流程:**
1. 插件将创建一个新的Clash Test。
2. 将“运输车辆模型”设置为选择集A。
2. 将“运输物体模型”设置为选择集A。
3. 将“门、通道、其他模型”等障碍物设置为选择集B。
4. 将此Clash Test的“Link”选项设置为“TimeLiner” 18。
5. 当TimeLiner模拟播放时Clash Detective将在每个时间步长自动运行碰撞检测并记录碰撞结果 18。
* **碰撞高亮显示:**
* **获取碰撞结果:** 插件将通过Navisworks API访问Clash Detective的碰撞结果ClashResult对象 17。
* **视觉反馈:** 当检测到碰撞时插件将使用DocumentModels.OverrideTemporaryColor方法 30将发生碰撞的模型运输车辆和障碍物)醒目地高亮显示(例如,变为红色或闪烁)。这种临时颜色覆盖可以在碰撞结束后或模拟停止时重置,使用
* **视觉反馈:** 当检测到碰撞时插件将使用DocumentModels.OverrideTemporaryColor方法 30将发生碰撞的模型运输物体和障碍物)醒目地高亮显示(例如,变为红色或闪烁)。这种临时颜色覆盖可以在碰撞结束后或模拟停止时重置,使用
DocumentModels.ResetTemporaryMaterials或DocumentModels.ResetAllTemporaryMaterials 53。
**路径规划输出基础可视化与DELMIA导出**
@ -243,7 +243,7 @@ Navisworks-Net-Plugin-Property-Database-Example 41展示了类似的方法来显
构建体素网格的过程将涉及:
1. **几何数据提取:** 遍历Navisworks模型中的ModelItem并使用COM API的InwOaFragment3.GenerateSimplePrimitives方法提取其底层的三角形、顶点等几何原始数据 17。
2. **体素化:** 将提取的几何数据投影到预定义的体素网格上。如果任何几何图元占据了某个体素,则该体素将被标记为“障碍物”。为了应对不同尺寸的运输车辆不小于10种尺寸规格体素网格的分辨率需要足够精细或者在路径规划时考虑车辆的包络体积,以确保规划的路径能够容纳车辆通过 \[用户查询\]。
2. **体素化:** 将提取的几何数据投影到预定义的体素网格上。如果任何几何图元占据了某个体素,则该体素将被标记为“障碍物”。为了应对不同尺寸的运输物体不小于10种尺寸规格体素网格的分辨率需要足够精细或者在路径规划时考虑物体的包络体积,以确保规划的路径能够容纳物体通过 \[用户查询\]。
3. **属性映射:** 结合之前为物流元素分配的自定义属性(例如,“门”、“电梯”、“通道”、“障碍物”),这些语义信息可以进一步丰富体素网格。例如,标记为“通道”的区域将被视为可通行,而标记为“障碍物”的区域则被视为不可通行。这使得路径规划算法能够理解模型的语义,而不仅仅是几何形状。
**动态碰撞检测与动画集成**
@ -252,14 +252,14 @@ Navisworks-Net-Plugin-Property-Database-Example 41展示了类似的方法来显
1. **路径生成与动画创建:**
* 插件首先利用A\*算法生成一条离散的3D路径一系列坐标点
* 然后插件将利用Navisworks的Animator功能为代表“运输车辆”的模型创建一个对象动画Object Animation。这可以通过在路径的关键点处设置模型的变换位置和旋转关键帧来实现 45。虽然Navisworks API对Animator的直接编程控制例如直接创建动画集和关键帧可能有限 44但可以通过在TimeLiner模拟的每个时间步长动态更新模型位置来模拟动画效果或者利用Navisworks内置的动画功能如通过保存视点创建动画 55
* 然后插件将利用Navisworks的Animator功能为代表“运输物体”的模型创建一个对象动画Object Animation。这可以通过在路径的关键点处设置模型的变换位置和旋转关键帧来实现 45。虽然Navisworks API对Animator的直接编程控制例如直接创建动画集和关键帧可能有限 44但可以通过在TimeLiner模拟的每个时间步长动态更新模型位置来模拟动画效果或者利用Navisworks内置的动画功能如通过保存视点创建动画 55
2. **TimeLiner集成**
* 将创建的动画与TimeLiner任务关联 45。TimeLiner允许将模型与时间表链接并模拟其在不同时间点的状态。
* 插件可以创建或修改TimeLiner任务并将运输车辆模型分配给这些任务。
* 插件可以创建或修改TimeLiner任务并将运输物体模型分配给这些任务。
3. **Time-Based Clash Detection设置**
* 通过Navisworks API访问Document.Clash属性获取Clash Detective功能 37。
* 创建一个新的ClashTest 37。
* 将“运输车辆模型”定义为碰撞测试的“选择集A”ClashSelection
* 将“运输物体模型”定义为碰撞测试的“选择集A”ClashSelection
* 将“门、通道、其他模型”等障碍物通过自定义属性筛选获得定义为碰撞测试的“选择集B”。
* **关键步骤:** 将ClashTest的Link属性设置为TimeLiner 37。这将使碰撞检测与TimeLiner的模拟进度同步。
* 运行TimeLiner模拟Navisworks将自动在每个时间步长执行碰撞检测 18。
@ -305,9 +305,9 @@ Navisworks-Net-Plugin-Property-Database-Example 41展示了类似的方法来显
* 实现ModelItem的交互式选择并获取起点/终点坐标。
* 实现通过COM API向ModelItem添加自定义“物流”属性例如类型、可通行性
* 实现基于自定义属性的ModelItem隐藏/显示功能。
* 构建基础的体素网格环境表示,并集成考虑车辆尺寸的A\*路径规划算法。
* 构建基础的体素网格环境表示,并集成考虑物体尺寸的A\*路径规划算法。
* 在Navisworks视图中可视化生成的路径临时图形
* **核心:** 实现将运输车辆模型沿规划路径进行动画化通过API控制变换或利用现有动画功能并将其与TimeLiner任务关联。
* **核心:** 实现将运输物体模型沿规划路径进行动画化通过API控制变换或利用现有动画功能并将其与TimeLiner任务关联。
* **核心:** 设置TimeLiner与Clash Detective的联动运行时间线模拟并获取碰撞结果。
* **核心:** 在检测到碰撞时使用DocumentModels.OverrideTemporaryColor醒目高亮显示碰撞对象并在模拟结束后重置颜色。
* 实现路径规划结果到简单XML文件的导出以供DELMIA导入进行概念验证。

View File

@ -228,7 +228,7 @@ public class TransportCollisionDetector
{
// 创建运输路径碰撞检测
public List<CollisionResult> DetectPathCollisions(List<Point3D> pathPoints,
TransportVehicle vehicle)
TransportObject object)
{
List<CollisionResult> collisions = new List<CollisionResult>();
@ -238,8 +238,8 @@ public class TransportCollisionDetector
// 为每个路径段创建碰撞测试
for (int i = 0; i < pathPoints.Count - 1; i++)
{
// 创建虚拟运输车辆几何体
var vehicleGeometry = CreateVehicleGeometry(pathPoints[i], pathPoints[i + 1], vehicle);
// 创建虚拟运输物体几何体
var objectGeometry = CreateObjectGeometry(pathPoints[i], pathPoints[i + 1], object);
// 创建碰撞测试
ClashTest test = new ClashTest();
@ -247,7 +247,7 @@ public class TransportCollisionDetector
// 设置检测对象
test.SelectionA.Selection.SelectAll(); // 选择所有模型
test.SelectionB.Selection.SelectGeometry(vehicleGeometry); // 虚拟车辆
test.SelectionB.Selection.SelectGeometry(objectGeometry); // 虚拟物体
// 运行检测
test.Run();
@ -277,13 +277,13 @@ public class PathAnimationController
{
private Timer animationTimer;
private List<Point3D> pathPoints;
private ModelItem transportVehicle;
private ModelItem transportObject;
private int currentSegment;
private float animationProgress;
public void StartAnimation(ModelItem vehicle, List<Point3D> path, TimeSpan duration)
public void StartAnimation(ModelItem object, List<Point3D> path, TimeSpan duration)
{
transportVehicle = vehicle;
transportObject = object;
pathPoints = path;
currentSegment = 0;
animationProgress = 0f;
@ -312,8 +312,8 @@ public class PathAnimationController
pathPoints[currentSegment + 1],
animationProgress);
// 更新车辆位置
UpdateVehiclePosition(transportVehicle, currentPos);
// 更新物体位置
UpdateObjectPosition(transportObject, currentPos);
// 更新进度
animationProgress += 0.05f; // 每帧5%进度
@ -327,17 +327,17 @@ public class PathAnimationController
Application.ActiveDocument.CurrentViewpoint.Redraw();
}
private void UpdateVehiclePosition(ModelItem vehicle, Point3D position)
private void UpdateObjectPosition(ModelItem object, Point3D position)
{
// 创建变换矩阵
Transform3D transform = Transform3D.CreateTranslation(position.ToVector3D());
// 应用变换需要COM API
ComApi.InwOaPath vehiclePath = ComApiBridge.ComApiBridge.ToInwOaPath(vehicle);
ComApi.InwOaNode vehicleNode = vehiclePath.Nodes().Last() as ComApi.InwOaNode;
ComApi.InwOaPath objectPath = ComApiBridge.ComApiBridge.ToInwOaPath(object);
ComApi.InwOaNode objectNode = objectPath.Nodes().Last() as ComApi.InwOaNode;
// 设置变换
vehicleNode.SetAttribute("LcOaNodeBaseTransform", transform.ToComMatrix());
objectNode.SetAttribute("LcOaNodeBaseTransform", transform.ToComMatrix());
}
}
```
@ -654,7 +654,7 @@ public class PluginInstaller
private static readonly string NavisworksPath =
@"%PROGRAMFILES%\Autodesk\Navisworks Manage 2017";
private static readonly string PluginTargetPath =
@"%PROGRAMFILES%\Autodesk\Navisworks Manage 2017\Plugins\NavisworksTransportPlugin";
@"%PROGRAMFILES%\Autodesk\Navisworks Manage 2017\Plugins\TransportPlugin";
public static bool InstallPlugin()
{

View File

@ -0,0 +1,278 @@
# Roy-T.AStar库的使用方法与经验总结
## 概述
Roy-T.AStar是一个高性能的C# A*寻路算法库,位于`C:\Users\Tellme\apps\OpenSource\AStar-master`。本文档记录了在NavisworksTransport项目中集成和使用该库的经验教训。
## 核心概念理解
### 1. 坐标系统
**关键发现**Roy-T.AStar使用**米坐标系统**,而不是网格索引。
```csharp
// 创建网格时,传入的是物理尺寸
var cellSize = new Size(
Distance.FromMeters((float)cellSizeInMeters),
Distance.FromMeters((float)cellSizeInMeters)
);
// GridPosition构造函数接受的是网格索引
var gridPos = new GridPosition(x, y); // x,y是网格索引如(0,0), (1,0)等
// 但Node.Position返回的是米坐标
// 例如:网格(1,0)的Node.Position可能是(2.0, 0.0)米假设cellSize=2米
```
### 2. 路径数据结构
**Path对象结构**
- `Path.Edges`: 边的列表IReadOnlyList<IEdge>
- 每条边包含:
- `Start`: 起始节点
- `End`: 终止节点
- `Distance`: 边的长度
- `TraversalVelocity`: 遍历速度
**重要特性**:连续边的关系
```
Edge[0]: Start=A, End=B
Edge[1]: Start=B, End=C // 注意Edge[0].End == Edge[1].Start
Edge[2]: Start=C, End=D
```
## 常见陷阱与解决方案
### 陷阱1坐标转换时的重复点问题
**错误做法**
```csharp
// ❌ 错误:会产生重复点
var gridPath = new List<GridPoint2D>();
gridPath.Add(ConvertToGrid(edges[0].Start)); // 添加起点
foreach (var edge in edges) {
gridPath.Add(ConvertToGrid(edge.End)); // 每条边的终点
}
// 结果:[A, B, B, C, C, D] - 转弯点重复!
```
**正确做法**
```csharp
// ✅ 正确:避免重复
var gridPath = new List<GridPoint2D>();
if (edges.Count > 0) {
gridPath.Add(ConvertToGrid(edges[0].Start)); // 只添加第一个起点
foreach (var edge in edges) {
var gridPoint = ConvertToGrid(edge.End);
// 检查是否与上一个点重复
if (gridPath.Count == 0 || !gridPath.Last().Equals(gridPoint)) {
gridPath.Add(gridPoint);
}
}
}
// 结果:[A, B, C, D] - 完美的连续路径
```
### 陷阱2米坐标到网格坐标的转换
**关键代码**
```csharp
// 从A*的米坐标转换为网格索引
double cellSizeInMeters = UnitsConverter.ConvertToMeters(gridMap.CellSize);
int gridX = (int)Math.Floor(node.Position.X / cellSizeInMeters);
int gridY = (int)Math.Floor(node.Position.Y / cellSizeInMeters);
```
### 陷阱3网格创建时的节点连接
**默认创建方法的限制**
```csharp
// Roy-T.AStar提供的默认方法会连接所有节点
var grid = Grid.CreateGridWithLateralConnections(gridSize, cellSize, velocity);
```
**自定义障碍物处理**
```csharp
// 1. 先断开所有连接
for (int x = 0; x < gridMap.Width; x++) {
for (int y = 0; y < gridMap.Height; y++) {
grid.DisconnectNode(new GridPosition(x, y));
}
}
// 2. 只连接可通行的节点
for (int x = 0; x < gridMap.Width; x++) {
for (int y = 0; y < gridMap.Height; y++) {
if (IsWalkable(x, y)) {
// 连接到右侧邻居
if (x + 1 < width && IsWalkable(x + 1, y)) {
grid.AddEdge(new GridPosition(x, y),
new GridPosition(x + 1, y), velocity);
}
// 连接到下方邻居
if (y + 1 < height && IsWalkable(x, y + 1)) {
grid.AddEdge(new GridPosition(x, y),
new GridPosition(x, y + 1), velocity);
}
}
}
}
```
## 路径优化策略
### 1. 网格路径优化算法
**问题**A*输出的路径包含大量中间点,需要优化。
**解决方案**:基于方向变化的路径简化
```csharp
private List<GridPoint2D> SimplifyPath(List<GridPoint2D> path) {
if (path.Count < 3) return path;
var simplified = new List<GridPoint2D> { path[0] };
// 计算初始方向(归一化)
int dx = path[1].X - path[0].X;
int dy = path[1].Y - path[0].Y;
var prevDirection = new GridPoint2D(
dx == 0 ? 0 : Math.Sign(dx),
dy == 0 ? 0 : Math.Sign(dy)
);
// 检测方向变化
for (int i = 2; i < path.Count; i++) {
dx = path[i].X - path[i-1].X;
dy = path[i].Y - path[i-1].Y;
var currentDirection = new GridPoint2D(
dx == 0 ? 0 : Math.Sign(dx),
dy == 0 ? 0 : Math.Sign(dy)
);
// 方向改变时保留转弯点
if (!currentDirection.Equals(prevDirection)) {
simplified.Add(path[i - 1]);
prevDirection = currentDirection;
}
}
simplified.Add(path.Last()); // 添加终点
return simplified;
}
```
### 2. 方向归一化的重要性
**问题**:不同步长的移动被误判为转弯
- `(-1, 0)``(-2, 0)`:都是向左,但步长不同
- `(0, -1)``(0, -3)`:都是向上,但步长不同
**解决**:使用`Math.Sign()`归一化方向向量
```csharp
// 归一化为单位方向向量 (-1, 0, 1)
int dirX = dx == 0 ? 0 : Math.Sign(dx);
int dirY = dy == 0 ? 0 : Math.Sign(dy);
```
## 性能优化建议
### 1. 路径缓存
对于频繁查询的路径,考虑缓存结果:
```csharp
private Dictionary<(Point3D, Point3D), Path> _pathCache;
```
### 2. 分层寻路
对于大型地图可以使用分层A*算法:
- 高层:粗略网格,快速找到大致路径
- 低层:精细网格,优化局部路径
### 3. 动态障碍物
Roy-T.AStar支持动态修改网格连接
```csharp
// 添加障碍物
grid.DisconnectNode(position);
// 移除障碍物
grid.AddEdge(from, to, velocity);
```
## 2.5D路径规划扩展
### 高度约束处理
```csharp
// 检查节点是否满足高度约束
if (cell.PassableHeights != null && cell.PassableHeights.Any()) {
bool heightOk = cell.PassableHeights.Any(
interval => interval.GetSpan() >= objectHeight
);
if (!heightOk) {
grid.DisconnectNode(position); // 不满足高度要求,断开连接
}
}
```
## 调试技巧
### 1. 日志输出
```csharp
LogManager.Info($"[A*执行] 找到路径,包含 {path.Edges.Count} 条边");
LogManager.Debug($"[路径优化] 方向从({prev.X},{prev.Y})变为({curr.X},{curr.Y})");
```
### 2. 路径验证
```csharp
// 验证路径连续性
for (int i = 1; i < path.Count; i++) {
var dist = Math.Abs(path[i].X - path[i-1].X) +
Math.Abs(path[i].Y - path[i-1].Y);
if (dist > 1) {
LogManager.Warning($"路径不连续:从{path[i-1]}到{path[i]}");
}
}
```
## 实际优化效果
在NavisworksTransport项目中的实测结果
- **原始A*输出**101个路径点
- **优化后**19个关键转弯点
- **优化率**81.2%
- **处理时间**约8ms
## 总结
使用Roy-T.AStar库的关键要点
1. 理解米坐标系统,正确进行坐标转换
2. 注意Path.Edges的连续性避免重复点
3. 使用方向归一化进行路径优化
4. 灵活运用DisconnectNode和AddEdge处理障碍物
5. 对于2.5D场景,在网格连接阶段处理高度约束
## 参考资源
- Roy-T.AStar源码`C:\Users\Tellme\apps\OpenSource\AStar-master`
- NavisworksTransport集成代码
- `src\PathPlanning\AutoPathFinder.cs`
- `src\PathPlanning\PathOptimizer.cs`

View File

@ -0,0 +1,429 @@
# **性能优化C\# A\*寻路算法GitHub上的高性能实现深度解析**
## **I. 执行摘要**
A\*算法作为路径规划领域的基石在游戏开发、机器人导航、物流优化和人工智能等多个领域发挥着举足轻重的作用。它以其在静态环境中寻找最优路径的能力而备受青睐在完备性和计算效率之间取得了平衡。当需要在已知且不变的图或网格中找到两点之间的最短或最低成本路径时A\*通常是首选算法。
然而尽管A\*算法在理论上表现出色但其在C\#中的标准实现常常面临显著的性能瓶颈。这些瓶颈通常源于低效的数据结构,尤其是“开放列表”(优先级队列),导致成本高昂的插入和提取操作。此外,堆上过多的对象分配会增加垃圾回收的开销,从而导致性能出现不可预测的波动。次优的网格遍历技术和冗余计算进一步加剧了这些问题,使得基本实现不足以应对大规模或实时应用的需求 1。
要在C\#中实现高性能A\*需要采取多方面的方法。关键策略包括采用高效的优先级队列例如二叉堆、利用C\#的值类型struct来表示节点以减少垃圾回收开销、以及实现专门的网格表示例如直接的“计算网格”、线性数组和2的幂次方网格尺寸以实现位运算。算法层面的改进例如“忽略旧节点”技术和优化的网格清理机制在最大限度地减少冗余工作和提高吞吐量方面也发挥着至关重要的作用 1。
开源社区提供了这些优化实践的优秀范例。例如roy-t/AStar项目展示了针对网格和图的现代高性能实现而CastorTiu在CodeProject上发表的“Fast PathFinder”文章中概述的详细原理则为理解各种底层优化如何产生复合效应提供了宝贵的见解。BlueRaja/High-Speed-Priority-Queue-for-C-Sharp存储库虽然并非完整的A\*实现但它是任何C\# A\*解决方案实现峰值性能的关键基础组件 1。
最终C\#中A\*算法的最佳性能是一个全面的工程挑战需要仔细考虑数据结构效率、内存管理和算法的独创性。此外路径规划算法本身的选择例如A\*与D\* Lite等动态变体至关重要必须与环境特性尤其是其动态性相符。开发人员必须严格测试其解决方案以验证其在特定应用场景中的性能提升。
## **II. A\*寻路基础**
### **A\*算法解析**
A\*是一种启发式搜索算法,旨在加权图或网格中查找从指定起始节点到目标节点的最短路径。其“启发式”特性源于它使用启发式函数来指导搜索,使其比非启发式算法更高效。它通过维护两个列表来运行:一个“开放列表”(待评估节点)和一个“关闭列表”(已评估节点) 7。
* 核心原理
A\*算法的核心在于其对每个节点的评估该评估结合了从起始点到当前节点的实际成本和从当前节点到目标点的估计成本。这种结合使得A\*能够在探索最有可能通向目标的路径时,同时避免不必要的搜索。算法在每次迭代中都会从开放列表中选择估计总成本最低的节点进行扩展,从而确保在满足启发式函数条件下找到最优路径。
* **组成部分**
* 节点与边
节点是搜索空间的基本构建块,代表离散位置,例如网格单元格或交叉点。边表示节点之间的连接,通常与“成本”或“权重”相关联。这些成本可以代表距离、时间、资源消耗等 7。
* 成本函数g(n)
这个值代表从起始节点到当前节点n的实际累积路径成本。当从当前节点移动到下一个相邻节点时新成本的计算方式为newCost \= costSoFar\[current\] \+ graph.Cost(current, next)。costSoFar字典存储了到达每个节点迄今为止的最低累积成本 7。
* 启发式函数h(n)
这是从当前节点n到目标节点的估计成本。启发式函数对于A\*的效率至关重要一个好的启发式函数可以显著减少探索的节点数量。对于基于网格的寻路常见的启发式函数包括曼哈顿距离适用于4方向移动a.xb.x+a.yb.y和欧几里得距离适用于8方向或连续移动。为了保证A\*找到最优路径启发式函数必须是可接受的从不高估到目标的真实成本和一致的从n到目标的估计成本小于或等于移动到相邻节点$n'的成本加上从n'$到目标的估计成本) \[7, 7。
* 评估函数f(n)
这是A\*优先级排序的核心计算公式为f(n)=g(n)+h(n)。这个值代表从起始节点经过当前节点n到目标的估计总成本。$f(n)$值越低的节点被认为越有希望,并被优先扩展 8。
* 开放列表(前沿/优先级队列)
这是一个数据结构,用于存储所有已发现但尚未完全评估的节点。节点根据其$f(n)值进行排序其中f(n)$值最低的节点具有最高优先级。算法不断从该列表中提取最高优先级的节点进行处理 7。
* 关闭列表cameFrom/costSoFar
这些通常是Dictionary对象。cameFrom存储每个已访问节点的父节点以便在找到目标后重建路径。costSoFar存储从起始点到每个节点迄今为止找到的最低累积成本。这些列表可防止算法不必要地重新访问和重新评估节点从而避免无限循环或低效搜索 7。
### **标准C\#实现基线 (Red Blob Games)**
Red Blob Games 提供的C\#实现 7 是一个出色的A\*算法基本教学示例。它清晰地定义了
AStarSearch方法接受Graph、start Location和goal Location作为参数。它使用Dictionary\<Location, Location\>来存储cameFrom路径信息并使用Dictionary\<Location, double\>来跟踪costSoFar。Location被定义为一个包含整数x和y坐标的struct并且为了在Dictionary和HashSet等基于哈希的集合中正确高效地使用Location对象作为键它重写了Equals和GetHashCode方法 7。
一个关键的观察点是Red Blob Games实现的PriorityQueue类。作者明确指出这是一个“占位符效率低下的实现”它使用了List的Tuple\<TElement, TPriority\> 7。这种简单的基于
List的方法需要线性扫描来查找和移除最高优先级的元素使得Dequeue操作在最坏情况下具有$O(N)$的时间复杂度,其中$N$是队列中的元素数量。
基于List的优先级队列虽然易于理解但代表着一个显著的性能瓶颈。对于大型图在每次迭代中频繁地对前沿队列进行$O(N)$的入队和出队操作会占据算法总执行时间的大部分使得算法的运行速度慢得令人无法接受。Red Blob Games 自己也建议使用C\# 2020+中内置的PriorityQueue\<\>或其它高速优先级队列库来获得生产级别的性能 1。
A\*算法的核心循环会重复地从“开放列表”中提取$f(n)$值最低的节点,并插入新的或更新的相邻节点。如果“开放列表”使用简单的\`List\`实现如Red Blob Games基线所示 7查找最小元素需要遍历所有$N$个元素时间复杂度为O(N)。类似地在维护排序顺序如果尝试的话或移除任意元素时也可能是O(N)。由于这些操作在主while循环中频繁发生对于V个节点大约运行V次对于E条边大约运行E次因此整体复杂度会迅速升级到$O(V^2)$或$O(VE)$从而使该算法对于大型搜索空间来说变得不切实际。Red Blob Games在7中明确警告其基于
List的PriorityQueue效率低下并建议使用优化的替代方案这直接证实了这是一个主要的性能瓶颈。这一观察强调了算法设计和优化中的一个基本原则关键操作的数据结构选择通常比任何其他因素更能决定算法的整体性能特征。一个看似小的$O(N)$操作当在嵌套循环中重复执行时可以将一个原本高效的算法转变为性能瓶颈。这为A\*中高效优先级队列的关键作用奠定了基础。
### **A\*与其他寻路算法的比较**
* Dijkstra算法
A\*本质上是Dijkstra算法的改进。Dijkstra算法查找图中从单个源节点到所有其他可达节点的最短路径。相比之下A\*通过使用启发式函数来指导搜索专门针对查找单个特定目的地的最短路径进行了优化。对于单目的地寻路A\*通常比Dijkstra算法探索的节点少得多从而显著加快了计算时间尤其是在大型地图上。例如一项基准测试显示在包含5000个节点的地图上A\*比Dijkstra算法快约7倍同时仍能找到相同的最优路径 \[12, 7。
* 广度优先搜索 (BFS)
BFS通过在当前深度级别探索所有相邻节点然后移动到下一级别从而在无权重图中找到最短路径。虽然它在无权重场景中保证了最短路径但对于加权图或包含障碍物的复杂环境它变得非常低效因为它不根据成本优先考虑路径。A\*及其成本函数和启发式函数专为加权图设计,在此类上下文中效率更高 \[75, S\_R42, S\_S8\]。
* 深度优先搜索 (DFS)
DFS通过尽可能深入地遍历每个分支然后回溯来探索图。它不保证找到最短路径因此不适用于大多数需要最优性的寻路应用。
A\*的核心区别在于其“启发式”特性这意味着它使用启发式函数h(n))来估计到目标的成本 \[7。这种指导允许A\*优先探索似乎直接通向目标的路径从而有效地修剪掉搜索空间中不太可能包含最优路径的大部分。这与Dijkstra算法向所有方向扩展直到找到目标或BFS逐层探索而不考虑路径成本等“非启发式”算法形成鲜明对比。12中的定量数据表明A\*在5000节点地图上比Dijkstra“快约7倍”这直接证明了这种性能优势。启发式函数减少访问节点数量的能力如12中的视觉比较所示是这种加速的直接原因。这一观察强调了算法选择是一个关键的性能决策。对于需要在加权图中找到单目的地最短路径的问题A\*通常是更好的选择因为它具有智能剪枝功能。然而A\*的有效性高度依赖于其启发式函数的质量和可接受性。选择不当的启发式函数可能导致次优路径,甚至降低性能,有时甚至比更简单的非启发式搜索更慢。这突出了在设计有效启发式函数时领域特定知识的重要性。
## **III. 性能优化的必要性**
### **为何优化A\***
* 大型搜索空间
现代应用程序尤其是在游戏开发例如开放世界环境、大型策略地图、机器人导航和复杂网络路由中涉及包含数百万个节点的地图或图。未经优化的A\*算法可能需要数秒甚至数分钟才能计算出路径,使其无法使用 1。
* 实时性要求
许多应用程序要求即时寻路。游戏、自动驾驶物体和实时策略 (RTS) 模拟需要路径在毫秒内计算完成,以确保流畅的游戏体验、响应式导航或即时决策。延迟可能导致糟糕的用户体验或关键系统故障 1。
* 高智能体密度
在涉及大量智能体(例如人群模拟、多机器人协调)的场景中,每个智能体都需要频繁地重新计算路径,即使是中等效率的算法,其累积计算负载也可能变得不堪重负。这会导致系统变慢、帧率下降或智能体“卡住”。
* 动态环境(即使有重新规划)
尽管A\*主要用于静态环境但即使在半动态场景中由于微小变化或新信息需要重新计算路径时底层A\*实现的效率也至关重要。频繁的重新规划会放大核心算法中的任何低效率。
### **识别性能瓶颈**
* 低效数据结构操作
A\*中最常见且最重要的瓶颈是其核心数据结构特别是“开放列表”优先级队列和“关闭列表”的性能。如果这些数据结构使用List或ArrayList等通用集合实现则添加、删除或搜索元素等操作可能具有$O(N)$时间复杂度。CodeProject文章明确指出“搜索开放和关闭节点列表所花费的时间”是标准A\*实现中的“主要瓶颈” 1。
* 过多内存分配(垃圾回收开销)
在C\#和其他托管语言中在堆上频繁创建对象例如为每个节点或路径段使用class实例会导致垃圾回收器GC的压力增加。GC周期可能在实时应用程序中引入不可预测的暂停或“卡顿”严重影响响应能力 1。
* 冗余计算
重复计算相同的值(例如成本或启发式估计),或在紧密循环中执行复杂的坐标转换,可能会累积显著的开销。尽管这些操作单独来看很小,但当执行数百万次时,它们可能会成为主要的性能消耗 1。
CodeProject文章 1 提供了一个引人注目的例子说明了同时优化CPU周期和内存占用对性能的重要性。文章指出主要瓶颈是“搜索开放和关闭节点列表所花费的时间”这表明与低效数据结构访问相关的CPU密集型问题。同时它强调将节点改为
struct“减少了垃圾回收开销”解决了与内存相关的性能问题。文章中的“Fast PathFinder”实现了“300到1500倍”的速度提升但代价是“对于1024x1024的网格额外增加了13MB的内存”。这明确展示了经典的空时权衡投入更多内存用于直接访问的“计算网格”和可能更大的优先级队列结构可以显著减少查找和操作所需的CPU周期从而带来整体性能的提升。这一分析强调性能优化很少是一蹴而就的。它通常是一个整体过程其中一个领域的改进例如数据结构效率降低CPU周期可能需要在另一个领域进行权衡例如增加内存使用。深入理解所选编程语言本例中为C\#如何管理内存堆与栈、垃圾回收与理解算法复杂度同样重要。开发人员必须仔细分析其应用程序的具体限制例如嵌入式设备上的内存限制与游戏PC上充足的RAM以做出明智的架构决策。
### **优化格局**
* 速度与内存
这是一个反复出现的主题。实现更高的速度通常需要使用更多的内存例如通过预计算数据结构、更大的查找表或更复杂的优先级队列数据结构。CodeProject的优化就明确指出了其内存开销 1。
* 路径质量与速度
激进的启发式算法、简化假设例如仅使用整数成本或某些算法捷径可以加速寻路但可能导致路径并非真正最优最短或成本最低。CodeProject实现中的“重新开放关闭节点”设置就是一个很好的例子启用它会产生“更好、更平滑的路径”但“会花费更多时间”。开发人员必须决定严格的最优性是否比实时响应性更优先1, 7。
* 实现复杂性
高度优化的算法,特别是那些采用巧妙位运算、自定义数据结构或复杂内存管理的算法,在实现、调试和维护方面固有地比简单、教科书式的版本更复杂。这需要在开发工作量和运行时性能之间进行权衡 1。
## **IV. C\# A\*核心优化策略**
### **提升效率的高级数据结构**
* **优先级队列**
* 基石
高效的优先级队列可以说是A\*性能最关键的组成部分。它管理着“开放列表”,确保始终首先检索到$f(n)$值最低估计总成本的节点进行扩展。这个操作即Dequeue或extract-min以及Enqueue插入新节点和可能的Decrease-Key更新节点优先级在算法执行过程中会重复进行 7。
* **优化实现**
* 二叉堆 (MinHeap)
这是A\*优先级队列最常见且广泛推荐的数据结构。它为Enqueue插入和Dequeue提取最小元素操作提供了对数时间复杂度O(logN)其中N是堆中的元素数量。这使其比简单的基于List的方法性能显著提高 3。
BlueRaja/High-Speed-Priority-Queue-for-C-Sharp库是专为C\#寻路优化的二叉堆的典型示例,强调速度和低开销 4。
roy-t/AStar库也明确利用了MinHeap来实现其高性能 3。
* 斐波那契堆
虽然理论上为decrease-key操作提供了卓越的渐近复杂度$O(1)$摊还但斐波那契堆通常具有更高的常数因子并且实现起来更复杂。在实践中对于大多数A\*场景,二叉堆由于结构更简单且缓存性能更好,通常会优于斐波那那契堆 5。
* SortedSetPriorityQueue (红黑树)
使用System.Collections.Generic.SortedSet通常实现为红黑树可以为插入、decrease-key和提取最小元素操作提供$O(\\log V)$的复杂度。虽然这是一个可行的选择,但自定义的二叉堆实现通常针对寻路特定需求进行了优化,并能提供更好的实际性能 5。
* **基于哈希的集合**
* Dictionary 或 Hashtable 用于 cameFrom 和 costSoFar
这些集合对于存储和高效检索路径信息和累积成本至关重要。它们为插入、查找和更新操作提供了平均$O(1)$的时间复杂度考虑到A\*中频繁的访问模式这一点至关重要。CodeProject文章特别指出通过将关闭列表从ArrayList替换为Hashtable性能得到了提升 1。
* Equals 和 GetHashCode 的重要性
对于用作Dictionary或HashSet中键的自定义Location或Node结构体/类正确重写Equals和GetHashCode方法至关重要。如果没有正确的实现哈希冲突会使平均$O(1)$的性能降级为$O(N)$并且Dictionary查找可能无法找到等效的节点 7。
表1A\*优先级队列实现比较分析
| 优先级队列类型 | Enqueue 复杂度 | Dequeue 复杂度 | Decrease-Key 复杂度 | A\*实际性能 | 内存开销 | C\#实现注意事项 |
| :---- | :---- | :---- | :---- | :---- | :---- | :---- |
| 无序列表 | O(1) | O(N) | O(N) | 差 | 低 | 简单但慢,不推荐 |
| 有序列表 | O(N) | O(1) | O(N) | 较差 | 低 | 插入慢,不推荐 |
| 二叉堆 (MinHeap) | O(logN) | O(logN) | O(logN) | 优秀 | 中等 | 常见BlueRaja/High-Speed-Priority-Queue-for-C-Sharp.NET内置PriorityQueue\<\> |
| 斐波那契堆 | O(1) | O(logN) | O(1) (摊还) | 良好 (高常数) | 高 | 理论最优,但实现复杂,实际常数高 |
| 红黑树 (SortedSet) | O(logN) | O(logN) | O(logN) | 良好 | 中等 | System.Collections.Generic.SortedSet |
上述表格基于5中对各种优先级队列实现的渐近复杂度分析这些分析直接适用于A\*算法。以表格形式呈现这些信息,可以清晰、简洁地比较每种实现的理论性能特征,并解释为何二叉堆(或
MinHeap通常是A\*最实用和高效的选择。它还突出了所涉及的权衡例如斐波那契堆理论上更优的decrease-key复杂度可能由于常数因子较高而无法转化为更好的实际性能。此表格可作为选择合适优先级队列的宝贵决策工具。
### **内存与对象管理**
* **节点的值类型struct**
* 减少垃圾回收开销
在C\#中class实例是分配在托管堆上的引用类型受垃圾回收的影响。而struct是值类型通常分配在栈上或内联在其他数据结构中例如数组、其他结构体。通过将节点表示定义为struct而不是class可以显著减少频繁堆分配和随后的垃圾回收周期所带来的开销。这对于A\*中频繁创建和处理大量节点的性能关键循环来说,是一项至关重要的优化 1。
* 改善缓存局部性
当struct连续存储在内存中例如在数组中它们受益于更好的CPU缓存利用率。访问已在缓存中的数据比从主内存中获取数据快得多从而带来整体性能提升。
* **优化节点结构**
* 最小化大小
除了使用struct之外最小化每个节点struct的实际内存占用也至关重要特别是对于可能隐式或显式表示数百万个节点的大型网格。CodeProject文章通过将节点结构大小从32字节优化到仅13字节来证明了这一点。这是通过删除冗余数据例如通过数组索引而不是在节点内显式存储坐标和使用更紧凑的数据类型例如对于父节点链接使用ushort而不是int假设坐标在ushort范围内实现的 1。
CodeProject文章 1 明确指出通过将节点改为
struct来“减少垃圾回收开销”这突出了C\#等托管语言中一个关键但经常被忽视的性能方面。在典型的A\*实现中可能会实例化和丢弃大量的Node对象。如果这些是class实例则每次分配都会增加垃圾回收器的压力。当GC运行时它可能会引入不可预测的暂停即使很短暂这对实时应用程序有害。通过使用struct节点被分配在栈上对于局部变量或直接嵌入到包含结构中如数组或其他结构体从而避免了单独的堆分配从而显著降低了GC周期的频率和持续时间。这是一种微妙但深刻的优化直接影响寻路算法的响应能力和可预测性。这一分析扩展到C\#性能调优的一般原则在性能关键的代码路径中最小化堆分配。这可能涉及使用struct、为频繁创建的对象实现对象池或利用更新的.NET功能如Span\<T\>进行直接内存操作所有这些都旨在减少GC压力并提高缓存局部性。
### **网格与地图表示增强**
* **用于$O(1)$访问的计算网格**
* 消除列表搜索
对于基于网格的寻路CodeProject文章中识别出的最重要优化是引入了“计算网格”。它不再维护一个需要查找操作的单独的“关闭列表”例如Hashtable而是使用一个二维数组PathFinderNode\[,\]或一个线性一维数组PathFinderNode来直接存储每个网格单元格的状态。这允许通过使用其 (X, Y) 坐标作为索引以O(1)(常数时间)访问任何节点的状态或成本信息。这完全消除了对单独的“关闭列表”查找的需求,并简化了“开放列表”的作用,使其仅管理待处理的节点 1。
* **线性数组转换**
* 简化坐标访问
为了进一步增强计算网格作者将固定的二维数组PathFinderNode\[,\]转换为线性一维数组PathFinderNode。这简化了坐标转换例如index \= y \* width \+ x并且与C\#中的原生二维数组索引相比有时可以带来更高效的CPU内存访问模式 1。
* **2的幂次方网格尺寸**
* 利用位运算
一个非常巧妙的优化是限制网格的宽度和高度为2的幂次方例如64x64、128x128、1024x1024。这允许使用位运算例如(y \<\< log2\_width) \+ x而不是计算成本更高的乘法和除法运算来进行坐标转换例如将二维(x, y)转换为一维index。位运算在CPU级别通常快得多 1。
“计算网格”、“线性数组转换”和“2的幂次方网格尺寸”的结合 1 展示了对网格问题优化深刻的理解。通过将二维坐标映射到一维数组算法本质上实现了一种高效的空间哈希形式。当与2的幂次方约束结合时坐标查找和转换可以使用闪电般的位运算来执行而不是较慢的算术运算。这将通常为
O(logN)对于基于哈希的集合或O(N)(对于基于列表的)查找转换为直接的$O(1)$内存访问。这是访问模式的根本性转变带来了巨大的性能提升特别是对于大型网格。这种方法虽然特定于基于网格的寻路但说明了高性能计算中的一个更广泛的原则理解底层内存布局、CPU架构以及利用底层操作如位移可以解锁超越通用数据结构所能提供的性能增益。对于具有固定、规则结构的问题直接数组访问和位运算技巧通常优于更抽象或通用的数据结构。
### **算法改进**
* **“忽略旧节点”策略**
* 避免昂贵的移除操作
在A\*中,可能会找到一条通往已添加到“开放列表”(优先级队列)中的节点的更短路径。一种天真的方法是搜索并移除优先级队列中旧的、成本更高的条目,这可能是一个昂贵的$O(N)或O(\\log N)$操作具体取决于优先级队列的实现。“忽略旧节点”优化避免了这种情况。相反旧的、成本更高的条目只是留在优先级队列中。当这个旧节点最终出队时通过检查“计算网格”中的costSoFar或“关闭”状态会检测到已经处理了通往它的更好路径并且旧节点会被简单地忽略。这显著简化了优先级队列的操作使其仅限于Enqueue和Dequeue避免了对现有元素进行昂贵的移除或“减少键”操作 1。
* **优化网格清理**
* 递增状态值
对于需要频繁调用寻路的应用(例如在游戏循环中),将整个网格的“开放”或“关闭”状态重置为默认值(例如零)可能是一个耗时的$O(N)$操作。CodeProject实现引入了一个巧妙的优化它不是物理上清除计算网格而是在每次新搜索时递增“开放”和“关闭”状态值或使用唯一的搜索ID。如果节点的当前状态值低于当前搜索的唯一标识符则其在先前搜索中的状态值在当前搜索中被视为“未研究”。这避免了完全的内存重新初始化大大减少了寻路调用之间的开销 1。
* **启发式函数调优**
* 对速度和路径质量的影响
启发式函数h(n)的选择和实现深刻影响A\*搜索的速度和结果路径的质量(最优性)。更准确(但仍可接受且一致)的启发式函数可以更直接地引导搜索到目标,探索更少的节点,从而加快计算速度。然而,过于复杂的启发式函数本身可能成为计算瓶颈 1。
* “破局”机制
当A\*遇到多个具有相同计算$f(n)$成本的路径时,可以应用“破局”启发式。这个额外因素有助于算法做出“最佳猜测”,以继续朝着有希望的方向搜索,通常会产生更平滑、更“自然”的路径,并防止算法不必要地探索同样“好”但最终不那么直接的替代方案 1。
* **成本精度**
* 整数与浮点成本
CodeProject作者对成本计算精度进行了实验发现使用int进行成本和总成本计算有效地丢弃小数会使算法在使用浮点数时“慢约10倍”而对于复杂地图路径质量没有显著改善。浮点运算在某些架构上可能比整数运算慢并且微妙的精度差异可能导致算法更频繁地重新评估节点。这说明了牺牲一些精度可以带来显著性能提升的权衡 1。
* **移动限制**
* 对角线
启用或禁用对角线移动8个方向与4个方向会影响搜索空间和路径外观 1。
* 重对角线
如果允许对角线移动,增加其成本(“重对角线”)可以阻止其使用,从而导致路径更趋向于正交 1。
* 惩罚转向
每次算法改变方向时增加少量成本,会导致路径更平滑、更“自然”,因为会惩罚过多的转向。这可能会增加计算时间,但会改善路径美观度 1。
* **重新开放关闭节点**
* 最优性与速度
标准A\*实现可能不会重新开放已移至“关闭列表”的节点。然而,如果发现通往“关闭”节点的新的、成本更低的路径,允许算法“重新开放”并重新评估该节点可以导致真正最优且更平滑的路径。这会增加计算时间,因为算法可能会多次访问和处理节点。对于实时应用程序,可能更倾向于稍微次优但更快的路径 1。
“忽略旧节点”和“优化网格清理”技术 1 是算法巧妙性的典范,它超越了仅仅选择高效数据结构。 “忽略旧节点”通过利用A\*最终会找到节点最低成本路径的事实,避免了优先级队列中昂贵的移除操作。这推迟并有效地消除了昂贵的列表操作。“优化网格清理”是一种巧妙的技巧,可以避免在连续寻路调用之间进行$O(N)$的内存重置这在动态或频繁查询的场景中可能是一个显著的开销。它不是将内存清零而是使用唯一的搜索ID或递增状态值从而利用现有的内存状态。这些优化展示了对算法迭代性质、内存访问模式和C\#运行时的深刻理解,寻找在微观层面减少冗余工作和昂贵操作的方法。这些技术表明,真正的性能优化通常需要多层方法。虽然数据结构提供了基础效率,但显著的收益也可以来自高度专业的算法调整,这些调整利用了问题和执行环境的特定特征。这通常涉及权衡:为了实际的、特定领域的速度而牺牲一些理论上的纯粹性或通用性。
### **预计算与缓存**
* 预计算成本
对于图结构和边成本不经常变化的静态或半静态地图预计算某些值可以显著减少运行时计算。例如roy-t/AStar库明确指出“大多数计算如边成本在构建图时就已预计算”这在实际路径搜索时节省了时间。这会将计算负载从运行时转移到初始化阶段 3。
## **V. GitHub上领先的C\# A\*实现**
### **roy-t/AStar**
* 项目概述
该项目位于github.com/roy-t/AStar被描述为C\#中“基于A\*算法的快速2D寻路库”。它支持任何面向.NET Standard 2.0或更高版本的.NET变体确保了广泛的兼容性。一个关键的设计理念是它不依赖外部依赖项使其轻量且易于集成。该库采用MIT许可证鼓励开放使用 3。
* **关键优化**
* MinHeap用于优先级队列
该库明确指出其优先级队列使用了MinHeap数据结构。这是高性能A\*的基础选择,为添加和提取元素提供了高效的$O(\\log N)$操作 3。
* 预计算成本
一个显著的优化是“大多数计算(如边成本)在构建图时就已预计算”。这会将计算工作从关键的寻路循环转移到图初始化阶段,从而缩短搜索时间 3。
* 图优先表示
尽管它提供了方便的网格类Grids.CreateGridWithLateralConnections、Grids.CreateGridWithDiagonalConnections但该库内部使用图进行所有底层寻路。这种抽象允许灵活地建模各种移动模式例如网格上的车、象或后棋移动同时利用优化的图遍历算法 3。
* 性能基准
该存储库声称具有令人印象深刻的性能指出“即使对于包含10,000个节点和40,000条边的大型图该算法也能在10毫秒内找到路径”。这一定量声明突显了其对速度的关注以及其优化的有效性 3。
* 可用性与特性
该库旨在通过网格类抽象图的细节,从而易于使用。它支持定义遍历速度,允许加权路径,而不仅仅是简单的最短距离。它还提供了模仿防止切角等行为的选项,这是旧寻路器中的常见功能 3。
### **BlueRaja/High-Speed-Priority-Queue-for-C-Sharp**
* 作为基础组件的作用
BlueRaja/High-Speed-Priority-Queue-for-C-Sharp GitHub存储库包含一个针对寻路应用优化的C\#优先级队列 4。高效的优先级队列对于A\*算法的性能至关重要。A\*的效率在很大程度上取决于其管理“开放列表”的能力该列表需要快速地插入新节点、更新现有节点的优先级以及提取具有最低成本的节点。如果优先级队列操作效率低下即使A\*算法的核心逻辑再优化整体性能也会受到严重限制。因此一个高性能的优先级队列是构建任何快速A\*实现的基础。
* 实现细节
该项目提供了一个高度优化的优先级队列实现,具有以下关键特性:
* 速度
它被描述为比其他C\#优先级队列更快,特别适用于寻路场景 4。
* 易用性
该库易于使用,简化了开发人员的集成过程 6。
* 无外部依赖
它不依赖于第三方库,这降低了项目的复杂性并简化了部署 6。
* 许可
该软件在MIT许可下可免费用于个人和商业用途 6。
* LINQ支持
它实现了IEnumerable\<T\>接口提供了对LINQ的支持使得数据查询和操作更加便捷 6。
* 单元测试
该实现经过了全面的单元测试,确保了其可靠性和正确性 6。
* 稳定优先级队列
它具有稳定的优先级队列实现,这意味着具有相同优先级的项目将按照它们入队的顺序出队,这在某些应用中可能很重要 6。
* 性能增强
在.NET 4.5下编译时,它利用了新的强制内联支持,以实现更快的速度 6。
* 分发
该项目已发布到NuGet便于集成到其他项目中 6。
* 兼容性
它应适用于.NET 2.0及更高版本 6。
* 实现
该项目包含两种优先级队列实现:一种用于最大速度(无线程安全和安全检查),另一种更易于使用且更安全 6。
* 语言
该项目完全用C\#编写 6。
* 对性能的影响
该优先级队列对A\*算法的整体性能贡献巨大。通过提供高效的入队、出队和优先级更新操作它显著减少了A\*算法核心循环中的时间消耗。例如如果一个A\*算法需要处理数百万个节点,那么每次操作从$O(N)降到O(\\log N)$所带来的性能提升是指数级的。这种基础组件的优化使得上层A\*算法能够充分发挥其启发式搜索的优势,从而在大型复杂环境中实现毫秒级的路径查找。
### **CodeProject的“Fast PathFinder” (CastorTiu的实现)**
* 历史意义与影响
CodeProject文章“A\* algorithm implementation in C\#” 1 详细介绍了CastorTiu的A\*算法实现及其优化。该实现因其在性能方面的开创性工作而具有重要的历史意义。作者最初因找不到满足其项目特定需求的C\# A\*版本而开发此实现旨在提供一个高性能且可重用的资源。该项目附带一个前端应用程序允许用户试验各种参数并分析算法行为这对于理解和调优A\*算法非常有价值 1。
* 量化性能增益
作者对标准A\*算法的性能感到沮丧尤其是在大型网格上。主要瓶颈在于搜索开放和关闭节点列表所花费的时间。为了解决这个问题作者进行了一系列关键优化最终实现了惊人的性能提升与标准算法相比速度提高了300到1500倍。例如一个标准算法需要131秒才能解决的地图使用优化版本只需100毫秒。然而这种显著的速度提升并非没有代价优化版本对于1024x1024的网格需要额外约13MB的内存 1。
* 架构经验
CastorTiu的实现引入了多项创新这些创新对于现代高性能A\*实现仍然具有重要意义:
* 数据结构优化
将开放列表从标准的ArrayList或List替换为优先级队列以提高节点检索时间。关闭列表则替换为Hashtable以实现更快的查找 1。
* 使用结构体
将节点从类切换为结构体,以减少垃圾回收开销,提高内存效率 1。
* 计算网格
最显著的优化是使用第二个“计算网格”来存储节点,从而允许通过其 (X, Y) 坐标进行$O(1)$访问。这消除了对关闭列表的需求,并简化了开放列表的作用,使其仅限于推送和弹出成本最低的节点 1。
* 内存减少
节点结构经过优化通过删除冗余坐标数据、启发式值以及使用ushort而不是int作为父节点链接将其大小从32字节减少到13字节 1。
* 线性数组
计算网格从固定二维数组PathFinderNode\[,\]更改为线性数组PathFinderNode以简化坐标访问并消除来回转换 1。
* 2的幂次方网格
添加了一个约束即网格宽度和高度必须是2的幂次方从而可以使用更快的位运算移位和逻辑运算代替数学运算进行坐标转换 1。
* 忽略旧节点
当发现通往已在开放列表中的节点的新的、成本更低的路径时,作者决定将旧节点留在列表中,而不是移除或替换它。旧节点将具有更高的成本,并在稍后处理时,由于已被标记为关闭而直接忽略。这比在列表中执行移除操作快得多 1。
* 优化网格清理
为了避免在寻路调用之间耗时的计算网格清理过程作者实现了一个系统其中“开放”和“关闭”状态值在每次新搜索时递增2。这意味着先前搜索的节点状态在当前搜索中被有效地视为“未研究”而无需重置 1。
* 变量作用域
局部变量被提升为成员变量,以在堆上一次性创建,避免在栈上重复创建和销毁 1。
* 成本计算
作者选择使用int进行成本和总成本计算舍弃小数。因为虽然浮点数提供了更多细节但它们导致算法更频繁地重新评估节点使其速度慢约10倍而对于复杂地图路径质量没有显著改善 1。
### **其他相关的C\# A\*项目(简要提及)**
除了上述重点项目外GitHub上还有其他值得关注的C\# A\*寻路实现,它们针对特定用例或提供了不同的功能集:
* TheCyaniteProject/PathFinder3D
该项目专注于3D A\*寻路其特点是不需要烘焙导航网格并且可以与动态创建的地形如MapMagic或其他一起使用。这对于需要实时适应变化环境的3D游戏或模拟非常有用 10。
* kbrizov/Pathfinding-Algorithms
这是一个更通用的存储库其中包含各种寻路算法的实现包括A\*。虽然可能没有像roy-t/AStar那样专门针对A\*进行极致优化,但它为学习和比较不同算法提供了有用的资源 11。
* hugoscurti/hierarchical-pathfinding
该项目实现了Unity中的近最优分层寻路HPA\*算法并使用《龙腾世纪起源》的地图进行测试。HPA\*是一种高级技术,通过在不同抽象级别上规划路径来提高大型地图的寻路性能,适用于需要在大规模环境中进行高效导航的场景 10。
表2选定C\# A\*实现的功能与优化比较
| 项目名称 | 主要关注点 | 关键优化技术 | 性能特性 | 内存权衡 | 环境类型 | 许可证 |
| :---- | :---- | :---- | :---- | :---- | :---- | :---- |
| roy-t/AStar | 2D网格和图寻路 | MinHeap、预计算成本、图优先表示 | 10,000节点/40,000边在10ms内 | 低 | 静态 | MIT |
| BlueRaja/High-Speed-Priority-Queue-for-C-Sharp | 高性能优先级队列 | 二叉堆、强制内联、无外部依赖 | 极快降低GC开销 | 低 | 通用 | MIT |
| CodeProject "Fast PathFinder" | 高速网格寻路 | 计算网格、线性数组、2的幂次方网格、忽略旧节点、结构体、整数成本 | 300-1500x加速13MB额外内存 | 高 | 静态 | 自定义 |
| TheCyaniteProject/PathFinder3D | 3D动态地形寻路 | 无需烘焙导航网格 | 实时动态 | 未指定 | 动态 | 未指定 |
| kbrizov/Pathfinding-Algorithms | 通用寻路算法 | 未指定 | 学习/比较 | 未指定 | 未指定 | 未指定 |
| hugoscurti/hierarchical-pathfinding | 分层寻路 (HPA\*) | HPA\*算法 | 大型地图高效寻路 | 未指定 | 静态 | 未指定 |
此表格总结了上述C\# A\*项目的主要特点、优化策略和性能概览,为开发人员在选择适合其特定需求的实现时提供了快速参考。
## **VI. 动态与复杂环境下的A\*变体**
### **标准A\*在动态环境中的局限性**
标准A\*算法主要设计用于静态或已知环境。它在搜索开始时假定所有障碍物、成本和图结构都是已知的且不会改变。当环境发生变化时例如出现新的障碍物、现有障碍物移动或成本发生变化标准A\*算法无法有效适应。它需要重新从头开始计算整个路径这在动态或未知环境中效率极低尤其是在需要频繁重新规划路径的场景中。在多智能体系统中A\*在低密度情况下表现良好,但在高拥堵水平下,特定起始位置可能会出现问题,导致算法卡住并失败,这凸显了其在动态环境中的局限性。
### **D\*与D\* Lite简介**
为了解决标准A\*在动态环境中的局限性开发了D\*算法及其变体如D\* Lite动态A\*。D\*算法是一种寻路算法,用于机器人和自主系统在未知或动态环境中导航。它旨在处理环境变化并相应地重新规划路径,使其成为环境未知或不断变化的应用程序的流行选择。
D\*算法是A\*算法的扩展它结合了前向和后向搜索以高效地重新规划路径。它通过维护一个待处理节点的优先级队列来工作并迭代处理队列中的节点在必要时更新成本并重新规划路径。当环境发生变化时D\*算法通过更新受影响节点的成本来重新规划路径。
D\*及其变体已被广泛用于自主机器人包括火星探测器“机遇号”和“勇气号”。Field D\*是D\*的一种基于插值的变体它将节点定义在网格的角点上并使用线性插值使路径点可以位于网格边的任何位置。这样它可以在非均匀环境中生成直接、低成本和平滑的路径解决了传统网格寻路算法将机器人运动限制在少数几个离散方向例如0、45、90度的问题从而产生非自然、次优的路径。
D\* Lite是D\*算法的简化版本更高效且易于实现。它在自主物体和机器人领域得到了应用。与A\*不同D\* Lite在动态环境中表现出卓越的适应性通过实时重新计算路线在所有测试场景中都成功完成任务而没有失败这表明它适用于需要对环境变化有高成功率的应用。
### **C\# D\* Lite实现**
在GitHub上可以找到D\* Lite的C\#实现例如Bastiantheone/DStarLite \[7。该项目提供了D\* Lite算法的C\#实现,可用于在机器人探索地图时将其导航到目标坐标。该实现假定地图可以表示为具有可导航和不可导航地形的网格。默认情况下,机器人可以向前、向后和侧向移动,但不能对角线移动。不过,通过简单的代码修改,可以允许对角线移动或为不同移动添加不同成本 \[7。
该实现要求创建一个继承自DStarLiteEnvironment接口的类该类负责环境与算法之间的交互包含MoveTo和GetObjectsInVision两个方法。例如TestProgram.cs中提供了如何使用该实现的示例 \[7。
### **选择正确的算法**
选择合适的寻路算法取决于应用程序的具体需求和环境特性:
* 静态与已知环境
如果环境是静态的且所有信息在搜索开始时都已知那么A\*算法通常是最佳选择。它能够找到最优路径,并且通过本报告中讨论的各种优化,可以实现极高的性能。
* 动态与未知环境
如果环境是动态的或者信息是逐步发现的例如机器人探索未知地形那么D\*或D\* Lite等动态寻路算法是更合适的选择。这些算法能够高效地重新规划路径而无需每次环境变化都从头开始计算。D\* Lite因其简化和高效的特性在需要快速适应环境变化的机器人和自主系统应用中特别受欢迎。
* 路径平滑度
对于需要更自然、平滑路径的应用例如自动驾驶Field D\*等基于插值的D\*变体可能更优,因为它们允许路径点位于网格边上的任意位置,从而生成更平滑的轨迹。
## **VII. 实际应用与未来展望**
### **选择最优实现**
选择最优的A\*实现需要根据项目的具体需求进行权衡。没有一个“一刀切”的解决方案,因为不同的优化策略会带来不同的性能特性和资源消耗。
* 网格尺寸与复杂度
对于小型或中型网格即使是标准A\*实现例如Red Blob Games的基线版本在经过优先级队列优化后也可能足够。但对于大型网格例如1024x1024或更大CodeProject的“Fast PathFinder”所展示的计算网格、线性数组和2的幂次方网格尺寸等优化变得至关重要因为它们提供了$O(1)$的节点访问速度,显著减少了查找时间 1。
* 环境动态性
如果环境是完全静态的A\*是理想选择。如果环境会发生变化但变化不频繁或者可以接受短暂的路径重新计算延迟那么一个高度优化的A\*实现仍然可行。然而对于环境持续变化或信息逐步发现的场景例如机器人导航D\*或D\* Lite等动态寻路算法是更好的选择因为它们能够高效地进行路径重新规划。
* 内存约束
一些高性能优化例如CodeProject的“Fast PathFinder”中的计算网格会增加内存消耗。在内存受限的环境例如嵌入式系统或移动设备可能需要权衡速度以减少内存占用。在这种情况下选择内存效率更高的优先级队列例如某些二叉堆实现和紧凑的节点结构变得更为重要 1。
* 路径质量要求
如果路径必须是严格最优的(最短或最低成本),则需要确保启发式函数是可接受且一致的,并且可能需要启用“重新开放关闭节点”等功能,即使这会增加计算时间 1。如果可以接受轻微次优但计算速度更快的路径则可以调整启发式或禁用某些功能以提高性能。
* 开发与维护成本
高度优化的算法通常更复杂开发和维护成本更高。选择一个成熟且文档完善的开源库如roy-t/AStar可以显著降低开发负担同时仍能获得高性能 3。
### **基准测试您的解决方案**
在任何性能关键型应用程序中,对寻路解决方案进行严格的基准测试至关重要。仅凭理论分析或通用基准测试结果不足以保证在特定应用场景下的性能。
* 使用BenchmarkDotNet
C\#生态系统提供了强大的基准测试工具如BenchmarkDotNet。该工具允许开发人员精确测量代码的执行时间、内存分配和CPU使用情况。它能够揭示隐藏的内存成本例如接口调用、Lambda表达式、值类型装箱以及其他看似无害的代码所导致的堆分配 2。通过
BenchmarkDotNet开发人员可以
* **量化优化效果**:精确测量特定优化(例如,切换优先级队列、使用结构体)对性能的影响。
* **识别新的瓶颈**当一个瓶颈被消除后BenchmarkDotNet可以帮助识别下一个性能瓶颈。
* **防止性能回归**:在持续集成/持续部署 (CI/CD) 流程中集成基准测试,可以防止未来的代码更改无意中引入性能下降。
### **调试与可视化**
理解算法行为和识别性能问题需要有效的调试和可视化工具。
* 实时进度显示
CodeProject的“Fast PathFinder”提供了一个前端应用程序可以实时显示算法的运行过程包括节点如何被开放和关闭 1。这种可视化对于理解算法的探索模式和识别低效区域非常有价值。
* 路径可视化
Red Blob Games的实现包含DrawGrid静态方法用于可视化cameFrom数组显示墙壁和路径方向 7。这种功能对于验证路径的正确性和直观地理解算法的输出至关重要。
* 内存分析器
使用C\#的内存分析器例如Visual Studio内置的分析器或JetBrains dotMemory可以帮助识别和解决内存泄漏、过度分配和垃圾回收压力问题。这与使用结构体和优化节点结构等内存管理策略相辅相成。
### **寻路算法的新兴趋势**
寻路领域仍在不断发展,新的研究和技术不断涌现:
* 多智能体寻路 (MAPF)
在许多真实世界场景中多个智能体需要同时找到路径并避免相互碰撞。MAPF算法旨在解决这一复杂问题例如基于冲突搜索 (CBS) 的方法。
* 连续空间寻路
传统的A\*通常在离散网格上操作。然而对于机器人和自动驾驶物体在连续空间中进行寻路以生成更平滑、更自然的轨迹变得越来越重要。Field D\*就是其中一种尝试解决此问题的算法。
* 深度学习与强化学习
人工智能领域的最新进展正在影响寻路。深度学习模型可以学习复杂的环境表示,而强化学习可以训练智能体在动态环境中找到最优策略,尽管这些方法通常需要大量数据和计算资源。
* 分层寻路
对于超大型地图分层寻路算法如HPA\*)通过在不同抽象级别上规划路径来提高效率。首先在高层规划粗略路径,然后在局部细化路径,从而显著减少搜索空间 10。
## **VIII. 结论**
在C\#中实现高性能A\*寻路算法是一个多方面且细致的工程挑战。它超越了对算法基本原理的理解,深入到数据结构选择、内存管理和算法微调的复杂性。
本报告的分析表明A\*性能优化的核心在于**高效的优先级队列**。像二叉堆这样的数据结构,其对数时间复杂度的操作,是实现快速节点插入和提取的基础,从而显著优于简单的基于列表的实现 3。
BlueRaja/High-Speed-Priority-Queue-for-C-Sharp等专门优化的优先级队列库为任何A\*实现提供了关键的性能支撑 6。
其次,**精细的内存管理**至关重要。将节点表示为struct而非class可以显著减少垃圾回收开销并改善CPU缓存局部性 1。CodeProject的“Fast PathFinder”所展示的节点结构紧凑化将节点大小从32字节缩减到13字节进一步体现了内存优化的重要性 1。
第三,对于网格环境,**创新的网格表示和访问模式**带来了巨大的性能飞跃。引入“计算网格”以实现$O(1)$的节点访问结合线性数组和2的幂次方网格尺寸以利用位运算进行坐标转换这些技术共同将寻路速度提高了数百甚至上千倍 1。
最后,**巧妙的算法改进**如“忽略旧节点”策略避免昂贵的优先级队列移除操作和“优化网格清理”避免在连续搜索之间进行耗时的内存重置展示了对算法行为和C\#运行时环境的深刻理解如何转化为显著的性能提升 1。
**最终建议:**
1. **优先级队列为王**始终使用高性能的优先级队列实现如二叉堆而不是C\#中简单的List或SortedList。
2. **拥抱值类型**在可能的情况下将A\*节点定义为struct以减少堆分配和垃圾回收压力从而提高实时应用的响应能力。
3. **网格优化**:对于基于网格的寻路,考虑实现一个直接的“计算网格”以实现$O(1)$访问并探索线性数组和2的幂次方网格尺寸以利用位运算。
4. **精细调整**:根据项目需求仔细选择和调整启发式函数、成本精度、移动规则和“重新开放关闭节点”等参数,以在路径最优性、计算速度和内存消耗之间找到最佳平衡。
5. **环境决定算法**在动态或未知环境中标准A\*的局限性显而易见。在这种情况下应优先考虑D\*或D\* Lite等动态寻路算法它们能够高效地进行路径重新规划。
6. **严格基准测试**使用BenchmarkDotNet等工具对您的寻路解决方案进行系统性基准测试以量化优化效果识别隐藏的性能成本并确保在目标环境中达到预期的性能水平。
通过采纳这些经过验证的优化策略并利用GitHub上可用的高性能C\#实现开发人员可以构建出能够满足最严苛性能需求的A\*寻路系统从而在游戏、机器人和各种AI应用中实现流畅、高效的导航。
#### **引用的著作**
1. A\* algorithm implementation in C\# \- CodeProject, 访问时间为 八月 14, 2025 [https://www.codeproject.com/Articles/15307/A-algorithm-implementation-in-C-](https://www.codeproject.com/Articles/15307/A-algorithm-implementation-in-C-)
2. Is Your C\# Code Fast? Benchmarking to Find Hidden Costs \- YouTube, 访问时间为 八月 14, 2025 [https://www.youtube.com/watch?v=yIRBO7xQ43o](https://www.youtube.com/watch?v=yIRBO7xQ43o)
3. roy-t/AStar: A fast 2D path finding library based on the A ... \- GitHub, 访问时间为 八月 14, 2025 [https://github.com/roy-t/AStar](https://github.com/roy-t/AStar)
4. priority-queue · GitHub Topics, 访问时间为 八月 14, 2025 [https://github.com/topics/priority-queue](https://github.com/topics/priority-queue)
5. EliahKagan/Dijkstra: Visualizing Dijkstra's algorithm with various priority queues \- GitHub, 访问时间为 八月 14, 2025 [https://github.com/EliahKagan/Dijkstra](https://github.com/EliahKagan/Dijkstra)
6. BlueRaja/High-Speed-Priority-Queue-for-C-Sharp: A C ... \- GitHub, 访问时间为 八月 14, 2025 [https://github.com/BlueRaja/High-Speed-Priority-Queue-for-C-Sharp](https://github.com/BlueRaja/High-Speed-Priority-Queue-for-C-Sharp)
7. Implementation of A\* \- Red Blob Games, 访问时间为 八月 14, 2025 [https://www.redblobgames.com/pathfinding/a-star/implementation.html](https://www.redblobgames.com/pathfinding/a-star/implementation.html)
8. Mastering Pathfinding with A-Star: A Practical Guide and C\# Implementation \- Medium, 访问时间为 八月 14, 2025 [https://medium.com/@hanxuyang0826/mastering-pathfinding-with-a-star-a-practical-guide-and-c-implementation-f76f1643d8c3](https://medium.com/@hanxuyang0826/mastering-pathfinding-with-a-star-a-practical-guide-and-c-implementation-f76f1643d8c3)
9. A\* Pathfinding Data Structure \- Stack Overflow, 访问时间为 八月 14, 2025 [https://stackoverflow.com/questions/27832405/a-pathfinding-data-structure](https://stackoverflow.com/questions/27832405/a-pathfinding-data-structure)
10. astar-pathfinding · GitHub Topics · GitHub, 访问时间为 八月 14, 2025 [https://github.com/topics/astar-pathfinding](https://github.com/topics/astar-pathfinding)
11. a-star-path-finding · GitHub Topics, 访问时间为 八月 14, 2025 [https://github.com/topics/a-star-path-finding](https://github.com/topics/a-star-path-finding)
12. Pathfinding Algorithms in C\# \- CodeProject, 访问时间为 八月 14, 2025 [https://www.codeproject.com/Articles/1221034/Pathfinding-Algorithms-in-Csharp](https://www.codeproject.com/Articles/1221034/Pathfinding-Algorithms-in-Csharp)

View File

@ -0,0 +1,837 @@
# Navisworks 插件 GPU 加速可行性研究
## 1. 研究背景与目标
### 1.1 背景
NavisworksTransport 插件当前实现了基于 A* 算法的路径规划和碰撞检测功能。随着模型规模的增长和实时性要求的提高,需要评估 GPU 加速技术在本项目中的可行性和必要性。
### 1.2 研究目标
- 调研 Navisworks API 是否提供 GPU 加速能力
- 评估第三方 .NET GPU 计算库的适用性
- 分析项目中哪些模块可以从 GPU 加速中受益
- 给出实施建议和优先级排序
### 1.3 当前性能瓶颈
根据现有代码分析,主要性能瓶颈包括:
1. **网格地图生成**GridMapGenerator
- BIM 模型几何扫描
- 障碍物边界膨胀算法
- 高度层识别和合并
2. **A* 路径规划**AutoPathFinder
- 3D 图构建(多层网格连接)
- A* 搜索算法执行
- 路径后处理优化
3. **碰撞检测**ClashDetectiveIntegration
- 动画过程中的实时碰撞检测
- 多对象间的碰撞测试
## 2. Navisworks API GPU 能力调研
### 2.1 调研方法
- 网络搜索Navisworks API 官方文档、开发者社区
- 本地文档:检索 Navisworks 2026 .NET API 文档
- 关键词GPU、hardware acceleration、parallel、multi-thread、compute shader
### 2.2 调研结果
#### 2.2.1 Navisworks API 线程模型
根据 Autodesk 官方论坛的讨论:
> "Navisworks C# API is basically, as far as I understand, single-threaded/not thread safe"
**结论**Navisworks .NET API 是**单线程的**,不支持多线程并发访问。
#### 2.2.2 硬件加速支持情况
Navisworks 支持通过用户界面启用硬件加速:
- **位置**Interface > Display > Hardware Acceleration
- **用途**:仅用于图形渲染加速
- **限制**:不是 API 层面的计算加速,插件无法直接调用
#### 2.2.3 GPU 系统要求
Navisworks 2026 的 GPU 要求:
- **基本要求**2 GB GPU29 GB/s 带宽DirectX 11 兼容
- **推荐配置**8 GB GPU106 GB/s 带宽DirectX 12 兼容
#### 2.2.4 API 文档搜索结果
在本地 Navisworks API 文档中搜索以下关键词:
```bash
GPU|hardware.*acceleration|parallel|multi.*thread|compute.*shader
```
**结果**:未找到任何相关 API 文档。
### 2.3 结论
**Navisworks API 本身不提供 GPU 加速接口**。所有 GPU 相关的功能仅限于内部渲染引擎,不对插件开发者开放。
## 3. 第三方 .NET GPU 计算库
### 3.1 可用方案概览
虽然 Navisworks API 不提供 GPU 接口,但可以通过集成第三方 .NET GPU 计算库来实现 GPU 加速。
### 3.2 ILGPU推荐
#### 基本信息
- **官网**https://ilgpu.net/
- **许可证**MIT License
- **支持平台**.NET Framework 4.8, .NET Core, .NET 5+
#### 主要特点
1. **多厂商支持**
- NVIDIA CUDA
- AMD ROCm
- Intel 集成显卡
- CPU 回退模式(无 GPU 时自动使用 CPU
2. **C# 原生编程**
```csharp
// 示例GPU 并行计算
var accelerator = new CudaAccelerator(new CudaAcceleratorId(0));
var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1, ArrayView<int>>(MyKernel);
static void MyKernel(Index1 index, ArrayView<int> data)
{
data[index] = index * 2; // GPU 上执行
}
```
3. **高层抽象**
- 无需编写 CUDA C++ 代码
- 自动内存管理
- 类型安全
4. **性能**
- 接近手写 CUDA 的性能90-95%
- 支持 Shared Memory、Atomic 操作
- 内置性能分析工具
#### 适用场景
- ✅ 大规模并行计算(数组操作、矩阵运算)
- ✅ 需要跨 GPU 厂商支持的项目
- ✅ 希望纯 C# 开发的团队
### 3.3 ManagedCUDA
#### 基本信息
- **GitHub**https://github.com/kunzmi/managedCuda
- **许可证**LGPL / 商业许可
- **支持平台**.NET Framework, .NET Core
#### 主要特点
1. **CUDA 工具包的 .NET 包装器**
- 直接映射 CUDA C API
- 完整的 CUDA 功能访问
- 需要安装 NVIDIA CUDA Toolkit
2. **性能**
- 接近原生 CUDA 性能98-100%
- 直接控制内存分配和传输
- 支持 CUDA Streams、Events
3. **限制**
- **仅支持 NVIDIA GPU**
- 学习曲线较陡(需要理解 CUDA 编程模型)
- 需要手动管理内存和资源
#### 适用场景
- ✅ 仅面向 NVIDIA GPU 用户
- ✅ 需要最大化 GPU 性能
- ✅ 团队有 CUDA 编程经验
### 3.4 DirectCompute
#### 基本信息
- **提供商**Microsoft
- **API**DirectX 11/12 Compute Shader
- **支持平台**Windows
#### 主要特点
1. **跨厂商支持**
- 所有支持 DirectX 11+ 的 GPU
- 与图形管线集成
2. **限制**
- .NET 集成复杂(需要 P/Invoke 或 SharpDX
- 需要编写 HLSL Compute Shader
- 文档和社区支持相对较少
#### 适用场景
- ⚠️ 需要与 DirectX 图形深度集成
- ⚠️ 不推荐作为首选方案(开发复杂度高)
### 3.5 方案对比
| 特性 | ILGPU | ManagedCUDA | DirectCompute |
|------|-------|-------------|---------------|
| **GPU 支持** | NVIDIA + AMD + Intel | 仅 NVIDIA | 所有 DX11+ GPU |
| **开发语言** | 纯 C# | C# + CUDA C | C# + HLSL |
| **学习曲线** | 低-中 | 中-高 | 高 |
| **性能** | 90-95% | 98-100% | 85-95% |
| **社区支持** | 活跃 | 中等 | 较少 |
| **推荐度** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ |
**推荐**:对于 NavisworksTransport 项目,**ILGPU 是最佳选择**,原因:
1. 跨厂商支持(用户可能使用 AMD 或 Intel GPU
2. 纯 C# 开发,与现有代码库一致
3. 降低技术门槛,易于团队掌握
4. CPU 回退模式,无 GPU 时仍可运行
## 4. 应用场景分析
### 4.1 方案 AA* 路径规划 GPU 加速
#### 4.1.1 数据流设计
```
┌─────────────────────┐
│ Navisworks API │
│ 提取 BIM 模型数据 │
└──────────┬──────────┘
┌─────────────────────┐
│ GridMapGenerator │
│ 生成网格地图 (CPU) │
└──────────┬──────────┘
┌─────────────────────┐
│ 数据传输到 GPU │
│ - 网格数据 │
│ - 障碍物信息 │
│ - 起终点列表 │
└──────────┬──────────┘
┌─────────────────────┐
│ GPU Kernel │
│ 并行 A* 搜索 │
│ - 多起终点同时计算 │
│ - 共享网格数据 │
└──────────┬──────────┘
┌─────────────────────┐
│ 结果传回 CPU │
│ - 路径点列表 │
│ - 路径成本 │
└──────────┬──────────┘
┌─────────────────────┐
│ PathOptimizer │
│ 路径后处理 (CPU) │
└─────────────────────┘
```
#### 4.1.2 关键技术点
1. **网格数据结构 GPU 化**
```csharp
// CPU 端(当前)
public class GridCell
{
public List<HeightLayer> HeightLayers { get; set; }
public bool IsInChannel { get; set; }
// ...
}
// GPU 端(需要转换为平面数组)
struct GPUGridCell
{
public int LayerCount;
public int LayerStartIndex; // 指向 HeightLayer 数组的索引
public bool IsInChannel;
}
struct GPUHeightLayer
{
public float Z;
public float MinPassableZ;
public float MaxPassableZ;
public bool IsWalkable;
public float SpeedLimit;
}
```
2. **并行 A* 算法实现**
- 每个 GPU 线程处理一个起终点对
- 使用 GPU Shared Memory 存储 Open/Close 集合
- 需要处理原子操作(更新最优路径)
3. **数据传输优化**
- 使用 Pinned Memory 减少传输延迟
- 批量处理多个路径请求
- 缓存不变的网格数据
#### 4.1.3 适用场景判断
**适合 GPU 加速的情况**
- ✅ 网格规模 > 100m × 100m> 40,000 单元格)
- ✅ 同时计算 10+ 条路径
- ✅ 实时路径重规划(动态障碍物)
- ✅ 多楼层复杂场景3D 图节点数 > 100,000
**不适合 GPU 加速的情况**
- ❌ 小规模网格 < 50m × 50m< 10,000 单元格
- ❌ 单次路径计算
- ❌ 简单平面场景2D A*
- ❌ 数据传输时间 > 计算时间
#### 4.1.4 性能预估
假设场景100m × 100m 网格0.5m 单元格3 层楼
| 指标 | CPU (RoyT.AStar) | GPU (ILGPU 估算) | 加速比 |
|------|------------------|------------------|--------|
| 单次路径 | 50-100 ms | 80-120 ms | **0.6-0.8×** ❌ |
| 10 条路径 | 500-1000 ms | 100-150 ms | **5-8×** ✅ |
| 100 条路径 | 5-10 秒 | 300-500 ms | **15-25×** ✅ |
**结论**:仅在批量路径计算时才有显著收益。
### 4.2 方案 B碰撞检测 GPU 加速
#### 4.2.1 数据流设计
```
┌─────────────────────┐
│ Navisworks API │
│ 提取模型几何 │
└──────────┬──────────┘
┌─────────────────────┐
│ GeometryExtractor │
│ 获取 BoundingBox │
└──────────┬──────────┘
┌─────────────────────┐
│ 数据传输到 GPU │
│ - 对象包围盒 │
│ - 对象位置/旋转 │
│ - 碰撞检测对列表 │
└──────────┬──────────┘
┌─────────────────────┐
│ GPU Kernel │
│ 并行碰撞检测 │
│ - AABB 相交测试 │
│ - OBB 相交测试 │
└──────────┬──────────┘
┌─────────────────────┐
│ 结果传回 CPU │
│ - 碰撞对列表 │
│ - 碰撞时间戳 │
└─────────────────────┘
```
#### 4.2.2 关键技术点
1. **包围盒数据结构**
```csharp
struct GPUAABB
{
public Vector3 Min;
public Vector3 Max;
}
struct GPUCollisionPair
{
public int ObjectA;
public int ObjectB;
public bool IsColliding;
public float PenetrationDepth;
}
```
2. **并行碰撞检测算法**
- 每个 GPU 线程处理一对对象
- N 个对象 = N×(N-1)/2 个线程
- 使用空间分区Grid-based减少检测对数
3. **动画过程集成**
- 每帧更新对象位置到 GPU
- 实时返回碰撞结果
- 与 Navisworks TimeLiner 同步
#### 4.2.3 适用场景判断
**适合 GPU 加速的情况**
- ✅ 对象数量 > 100
- ✅ 实时动画碰撞检测30+ FPS
- ✅ 动态场景(频繁更新位置)
- ✅ 全对全检测N² 复杂度)
**不适合 GPU 加速的情况**
- ❌ 对象数量 < 50
- ❌ 静态场景(预计算即可)
- ❌ 已有空间索引优化(如八叉树)
#### 4.2.4 性能预估
假设场景200 个动态物体
| 指标 | CPU (Navisworks API) | GPU (ILGPU 估算) | 加速比 |
|------|----------------------|------------------|--------|
| 单帧碰撞检测 | 50-100 ms | 5-10 ms | **8-15×** ✅ |
| 30 FPS 动画 | 无法实时 | 实时 | **显著改善** ✅ |
**结论**:碰撞检测是最适合 GPU 加速的场景。
## 5. 实施评估
### 5.1 工作量估算
| 任务 | 工作量 | 复杂度 | 依赖 |
|------|--------|--------|------|
| ILGPU 库集成与环境搭建 | 1-2 天 | 低 | - |
| 网格数据结构 GPU 化 | 2-3 天 | 中 | GridMapGenerator |
| GPU A* 内核实现 | 5-7 天 | 高 | 并行算法设计 |
| GPU 碰撞检测内核实现 | 3-5 天 | 中-高 | GeometryExtractor |
| CPU-GPU 数据传输优化 | 2-3 天 | 中 | 内存管理 |
| 性能测试与调优 | 3-5 天 | 中 | 测试场景 |
| 错误处理与回退机制 | 2-3 天 | 中 | 异常处理 |
| **总计** | **18-28 天** | **高** | - |
### 5.2 技术风险
1. **GPU 内存限制**
- 风险:大规模网格数据可能超出 GPU 显存
- 缓解:分块处理、数据压缩
2. **用户硬件支持**
- 风险:部分用户无独立 GPU
- 缓解ILGPU CPU 回退模式
3. **数据传输开销**
- 风险:频繁传输抵消 GPU 加速收益
- 缓解:数据缓存、批量处理
4. **Navisworks API 线程安全**
- 风险GPU 计算结果需要回 UI 线程
- 缓解:使用 Dispatcher.Invoke
### 5.3 维护成本
- **代码复杂度增加**:需要维护 CPU 和 GPU 两套代码路径
- **测试覆盖**:需要覆盖不同 GPU 厂商和型号
- **用户支持**:增加 GPU 驱动相关的技术支持成本
## 6. 优化建议与优先级
### 6.1 当前项目性能分析
基于现有代码分析:
- **网格生成**50-200 ms取决于模型规模
- **A* 搜索**20-100 ms单次路径
- **路径优化**10-30 ms
- **总耗时**80-330 ms
**主要瓶颈**网格生成BIM 几何扫描),而非 A* 搜索。
### 6.2 优化优先级排序
#### 🔥 第一优先级CPU 层面优化(高投入产出比)
**1. 网格生成优化**
```csharp
// 当前实现:逐个网格扫描
for (int x = 0; x < gridMap.Width; x++)
{
for (int y = 0; y < gridMap.Height; y++)
{
// 扫描所有模型元素
foreach (var item in allItems)
{
if (Intersects(x, y, item)) { ... }
}
}
}
// 优化方案:空间索引(八叉树/R-树)
var spatialIndex = BuildRTree(allItems); // 预处理一次
for (int x = 0; x < gridMap.Width; x++)
{
for (int y = 0; y < gridMap.Height; y++)
{
var nearbyItems = spatialIndex.Query(gridCell); // O(log n)
// 仅检查附近元素
}
}
```
**预期收益**:网格生成速度提升 **5-10×**
**工作量**3-5 天
---
**2. A* 启发式函数优化**
```csharp
// 当前:简单欧几里得距离
public float Heuristic(Position a, Position b)
{
return Vector3.Distance(a, b);
}
// 优化:考虑高度变化成本
public float Heuristic(Position a, Position b)
{
float horizontalDist = Vector2.Distance(a.XY, b.XY);
float verticalDist = Math.Abs(a.Z - b.Z);
// 垂直移动成本更高(楼梯/电梯)
return horizontalDist + verticalDist * 2.0f;
}
```
**预期收益**:搜索节点数减少 **20-40%**
**工作量**1-2 天
---
**3. 路径缓存机制**
```csharp
// 缓存常用路径
public class PathCache
{
private Dictionary<(Point3D, Point3D), List<PathPoint>> cache;
public List<PathPoint> GetPath(Point3D start, Point3D end)
{
var key = (start, end);
if (cache.ContainsKey(key))
{
return cache[key]; // 命中缓存
}
var path = ComputePath(start, end);
cache[key] = path;
return path;
}
}
```
**预期收益**:重复路径计算速度提升 **100×**
**工作量**2-3 天
---
**4. CPU 多线程优化**
```csharp
// 网格生成并行化
Parallel.For(0, gridMap.Height, y =>
{
for (int x = 0; x < gridMap.Width; x++)
{
ProcessGridCell(x, y);
}
});
// 多路径并行计算
var paths = Parallel.ForEach(pathRequests, request =>
{
return ComputePath(request.Start, request.End);
});
```
**预期收益**:网格生成和多路径计算速度提升 **2-4×**(取决于 CPU 核心数)
**工作量**3-5 天
**注意**:需要处理 Navisworks API 线程安全问题(数据提取在主线程,计算在工作线程)
---
#### ⚠️ 第二优先级:数据结构与缓存优化(中投入产出比)
**1. 网格数据结构优化**
```csharp
// 当前List<HeightLayer>(动态分配)
public class GridCell
{
public List<HeightLayer> HeightLayers { get; set; } // 堆分配
}
// 优化:固定大小数组或栈分配
public struct GridCell
{
public const int MaxLayers = 8;
public HeightLayer Layer0, Layer1, ..., Layer7; // 栈分配
public int LayerCount;
}
```
**预期收益**:内存分配减少 **50-80%**GC 压力降低
**工作量**5-7 天(涉及大量代码修改)
---
**2. 增量式网格更新**
```csharp
// 当前:每次全量重建网格
public void UpdateGrid()
{
gridMap = new GridMap(); // 全量重建
GenerateFromBIM(...);
}
// 优化:仅更新变化区域
public void UpdateGrid(IEnumerable<ModelItem> changedItems)
{
foreach (var item in changedItems)
{
var affectedCells = GetAffectedCells(item);
foreach (var cell in affectedCells)
{
RegenerateCell(cell); // 局部更新
}
}
}
```
**预期收益**:动态场景更新速度提升 **10-50×**
**工作量**4-6 天
---
#### 🚀 第三优先级GPU 加速(高投入,场景受限)
**实施条件**(必须同时满足):
1. ✅ 已完成 CPU 层面所有优化
2. ✅ CPU 优化后仍存在性能瓶颈
3. ✅ 存在批量计算需求(多路径/多碰撞)
4. ✅ 目标用户群体有独立 GPU
5. ✅ 团队有足够的开发和维护资源
**推荐实施顺序**
1. **先实施**:碰撞检测 GPU 加速(收益最明显)
2. **后实施**A* 路径规划 GPU 加速(仅在批量场景)
**工作量**18-28 天
---
### 6.3 综合建议
#### 短期1-2 周)
1. ✅ 实施空间索引优化R-树/八叉树)
2. ✅ 实施路径缓存机制
3. ✅ 优化 A* 启发式函数
**预期效果**:整体性能提升 **3-5×**,工作量 **6-10 天**
#### 中期1-2 月)
1. ✅ CPU 多线程优化(网格生成、多路径计算)
2. ✅ 数据结构优化(减少堆分配)
3. ✅ 增量式网格更新
**预期效果**:再提升 **2-3×**,工作量 **12-18 天**
#### 长期3-6 月)
1. ⚠️ **评估是否需要 GPU 加速**
- 如果 CPU 优化后仍不满足需求 → 实施碰撞检测 GPU 加速
- 如果存在大批量路径计算需求 → 实施 A* GPU 加速
2. ⚠️ **GPU 加速实施**
- 优先:碰撞检测(工作量 8-12 天)
- 次要A* 路径规划(工作量 10-16 天)
**预期效果**:特定场景下再提升 **5-15×**,工作量 **18-28 天**
---
### 6.4 投入产出比对比
| 优化方向 | 工作量 | 复杂度 | 性能提升 | 通用性 | 投入产出比 |
|---------|--------|--------|---------|--------|------------|
| 空间索引 | 3-5 天 | 中 | 5-10× | 高 | ⭐⭐⭐⭐⭐ |
| 路径缓存 | 2-3 天 | 低 | 100× (重复路径) | 高 | ⭐⭐⭐⭐⭐ |
| 启发式优化 | 1-2 天 | 低 | 1.2-1.5× | 高 | ⭐⭐⭐⭐ |
| CPU 多线程 | 3-5 天 | 中-高 | 2-4× | 中 | ⭐⭐⭐⭐ |
| 数据结构优化 | 5-7 天 | 高 | 1.2-1.5× | 高 | ⭐⭐⭐ |
| 增量更新 | 4-6 天 | 高 | 10-50× (动态场景) | 中 | ⭐⭐⭐ |
| GPU 碰撞检测 | 8-12 天 | 高 | 8-15× | 低 | ⭐⭐ |
| GPU A* | 10-16 天 | 极高 | 5-25× (批量) | 极低 | ⭐ |
---
## 7. 技术选型建议
### 7.1 如果决定实施 GPU 加速
**推荐技术栈**
- **GPU 计算库**ILGPU跨厂商支持纯 C# 开发)
- **首选场景**:碰撞检测(收益最明显)
- **次选场景**A* 路径规划(仅在批量计算时)
**架构设计**
```
┌─────────────────────────────────────────┐
│ NavisworksTransport 插件 │
├─────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ │
│ │ CPU Path │ │ GPU Path │ │
│ │ (默认) │ ←→ │ (可选) │ │
│ └─────────────┘ └─────────────┘ │
│ ↑ ↑ │
│ └───────┬───────────┘ │
│ ↓ │
│ ┌─────────────┐ │
│ │ Accelerator │ │
│ │ Selector │ │
│ └─────────────┘ │
│ ↓ │
│ 检测 GPU 可用性 │
│ - 有 GPU → GPU Path │
│ - 无 GPU → CPU Path (回退) │
└─────────────────────────────────────────┘
```
**配置选项**(添加到 `config.toml`
```toml
[performance]
# 性能优化选项
enable_gpu_acceleration = true # 是否启用 GPU 加速
gpu_fallback_to_cpu = true # GPU 不可用时回退到 CPU
spatial_index_type = "rtree" # 空间索引类型rtree, octree, none
enable_path_cache = true # 是否启用路径缓存
max_cached_paths = 1000 # 最大缓存路径数
```
---
### 7.2 如果不实施 GPU 加速
**推荐优化路线**(按优先级):
1. ✅ **第一阶段**1 周):空间索引 + 路径缓存 + 启发式优化
2. ✅ **第二阶段**2 周CPU 多线程优化
3. ✅ **第三阶段**2-3 周):数据结构优化 + 增量更新
**预期效果**:整体性能提升 **5-15×**,无需 GPU 硬件要求。
---
## 8. 参考资料
### 8.1 技术文档
- [ILGPU 官方文档](https://ilgpu.net/)
- [ManagedCUDA GitHub](https://github.com/kunzmi/managedCuda)
- [Navisworks .NET API 开发者指南](doc/navisworks_api/NET/documentation/)
### 8.2 研究来源
- Autodesk Navisworks 开发者社区
- Navisworks API 2026 本地文档
- NVIDIA CUDA 编程指南
- Microsoft DirectCompute 文档
### 8.3 相关设计文档
- [A* 寻路优化方案](C# A_ 寻路优化_.md)
- [自动路径规划设计方案](PATHFINDING_DESIGN.md)
- [A* 库的使用方法](AStar库的使用方法.md)
---
## 9. 结论
### 9.1 核心发现
1. **Navisworks API 不提供 GPU 加速接口**,但可通过第三方库实现。
2. **当前项目的主要瓶颈在网格生成,而非 A* 搜索**
3. **CPU 层面优化的投入产出比远高于 GPU 加速**
4. **GPU 加速仅在特定场景(批量计算、大规模数据)下有显著收益**
### 9.2 最终建议
**不建议立即实施 GPU 加速**,理由:
1. ✅ CPU 层面有大量优化空间未开发
2. ✅ 投入产出比更高的优化方案可优先实施
3. ✅ GPU 加速适用场景有限(批量计算)
4. ✅ 增加维护成本和技术复杂度
**建议优化路线**
```
短期 (1-2周) → 空间索引 + 路径缓存 + 启发式优化
↓ (性能提升 3-5×)
中期 (1-2月) → CPU 多线程 + 数据结构优化
↓ (再提升 2-3×)
长期 (3-6月) → 评估是否需要 GPU 加速
↓ (如需要)
→ 优先碰撞检测 GPU 加速
→ 次要 A* GPU 加速
```
### 9.3 重新评估触发条件
建议在以下情况下重新评估 GPU 加速方案:
1. ✅ CPU 层面所有优化已完成
2. ✅ 性能仍不满足需求(如单次路径计算 > 500ms
3. ✅ 出现批量路径计算需求10+ 条同时计算)
4. ✅ 用户群体确认有独立 GPU 硬件
5. ✅ 团队有足够资源进行开发和维护
---
**文档版本**v1.0
**创建日期**2025-10-12
**最后更新**2025-10-12
**作者**NavisworksTransport 开发团队

Some files were not shown because too many files have changed in this diff Show More