Compare commits
598 Commits
main
...
codex/rail
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a318f4dca | |||
| 454f1e6584 | |||
| 28d0708858 | |||
| 1a9d0972f0 | |||
| fdb857af17 | |||
| 6f037a4571 | |||
| b849bd9aed | |||
| 61e6b79299 | |||
| 315fa46043 | |||
| 970d3465f4 | |||
| 7e1931fd37 | |||
| bbd72345c9 | |||
| 18b530ef5f | |||
| a252dad46a | |||
| 5a03c3aa6c | |||
| 1a9c9ecd21 | |||
| da5f8a1ae1 | |||
| 6020bfa979 | |||
| 144ee4e6b2 | |||
| 4fbab52880 | |||
| 3892ddabab | |||
| 405f721811 | |||
| 042f30bf87 | |||
| 8a7563a4fe | |||
| 348b3a75bf | |||
| fb9dac9953 | |||
| e21e934705 | |||
| 7058e5fd23 | |||
| 9be5d250e8 | |||
| 4ec4cf77ee | |||
| 24951d4205 | |||
| 9e8fea3241 | |||
| 7ff510daf0 | |||
| 953306fdb1 | |||
| e71ea81cdf | |||
| 2e0b26e032 | |||
| c3b103c159 | |||
| c9744699b1 | |||
| d4c49fc227 | |||
| cb56737041 | |||
| 1d71d36e5f | |||
| 440c41d1d9 | |||
| 9093374399 | |||
| 766b5af887 | |||
| 382923fa32 | |||
| 6214cd4397 | |||
| f4735b164e | |||
| 7851e6affa | |||
| 115d70db66 | |||
| e4788c17a3 | |||
| 95500c9717 | |||
| 7db5a7a201 | |||
| a5d1db6416 | |||
| 9f1924a797 | |||
| ad8d86e79f | |||
| 2b6f54898a | |||
| 3a5693a453 | |||
| 921dc07856 | |||
| aabf263f0d | |||
| 029c7e37ad | |||
| 138eb43a67 | |||
| 6c74ea1319 | |||
| 290df34dec | |||
| 8d10f959b2 | |||
| 48de5aa921 | |||
| eaf24420b7 | |||
| d52b1aef08 | |||
| 1cf4fe5967 | |||
| e9128282b5 | |||
| 93ad55c725 | |||
| 70aaff10bf | |||
| b1d4170334 | |||
| 1802eda971 | |||
| 5e7f9bdf51 | |||
| 8f55b5e9a1 | |||
| 042b9a2804 | |||
| 7ccce8cf5a | |||
| c53db7a6fd | |||
| 3fd184934b | |||
| a651b69459 | |||
| c9a926356c | |||
| 015a2863ea | |||
| 5e8c50e043 | |||
| 5847ba9ab3 | |||
| 9adf9b5baf | |||
| a55ef42f71 | |||
| 0c83c73917 | |||
| 8b58f2853e | |||
| f596935c76 | |||
| e495ea4620 | |||
| 8b4afcfe04 | |||
| 3b801c6bd4 | |||
| 0f669fd995 | |||
| 486caf1c38 | |||
| 07b30a4fb5 | |||
| dc29c3526e | |||
| 0fbff5f114 | |||
| 6e6932394a | |||
| 97296085f5 | |||
| 759a41fb7e | |||
| 2b151ebee5 | |||
| cb7e1c0e17 | |||
| 369605e12f | |||
| dda04250b0 | |||
| 62548197b3 | |||
| edcbd0802b | |||
| ae845bc571 | |||
| c910ab3ae7 | |||
| cc14e8c41d | |||
| 1a959ceb9d | |||
| 27b4e546a2 | |||
| 1047c82c66 | |||
| 11f09faa13 | |||
| 65939f262c | |||
| 5c0e72ec98 | |||
| 0262a413dd | |||
| 6e09961069 | |||
| 5ca793e093 | |||
| 3f0b42770d | |||
| 507339d4f7 | |||
| d336f8302a | |||
| 69765e092b | |||
| f9df83ba5b | |||
| 76d277b6c2 | |||
| 7fbbcdd448 | |||
| 7da2ddf230 | |||
| 9751287884 | |||
| dc1380d7fe | |||
| 749eb07cef | |||
| eec957ad2e | |||
| 1b26e7b640 | |||
| ac6d3b4017 | |||
| fac4d3e2e3 | |||
| a304191b13 | |||
| 08b4ecc5d5 | |||
| 57751e85fa | |||
| 178aa995ff | |||
| 2fb883ae18 | |||
| fdcd8edfc0 | |||
| 8378872b8d | |||
| a9ccc057d3 | |||
| 3138b73a5f | |||
| 77b9da40fc | |||
| b36d20933e | |||
| 98c55045ad | |||
| c7781b15e4 | |||
| 579434fbff | |||
| 64c6079011 | |||
| fdb11d2119 | |||
| bdb3ed23db | |||
| 7ccb0ef6b0 | |||
| ee33e43048 | |||
| cf5aed422b | |||
| 1ffe567466 | |||
| c28f9762bc | |||
| 78158c1cb1 | |||
| 9a7a2da3c6 | |||
| e221d42812 | |||
| 322523bc77 | |||
| 77a49265aa | |||
| 5fdccf55c5 | |||
| 71fe79a447 | |||
| a655e8092c | |||
| d9da8e95ce | |||
| 84e5abf592 | |||
| 6be1de113e | |||
| a94d1c35b7 | |||
| d9c362dcc3 | |||
| 333298c902 | |||
| a7e1411fd6 | |||
| b7cdb72ff7 | |||
| 9e2ce0a3bc | |||
| 8e3759adea | |||
| 39749dcf3d | |||
| c0333810ec | |||
| 1370ce1fd2 | |||
| a0651d60cd | |||
| 0d9eeed89b | |||
| eeeaceacc4 | |||
| 5b6c8906aa | |||
| 1d170cb917 | |||
| 8f0a363c9a | |||
| dcf90a34fd | |||
| 0d241532c9 | |||
| f9f8b5f9aa | |||
| 2930ea71da | |||
| 971c993bb7 | |||
| 4d28ec5547 | |||
| a305ba31e1 | |||
| cb56910d68 | |||
| 0be608be82 | |||
| 438b000296 | |||
| 1d3f46dd00 | |||
| b1461436ff | |||
| dcbeabedf1 | |||
| b09564c4aa | |||
| da89105269 | |||
| 60bbd468d8 | |||
| 7671360454 | |||
| 289fb2016b | |||
| 5fb18b5869 | |||
| bc39552ed2 | |||
| cdd6ee9319 | |||
| 9fac32741e | |||
| dee5f1c834 | |||
| 6498df1818 | |||
| c3cbaaf6e0 | |||
| 0ee85bba40 | |||
| 3a79f0e50e | |||
| c24bff692e | |||
| 5836da25f5 | |||
| 4069668a2a | |||
| 4aa62864f3 | |||
| eeade5553f | |||
| 0869e32ccb | |||
| d1ae5c83f4 | |||
| ec135d5469 | |||
| ba51389df3 | |||
| b99d4e82b6 | |||
| d5cf6b2f8b | |||
| c1e20bf4ee | |||
| 1e277a4bc3 | |||
| eef2d6b01c | |||
| 8a3d480a0a | |||
| 423290bfd1 | |||
| 286228c6e7 | |||
| f8e7f046ce | |||
| e0d598a070 | |||
| a2018576e5 | |||
| 7f72ad5935 | |||
| 82c523a3db | |||
| d0ade35042 | |||
| 1e1d740aef | |||
| d5d50d7568 | |||
| de55d7ea1b | |||
| c117e0d0c3 | |||
| b4a73e2081 | |||
| 6dc266e526 | |||
| 3905db56bc | |||
| 4c34340fdb | |||
| 6f5afdd50b | |||
| 8398f0530a | |||
| af4b1538ab | |||
| f501b40dc4 | |||
| 58e7b858f1 | |||
| 52ca335e9f | |||
| e2fb22efe6 | |||
| 66353f9621 | |||
| b42089d491 | |||
| 1f43d919e4 | |||
| 91c8f5c6f7 | |||
| 131dfba768 | |||
| 366d9309f2 | |||
| 50814b3f99 | |||
| be9131b89f | |||
| ca503c0448 | |||
| b6dd1ca61b | |||
| 582ccfe1d6 | |||
| a235a65a20 | |||
| a52d8fdacb | |||
| 405ea5ac7c | |||
| 35cbd41a9a | |||
| ea3e02cfba | |||
| 668073b100 | |||
| bc1ad3f4c3 | |||
| ddf44b887f | |||
| 1a219b7b0e | |||
| 6dcd27708d | |||
| 1813c9831d | |||
| 07f8f5b2bf | |||
| f4fda4e308 | |||
| 519382f375 | |||
| 760786d9b1 | |||
| fcc87b2cb0 | |||
| c4cf718502 | |||
| cf2e5a4d2f | |||
| 0cdae60a00 | |||
| 779d043299 | |||
| b1da586b27 | |||
| 99d2690800 | |||
| 9ae4acdb03 | |||
| 708bf533f1 | |||
| 8424503576 | |||
| 0a2e29cee9 | |||
| 075fd5c602 | |||
| 001b45cc9a | |||
| 98c23b986e | |||
| 1a2312f3f6 | |||
| df2c09a167 | |||
| a40e52f538 | |||
| 42481a5edc | |||
| ee1b0cbe32 | |||
| 40fce35bc8 | |||
| 97bf6dbecd | |||
| a3bee9a0ba | |||
| f500861179 | |||
| 0de096aed0 | |||
| d473065025 | |||
| c044be57c0 | |||
| ffac0ae146 | |||
| 4f7935499d | |||
| f02e5cfc28 | |||
| cbc63809f0 | |||
| 792d6d249c | |||
| e3958affb7 | |||
| fd05ffce3c | |||
| aa0fdc2cec | |||
| be174ab6bb | |||
| 28c6f18f47 | |||
| 6fab2b3432 | |||
| 2d2e60c58b | |||
| 7fc62537d1 | |||
| 409b39ce78 | |||
| 3bae0f0274 | |||
| 6cc48c3500 | |||
| 174749e287 | |||
| 0702cc879a | |||
| 13e08faa8a | |||
| ec3ef5b30e | |||
| 0081015d0b | |||
| 10f408e361 | |||
| 33296c7415 | |||
| aa0557c9e6 | |||
| 93135d3c29 | |||
| 7c319b199f | |||
| f3a07eb482 | |||
| 0c1de9b45d | |||
| 0d2a240499 | |||
| 736e6e8448 | |||
| dab8dc34c3 | |||
| 5647ae9134 | |||
| ccdada3aad | |||
| d9c2ec8c12 | |||
| d63896bf63 | |||
| 2bd117ff8a | |||
| adde6cbdf4 | |||
| 2a8425d529 | |||
| 7446431f9c | |||
| e5b8501a63 | |||
| 23801726ab | |||
| a19e5f91f3 | |||
| 941bade44a | |||
| 393e1c7291 | |||
| 4736372552 | |||
| 01f200ca60 | |||
| 12616629b0 | |||
| f8320066c1 | |||
| a832e91b7b | |||
| a552ea3a1d | |||
| 18d6fa65fe | |||
| 221e13cb5a | |||
| 1cf5816cb4 | |||
| 811b815082 | |||
| ca4488dcb8 | |||
| fee00dfb82 | |||
| 5e1e4b04b2 | |||
| 330f6591a2 | |||
| 80975b829a | |||
| fbe46ebc85 | |||
| b0a63409b9 | |||
| dba1f76f0a | |||
| 55fbcbdb48 | |||
| 9c97633411 | |||
| fb13b81259 | |||
| 5791e57192 | |||
| 3aaa176ce6 | |||
| b7a112354d | |||
| b05bb727c6 | |||
| 821725d406 | |||
| 40072e2eb7 | |||
| 02ddc812cf | |||
| 1fb6c565fd | |||
| f194a835ed | |||
| 320dfa23f3 | |||
| 2c6187a674 | |||
| f761d93676 | |||
| c7f6586fa9 | |||
| 360d55ffa8 | |||
| 8cec18a141 | |||
| d889635c1c | |||
| 687b342e0f | |||
| bd7eeb3d46 | |||
| 882f8283b5 | |||
| 5cf9336a9c | |||
| 0732cb493f | |||
| 3343f6f5c1 | |||
| 1a3d1e7f49 | |||
| ca8bcc0bba | |||
| 89722efaca | |||
| 07bd9351bf | |||
| aaebbcad21 | |||
| 2e583cb9b6 | |||
| fec15d0805 | |||
| 543479ee65 | |||
| a0a667d45d | |||
| e9e0d8c83f | |||
| a1d2a65010 | |||
| e12e1125d2 | |||
| 51be24161d | |||
| 2e9b9fe2b3 | |||
| 27908540c2 | |||
| 2f78b4e58e | |||
| 1e11f60042 | |||
| b0b29c581c | |||
| 6460dda879 | |||
| 805814616a | |||
| f64e79d372 | |||
| 40946091dd | |||
| 064945bfa6 | |||
| aece9fbbe1 | |||
| c9ca6b4d32 | |||
| 0a61057476 | |||
| 2b0b13c43a | |||
| dd991d38ce | |||
| 455450726c | |||
| 37f03362c4 | |||
| a938afd946 | |||
| 7343133f12 | |||
| a46568f43e | |||
| df1885a352 | |||
| 83a4a0e7aa | |||
| 2464f17092 | |||
| 8b5e2baf23 | |||
| 9ea89aa8d0 | |||
| a4eaf46723 | |||
| 59ecebebc4 | |||
| 3cc840b183 | |||
| 163986f9e5 | |||
| 88712cc156 | |||
| ed7bc13866 | |||
| df6ba1c51e | |||
| 9ccf925964 | |||
| a8e8760e2b | |||
| f5d1361146 | |||
| 8cd988279f | |||
| e46931311f | |||
| 6091b794de | |||
| 4357b91446 | |||
| 2de531e98c | |||
| 5c98598311 | |||
| 4ddaa0603d | |||
| 52bb3da0eb | |||
| b048235657 | |||
| 47ade72438 | |||
| 64211439a8 | |||
| 3f2d66c255 | |||
| 95bf6e839b | |||
| e061ec4318 | |||
| 37e7a31a55 | |||
| 06faf04b83 | |||
| 3b1fae6a1e | |||
| 8a95820fca | |||
| cd74f857a2 | |||
| c9b7acbd0a | |||
| 504a2c9862 | |||
| 8c8ce89978 | |||
| 328263e846 | |||
| 1b95c37b80 | |||
| 6e20628bd2 | |||
| 7b11a91da0 | |||
| 5dfa24be86 | |||
| f698322e35 | |||
| d19a5f4ae6 | |||
| 6f1efe53af | |||
| 8fbad77e65 | |||
| 5ef1fdc747 | |||
| 35226d2209 | |||
| 97b9c2beca | |||
| 15a3a29a28 | |||
| a3d1915dec | |||
| 034ca80db2 | |||
| 2cb9475847 | |||
| 8a1e7b2614 | |||
| 387ec332fc | |||
| 30d89b8ad7 | |||
| 810f874a50 | |||
| 8946873e32 | |||
| 62349099aa | |||
| d1185d986d | |||
| 6510a0e124 | |||
| 403a7ac03b | |||
| 69336e2996 | |||
| e295675fe5 | |||
| 89c98f1556 | |||
| 9024eb2672 | |||
| cc8842dcd8 | |||
| 0b0028c19c | |||
| abda8a4a4d | |||
| d8b65342e1 | |||
| 049673c6bb | |||
| fc0b6d6aaa | |||
| 0195d3e8ad | |||
| 468b3ef0e6 | |||
| c098fb9b1f | |||
| 6c5400f172 | |||
| f131d0f8b7 | |||
| 4e43fb89b3 | |||
| 3b5d5963e5 | |||
| 739392ef7b | |||
| 13bc16dd62 | |||
| 5c21a8569b | |||
| 491ef09e66 | |||
| 4312a158c7 | |||
| 7e68a3ea65 | |||
| d3feaa7fc0 | |||
| 95a4c444a6 | |||
| 02b63111e0 | |||
| baec804172 | |||
| b449cf08ad | |||
| d046e31d6c | |||
| cd5dd3bf34 | |||
| 642feb76a2 | |||
| 0ec5989bd4 | |||
| 235529315e | |||
| 3732c6fa99 | |||
| 9924c3b304 | |||
| bd74b42df3 | |||
| 7d2edc9862 | |||
| 83aad61147 | |||
| 3341ef82b7 | |||
| ba01624152 | |||
| f32c367fd0 | |||
| dd62a6dce4 | |||
| 1622d6cb90 | |||
| ceb37e33a4 | |||
| 9f42c6f381 | |||
| eba60b23c7 | |||
| fb8d52398b | |||
| 289eff5554 | |||
| e73cd2113e | |||
| 2955bfd38b | |||
| 101c929f15 | |||
| 3ba3d328b8 | |||
| 722e2ce9cc | |||
| 1f82eb814f | |||
| ca3a1e5ccf | |||
| 41cac3dedd | |||
| 4411618662 | |||
| 1d28c71cba | |||
| e4771663b4 | |||
| d75582d664 | |||
| 2cd3772105 | |||
| 8438d809ae | |||
| d09ac6434b | |||
| 0b27c609c3 | |||
| c40e1219a7 | |||
| 3c1458245c | |||
| 508c3e8e79 | |||
| e72e581f85 | |||
| c71ae54ed0 | |||
| 385815cd28 | |||
| f05a6c30d0 | |||
| c3c1b8b994 | |||
| 6893b7efeb | |||
| 3df7124cf8 | |||
| 6efabb6dae | |||
| 1ae3ace54e | |||
| 0f8728ca4a | |||
| b7cbc64dd4 | |||
| 1e046e1e4d | |||
| 5938c817a4 | |||
| f91d142bc7 | |||
| eece385313 | |||
| ea809277c3 | |||
| 4dc989926e | |||
| 2b92e783bb | |||
| c9cd17c24a | |||
| 2d1c835398 | |||
| 0943637f5a | |||
| 065a9a2341 | |||
| 0ded3fca2e | |||
| ad86c2ab76 | |||
| 0de9de617f | |||
| 944f83bd7e | |||
| 2845f949e3 | |||
| 099afd3f93 | |||
| 7a5aa413bc | |||
| 9c83af59ca | |||
| 3012e4752f | |||
| 0e20be9e86 | |||
| 1add8c6410 | |||
| 5d2ed56936 | |||
| 9928dda6e3 | |||
| ede5ac68c9 | |||
| 4d4889e9d9 | |||
| 531e07f25d | |||
| cddb7de71e | |||
| 3bdffc2b37 | |||
| 773e3e63ae | |||
| 2f86f70a80 | |||
| 0d918d32b5 | |||
| 720727a370 | |||
| b261efcaae | |||
| 7d97dd1f86 | |||
| da28fe411a | |||
| 67a988286e | |||
| a625a498a1 | |||
| 4dc188f857 |
56
.agents/skills/geometry-transform/SKILL.md
Normal file
56
.agents/skills/geometry-transform/SKILL.md
Normal 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.
|
||||
8
.agents/skills/geometry-transform/agents/openai.yaml
Normal file
8
.agents/skills/geometry-transform/agents/openai.yaml
Normal 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
|
||||
@ -0,0 +1,53 @@
|
||||
# 几何问题排查清单
|
||||
|
||||
## 先问自己
|
||||
|
||||
1. 这是虚拟物体还是真实物体?
|
||||
2. 路径类型是 `Ground / Hoisting / Rail` 哪一种?
|
||||
3. 错的是:
|
||||
- 旋转轴
|
||||
- 起点位置
|
||||
- 逐帧位置
|
||||
- 通行空间
|
||||
- 终点诊断
|
||||
|
||||
## 日志优先看什么
|
||||
|
||||
### 平面路径
|
||||
|
||||
- `[移动到起点] 路径方向yaw`
|
||||
- `[动画姿态入口] ... 目标姿态`
|
||||
- `[模型增量姿态] ... 当前/目标/增量`
|
||||
|
||||
### Rail
|
||||
|
||||
- `[移动到起点] Rail旋转姿态`
|
||||
- `RailPathPoseHelper` 相关诊断
|
||||
|
||||
### 虚拟物体
|
||||
|
||||
- `[虚拟物体姿态] 应用前`
|
||||
- `[虚拟物体姿态] 应用后`
|
||||
- 关注 `BoundingBox.Center` 是否符合资源原点预期
|
||||
|
||||
## 需要同步检查的链
|
||||
|
||||
出现几何问题时,至少对齐这几条:
|
||||
|
||||
1. 起点落位
|
||||
2. 逐帧位置
|
||||
3. 通行空间
|
||||
4. 终点诊断
|
||||
|
||||
如果其中一条使用的是“原始高度”,另一条使用的是“旋转后法线尺寸”,就很容易出现看起来互相矛盾的问题。
|
||||
|
||||
## 测试建议
|
||||
|
||||
至少补一个最短链测试:
|
||||
|
||||
- `YUp` 或 `ZUp`
|
||||
- 指定单一路径类型
|
||||
- 指定单个旋转轴
|
||||
- 验证尺寸或姿态的关键不变量
|
||||
|
||||
能测数学层,就不要先靠现场猜。
|
||||
@ -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.
|
||||
|
||||
@ -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 应用变换的方法是否有问题
|
||||
|
||||
139
.agents/skills/nw-api/SKILL.md
Normal file
139
.agents/skills/nw-api/SKILL.md
Normal 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 API,COM 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`
|
||||
292
.agents/skills/nw-api/references/api-usage-patterns.md
Normal file
292
.agents/skills/nw-api/references/api-usage-patterns.md
Normal 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);
|
||||
```
|
||||
309
.agents/skills/nw-api/references/net-examples-guide.md
Normal file
309
.agents/skills/nw-api/references/net-examples-guide.md
Normal 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\
|
||||
```
|
||||
286
.agents/skills/nw-api/references/plugin-types.md
Normal file
286
.agents/skills/nw-api/references/plugin-types.md
Normal 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");
|
||||
```
|
||||
48
.agents/skills/project-tools/README.md
Normal file
48
.agents/skills/project-tools/README.md
Normal 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/` 下的详细文档
|
||||
- [ ] 代码中使用了正确的工具类方法
|
||||
- [ ] 没有重复实现已有功能
|
||||
157
.agents/skills/project-tools/SKILL.md
Normal file
157
.agents/skills/project-tools/SKILL.md
Normal 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)
|
||||
178
.agents/skills/project-tools/examples/common-usage.md
Normal file
178
.agents/skills/project-tools/examples/common-usage.md
Normal 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}");
|
||||
}
|
||||
}
|
||||
```
|
||||
182
.agents/skills/project-tools/utils/CoordinateConverter.md
Normal file
182
.agents/skills/project-tools/utils/CoordinateConverter.md
Normal 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. **线程安全**:此类的实例方法不是线程安全的,需要在单线程中使用或使用外部锁
|
||||
127
.agents/skills/project-tools/utils/DialogHelper.md
Normal file
127
.agents/skills/project-tools/utils/DialogHelper.md
Normal 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();
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 示例1:ViewModel中显示非模态对话框
|
||||
|
||||
```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 ...>
|
||||
```
|
||||
134
.agents/skills/project-tools/utils/GeometryHelper.md
Normal file
134
.agents/skills/project-tools/utils/GeometryHelper.md
Normal 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)
|
||||
```
|
||||
144
.agents/skills/project-tools/utils/LogManager.md
Normal file
144
.agents/skills/project-tools/utils/LogManager.md
Normal 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("[渲染] 路径显示完成");
|
||||
```
|
||||
130
.agents/skills/project-tools/utils/ModelHighlightHelper.md
Normal file
130
.agents/skills/project-tools/utils/ModelHighlightHelper.md
Normal 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()`
|
||||
108
.agents/skills/project-tools/utils/NavisworksApiHelper.md
Normal file
108
.agents/skills/project-tools/utils/NavisworksApiHelper.md
Normal 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);
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 示例1:API操作后刷新缓存
|
||||
|
||||
```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. **日志前缀**:传入的日志前缀用于标识调用来源,便于调试
|
||||
202
.agents/skills/project-tools/utils/NavisworksSelectionHelper.md
Normal file
202
.agents/skills/project-tools/utils/NavisworksSelectionHelper.md
Normal 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. **空值处理**:所有方法都处理了空值情况,不会抛出异常
|
||||
140
.agents/skills/project-tools/utils/PathHelper.md
Normal file
140
.agents/skills/project-tools/utils/PathHelper.md
Normal 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 中使用
|
||||
213
.agents/skills/project-tools/utils/SectionClipHelper.md
Normal file
213
.agents/skills/project-tools/utils/SectionClipHelper.md
Normal 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` 查看过滤效果和假阳性数量
|
||||
126
.agents/skills/project-tools/utils/UnitsConverter.md
Normal file
126
.agents/skills/project-tools/utils/UnitsConverter.md
Normal 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); // 可能不同!
|
||||
```
|
||||
144
.agents/skills/project-tools/utils/ViewpointHelper.md
Normal file
144
.agents/skills/project-tools/utils/ViewpointHelper.md
Normal 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.25(1/4视图) |
|
||||
| `baseDimension` | 基准尺寸计算相机距离 | 路径长度或包围盒最大边 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **视角保存**:在进行视角调整前建议保存当前视角,便于后续恢复
|
||||
2. **异常处理**:方法会抛出 `InvalidOperationException`(无活动文档)和 `ArgumentException`(参数错误),需要适当捕获
|
||||
3. **单位转换**:内部自动使用 `UnitsConverter` 进行米和模型单位的转换
|
||||
4. **标准视角**:`FocusOnModelItem` 和 `FocusOnPosition` 使用模型的标准前右上视角(`FrontRightTopViewVector`)
|
||||
5. **性能考虑**:频繁调整视角可能影响性能,建议在关键操作后统一调整
|
||||
112
.agents/skills/project-tools/utils/VisibilityHelper.md
Normal file
112
.agents/skills/project-tools/utils/VisibilityHelper.md
Normal 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` 表示成功/失败,内部已处理异常
|
||||
@ -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": []
|
||||
}
|
||||
}
|
||||
@ -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
15
.gitignore
vendored
@ -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
|
||||
@ -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文件进行修改,不涉及其他文件。
|
||||
@ -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显示器上显示时,字体大小应保持其当前的适当大小。
|
||||
|
||||
### 需求4:DPI感知实现
|
||||
|
||||
**用户故事:** 作为插件开发人员,我希望在插件中实现适当的DPI感知功能,以便它能够自动适应不同的显示分辨率。
|
||||
|
||||
#### 验收标准4
|
||||
|
||||
1. 当插件启动时,它应检测当前显示器的DPI设置。
|
||||
2. 当插件检测到高DPI设置时,它应自动调整UI缩放因子。
|
||||
3. 当插件在具有不同DPI设置的显示器之间移动时,它应相应地调整其缩放。
|
||||
4. 当系统DPI设置更改时,插件应在下次启动时适当响应。
|
||||
5. 如果插件无法确定DPI设置,则应默认为标准缩放,不会出现错误。
|
||||
|
||||
### 需求5:一致的UI元素比例
|
||||
|
||||
**用户故事:** 作为插件用户,我希望UI元素在不同显示分辨率下保持一致的比例,以便界面保持熟悉和可用。
|
||||
|
||||
#### 验收标准5
|
||||
|
||||
1. 当插件针对不同分辨率进行缩放时,UI元素的相对大小应保持一致。
|
||||
2. 当插件针对不同分辨率进行缩放时,UI元素的布局和定位应保持一致。
|
||||
3. 当列表视图在不同分辨率上显示时,列宽应按比例缩放。
|
||||
4. 当分组框在不同分辨率上显示时,其内容应适当排列,不会重叠。
|
||||
5. 当插件在任何支持的分辨率上显示时,滚动条应仅在必要时出现。
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -1,4 +1,4 @@
|
||||
{
|
||||
"dotnet.preferCSharpExtension": true,
|
||||
"dotnet.defaultSolution": "NavisworksTransport.sln"
|
||||
"dotnet.defaultSolution": "TransportPlugin.sln"
|
||||
}
|
||||
587
AGENTS.md
Normal file
587
AGENTS.md
Normal 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
|
||||
- UI:WPF + 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`
|
||||
|
||||
请默认这是高风险改动,先补测试,再动实现。
|
||||
1727
CHANGELOG.md
1727
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
113
CLAUDE.md
113
CLAUDE.md
@ -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
|
||||
105
NavisworksTransport.UnitTests.csproj
Normal file
105
NavisworksTransport.UnitTests.csproj
Normal 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>
|
||||
@ -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
|
||||
@ -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>
|
||||
@ -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
63
Properties/Resources.Designer.cs
generated
Normal 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
101
Properties/Resources.resx
Normal 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>
|
||||
90
README.md
90
README.md
@ -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
553
TransportPlugin.csproj
Normal 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
49
TransportPlugin.sln
Normal 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
|
||||
135
UnitTests/CoordinateSystem/AssemblyEndFaceAnalyzerTests.cs
Normal file
135
UnitTests/CoordinateSystem/AssemblyEndFaceAnalyzerTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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, "距离光轴过小");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
188
UnitTests/CoordinateSystem/CanonicalPlanarPoseBuilderTests.cs
Normal file
188
UnitTests/CoordinateSystem/CanonicalPlanarPoseBuilderTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
149
UnitTests/CoordinateSystem/CanonicalRailOffsetResolverTests.cs
Normal file
149
UnitTests/CoordinateSystem/CanonicalRailOffsetResolverTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
237
UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs
Normal file
237
UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
47
UnitTests/CoordinateSystem/FragmentDefaultUpContextTests.cs
Normal file
47
UnitTests/CoordinateSystem/FragmentDefaultUpContextTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
UnitTests/CoordinateSystem/GroundPassageSpaceOffsetTests.cs
Normal file
17
UnitTests/CoordinateSystem/GroundPassageSpaceOffsetTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
146
UnitTests/CoordinateSystem/HoistingCoordinateHelperTests.cs
Normal file
146
UnitTests/CoordinateSystem/HoistingCoordinateHelperTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
116
UnitTests/CoordinateSystem/HoistingRealObjectPoseHelperTests.cs
Normal file
116
UnitTests/CoordinateSystem/HoistingRealObjectPoseHelperTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
443
UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs
Normal file
443
UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
121
UnitTests/CoordinateSystem/ModelAxisConventionTests.cs
Normal file
121
UnitTests/CoordinateSystem/ModelAxisConventionTests.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
32
UnitTests/CoordinateSystem/PathPointVisualizationTests.cs
Normal file
32
UnitTests/CoordinateSystem/PathPointVisualizationTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
123
UnitTests/CoordinateSystem/PathTargetFrameResolverTests.cs
Normal file
123
UnitTests/CoordinateSystem/PathTargetFrameResolverTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
UnitTests/CoordinateSystem/ProjectReferenceFrameTests.cs
Normal file
38
UnitTests/CoordinateSystem/ProjectReferenceFrameTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
118
UnitTests/CoordinateSystem/RailAssemblyWorkflowContextTests.cs
Normal file
118
UnitTests/CoordinateSystem/RailAssemblyWorkflowContextTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
36
UnitTests/CoordinateSystem/RailPathPoseHelperTests.cs
Normal file
36
UnitTests/CoordinateSystem/RailPathPoseHelperTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
290
UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
Normal file
290
UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
Normal 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));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
162
UnitTests/CoordinateSystem/RealObjectPlanarPoseSolverTests.cs
Normal file
162
UnitTests/CoordinateSystem/RealObjectPlanarPoseSolverTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
145
UnitTests/CoordinateSystem/RealObjectRailExtentResolverTests.cs
Normal file
145
UnitTests/CoordinateSystem/RealObjectRailExtentResolverTests.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
322
UnitTests/CoordinateSystem/RotatedObjectExtentHelperTests.cs
Normal file
322
UnitTests/CoordinateSystem/RotatedObjectExtentHelperTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
85
UnitTests/CoordinateSystem/ViewpointHelperTests.cs
Normal file
85
UnitTests/CoordinateSystem/ViewpointHelperTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
100
UnitTests/CoordinateSystem/VirtualObjectModelTransformTests.cs
Normal file
100
UnitTests/CoordinateSystem/VirtualObjectModelTransformTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
UnitTests/Core/PathHelperTests.cs
Normal file
38
UnitTests/Core/PathHelperTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
120
UnitTests/Core/PathPersistenceTests.cs
Normal file
120
UnitTests/Core/PathPersistenceTests.cs
Normal 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
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
47
UnitTests/Core/PathPlanningManagerHoistingCompletionTests.cs
Normal file
47
UnitTests/Core/PathPlanningManagerHoistingCompletionTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
71
UnitTests/Core/PathRouteCloneTests.cs
Normal file
71
UnitTests/Core/PathRouteCloneTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
45
UnitTests/Core/SelectionClipBoxLockStateTests.cs
Normal file
45
UnitTests/Core/SelectionClipBoxLockStateTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"], "生成路径存在落在网格外的采样点");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
116
UnitTests/Integration/NavisworksTestAutomationClient.cs
Normal file
116
UnitTests/Integration/NavisworksTestAutomationClient.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
68
UnitTests/Integration/VirtualCollisionAutomationTests.cs
Normal file
68
UnitTests/Integration/VirtualCollisionAutomationTests.cs
Normal 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"], "缺少碰撞报告");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
UnitTests/Properties/AssemblyInfo.cs
Normal file
15
UnitTests/Properties/AssemblyInfo.cs
Normal 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")]
|
||||
400
UnitTests/Utils/BoundingBoxGeometryUtilsTests.cs
Normal file
400
UnitTests/Utils/BoundingBoxGeometryUtilsTests.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@ -1 +1,3 @@
|
||||
0.2.0
|
||||
# 版本号
|
||||
|
||||
0.15.0
|
||||
|
||||
28
compile.bat
Normal file
28
compile.bat
Normal 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
121
deploy-plugin.bat
Normal 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
|
||||
777
doc/architecture/System_Architecture_Design.md
Normal file
777
doc/architecture/System_Architecture_Design.md
Normal 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将为建筑工程领域的数字化转型提供强有力的技术支撑。
|
||||
162
doc/architecture/architecture_diagram.py
Normal file
162
doc/architecture/architecture_diagram.py
Normal 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()
|
||||
BIN
doc/architecture/system_architecture.png
Normal file
BIN
doc/architecture/system_architecture.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 387 KiB |
93
doc/bugfixes/优化选择提示信息_完成报告.md
Normal file
93
doc/bugfixes/优化选择提示信息_完成报告.md
Normal 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. 对于多选情况的智能显示
|
||||
|
||||
这大大提升了用户体验,让用户能够更清楚地了解当前的操作对象。
|
||||
196
doc/bugfixes/层属性选择同步问题修复报告.md
Normal file
196
doc/bugfixes/层属性选择同步问题修复报告.md
Normal 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选择变化事件监听,解决了楼层属性设置功能中状态不同步的问题。修复遵循了项目既有的架构模式,确保了代码质量和系统稳定性。
|
||||
|
||||
修复后的楼层属性设置功能将具备与选择集保存功能相同的响应性和用户体验,提升了整体插件的一致性和可用性。
|
||||
@ -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导入进行概念验证。
|
||||
@ -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()
|
||||
{
|
||||
278
doc/design/2026/AStar库的使用方法.md
Normal file
278
doc/design/2026/AStar库的使用方法.md
Normal 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`
|
||||
429
doc/design/2026/C# A_ 寻路优化_.md
Normal file
429
doc/design/2026/C# A_ 寻路优化_.md
Normal 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.x−b.x∣+∣a.y−b.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。
|
||||
|
||||
表1:A\*优先级队列实现比较分析
|
||||
|
||||
| 优先级队列类型 | 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)
|
||||
837
doc/design/2026/GPU加速可行性研究.md
Normal file
837
doc/design/2026/GPU加速可行性研究.md
Normal 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 GPU,29 GB/s 带宽,DirectX 11 兼容
|
||||
- **推荐配置**:8 GB GPU,106 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 方案 A:A* 路径规划 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
Loading…
Reference in New Issue
Block a user