mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
Compare commits
647 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e74eb0cc53 | |||
| 008d6bed5c | |||
| d0985a23d0 | |||
| a5d49e81a0 | |||
| bfcc086aad | |||
| 0eae3de6a2 | |||
| 1d39413717 | |||
| c57e5e2100 | |||
| ca5249a179 | |||
| 698c708bf6 | |||
| 084151ed4c | |||
| c56c7288e3 | |||
| f5398c77d5 | |||
| a724b5cb24 | |||
| f3a15953b2 | |||
| 567548c7f3 | |||
| 8c25349b49 | |||
| 49cd966d43 | |||
| b552ea1376 | |||
| 9c91ee31e5 | |||
| e9a063ccbd | |||
| e4e9db0b3e | |||
| 2ec41bf3a0 | |||
| 944cbeea1a | |||
| 4b8238a602 | |||
| 8789a19f57 | |||
| 8c8e83d98f | |||
| 527d26c1c6 | |||
| e2a5b09d77 | |||
| 98b6edf119 | |||
| 0a05197d3e | |||
| 75645a837a | |||
| 40e463a55f | |||
| 7b817f7359 | |||
| 22e69a606d | |||
| 8c7b2ab2a1 | |||
| 3512a2ce36 | |||
| 9e76b2b9eb | |||
| a08a78b3c9 | |||
| 9fc5f72831 | |||
| 391e09c16c | |||
| 60e4de5ffa | |||
| a248f16cf8 | |||
| 9240a10fa8 | |||
| 19d3208744 | |||
| c080567f53 | |||
| ac72f41051 | |||
| fc9273c1d3 | |||
| 43dc2da05f | |||
| 121190c9b9 | |||
| 0f2f6e2721 | |||
| 05070c3758 | |||
| d4d923e51b | |||
| 766ffa71cd | |||
| cbf213952b | |||
| b7c76048ab | |||
| 20fdb09a0c | |||
| 1943d59339 | |||
| d2836e07d5 | |||
| 90a788082d | |||
| eafad2862f | |||
| 1c0fa13f39 | |||
| 2e5ce6b862 | |||
| f63a33647b | |||
| 2f84c188f5 | |||
| 01b1b57f71 | |||
| 1ad0ef82d4 | |||
| 23212003e9 | |||
| aff281d00b | |||
| 4bbcf3fa1e | |||
| 5e50a15b41 | |||
| b673fc94a5 | |||
| 4c7a5e8a1a | |||
| 33ebcd7664 | |||
| 76ce726f1d | |||
| 54da579891 | |||
| e024e9e622 | |||
| 56f4b72161 | |||
| 7545313dee | |||
| 48eb6b70e3 | |||
| 5f2acb43b1 | |||
| b7eaefee50 | |||
| 13a387cc73 | |||
| f139f2ba1e | |||
| 1d8085f6c9 | |||
| 6699bfeb54 | |||
| 7cee0a85c6 | |||
| 351dcfa241 | |||
| 28022581a7 | |||
| 748aaa671d | |||
| 95b474c37d | |||
| 9bb9f91f57 | |||
| 7db967583b | |||
| 5d43f6db37 | |||
| 4cfa327159 | |||
| 920177d8c6 | |||
| 5f902600e3 | |||
| db9af13843 | |||
| dd49c092c6 | |||
| d347a7f893 | |||
| 3c61804e8b | |||
| e179070978 | |||
| de2ada1141 | |||
| eb48a68dd1 | |||
| fc6bf5e2bb | |||
| 3f2d86255d | |||
| d751ee9995 | |||
| 83f70624a4 | |||
| 356253c4f0 | |||
| a459c39c97 | |||
| e5719bd3c6 | |||
| 18387e7732 | |||
| 57e8e47061 | |||
| 5c5bb03652 | |||
| ea1d797605 | |||
| bc3bf07a60 | |||
| fbd4177d08 | |||
| 4842891fd7 | |||
| f6056f9bc3 | |||
| c7af6c4482 | |||
| c9c905fd3e | |||
| 7f76488869 | |||
| cd930d809a | |||
| f49d7ec2cb | |||
| 322abdff53 | |||
| 984978bc00 | |||
| ce8da8bd85 | |||
| 6d0fdb32c6 | |||
| 0cb3c0a907 | |||
| d9ea62a670 | |||
| 2228104a8e | |||
| e473574086 | |||
| f93604d6b7 | |||
| d106b0e017 | |||
| 90e35810d9 | |||
| 4fb910fc44 | |||
| 280ce56431 | |||
| 81e46b533b | |||
| 171db4b241 | |||
| 8fe9ddd599 | |||
| b299694ca0 | |||
| ca76b77130 | |||
| e94a5c97df | |||
| df845653f3 | |||
| a3e499656c | |||
| 79bb40092f | |||
| 2c7a4ca880 | |||
| 6a4375cf54 | |||
| c74b79213b | |||
| 806d147a85 | |||
| 227b1391bd | |||
| 68e2bb1fa7 | |||
| fccc233f42 | |||
| 86785ab181 | |||
| 3255053ac4 | |||
| 422cc4802c | |||
| fc0b0ca188 | |||
| 3120b0c6e2 | |||
| 91fac6d4bd | |||
| c7c59d9496 | |||
| 73ac5d314b | |||
| 3b2cbac9d1 | |||
| a63d6475b2 | |||
| 4cd0932d7d | |||
| cb447fe902 | |||
| 5ee4e2704d | |||
| 5b5540494d | |||
| 85e123fadb | |||
| f745222d37 | |||
| 88893a8c2c | |||
| eb8bf60e93 | |||
| 319aada33b | |||
| 77c9bf5bb6 | |||
| e5e81c818e | |||
| 7aad0fc119 | |||
| f7403e8eee | |||
| e2df4f335d | |||
| cbe944d030 | |||
| 87f0475b99 | |||
| 4689cc858a | |||
| 6d6b893db2 | |||
| e2d4a3ae24 | |||
| e4c1f7518d | |||
| 48f8c0e085 | |||
| 2c0ea75960 | |||
| e643972669 | |||
| d7c4936255 | |||
| 96d19952e9 | |||
| 57caf6a31f | |||
| 189fc6e1e4 | |||
| 88117941c5 | |||
| 0a6c8a4343 | |||
| 5bfad88a75 | |||
| 72544419da | |||
| d1baaa21e7 | |||
| 5d9fc44a69 | |||
| 917344ee1d | |||
| 68ef6458d7 | |||
| 36576c8d40 | |||
| b44a1cb2e2 | |||
| 5c7cf4469f | |||
| 147216cb7b | |||
| 62b1478af1 | |||
| 0f0055b0f1 | |||
| 3be8e4f1ce | |||
| 6aa4ca89fe | |||
| 5fb574cf5c | |||
| 85a502a038 | |||
| a7c56e742b | |||
| f11f648630 | |||
| 74d1bd7e3e | |||
| 248ac71215 | |||
| af4cb22756 | |||
| 56393ef358 | |||
| 5d4512033d | |||
| b9cb1ab012 | |||
| e34b737428 | |||
| 557d3429eb | |||
| f9019a4007 | |||
| 0668eadc36 | |||
| fdfaf54698 | |||
| ffa3aae132 | |||
| 0293c133ab | |||
| f9db685753 | |||
| a800d1af7e | |||
| f2bd362f57 | |||
| 87021ebbe7 | |||
| 36f2ed6da6 | |||
| a61c084f27 | |||
| 6849bb8c17 | |||
| a26f60861e | |||
| 106b8fdd07 | |||
| 87c6b06cfc | |||
| af4917efba | |||
| 806b2e2714 | |||
| 4ae8e4c74b | |||
| 8101ffe06a | |||
| f07b6e14b4 | |||
| f2c31cc17d | |||
| 39a626edeb | |||
| 02379c5110 | |||
| 1e661c42fb | |||
| eb7e9bacbf | |||
| c38e608a7c | |||
| 544ed696da | |||
| 322e47c5b5 | |||
| 3ee9e6f45c | |||
| 7df94a906d | |||
| af1a40c913 | |||
| 451512ddff | |||
| fbd33145e1 | |||
| 8b0b1699f6 | |||
| 2e13ed77d8 | |||
| 804c0f4a77 | |||
| 37dec500fd | |||
| ac3eb6fbda | |||
| 6a0bc0cd91 | |||
| 699c8bf6b6 | |||
| 7806f989f3 | |||
| a0ca387ae7 | |||
| 2a716ec242 | |||
| 93ab51fb5e | |||
| 7c59fc0b83 | |||
| 819466a819 | |||
| 33a8ba9834 | |||
| 6ce86ae368 | |||
| 68bf7a08e0 | |||
| 771ba4c337 | |||
| 33199b4bb6 | |||
| 74d730f368 | |||
| eb2a71bf48 | |||
| 4b4e2e1622 | |||
| b5f4c8218d | |||
| e3a22601d5 | |||
| f67856a2ae | |||
| 6b71b32e63 | |||
| 60d99fe2fa | |||
| 75e1786682 | |||
| 9b3f964000 | |||
| de6b578d9c | |||
| f50e790b11 | |||
| 9821c3af67 | |||
| 0844d6f52e | |||
| 520e7653e4 | |||
| 9512075506 | |||
| bdd61713ea | |||
| 97cf1da4ff | |||
| 836a162d57 | |||
| 09a6f56f47 | |||
| 35ce8c2848 | |||
| ae2fd67260 | |||
| 9def041f27 | |||
| e9d86253ec | |||
| f95466ee0c | |||
| e85f21ce7a | |||
| d3795c9836 | |||
| ab647b13d9 | |||
| dcad4146b6 | |||
| adb2883310 | |||
| 7867c71b6e | |||
| a5b1a96dfd | |||
| 4e5503002a | |||
| fa8fc161da | |||
| e4016313c4 | |||
| 81fdfbacd0 | |||
| 7c2c946b2f | |||
| 5b34e08c37 | |||
| 0b16c07e3a | |||
| 0210131a56 | |||
| a44d7e997a | |||
| 1df8908689 | |||
| f9daba9fba | |||
| fd6f2f3fc4 | |||
| a4d0378405 | |||
| 1629205c3f | |||
| d058f17c15 | |||
| bbb3b62b8a | |||
| f9ef00fd45 | |||
| 709c530434 | |||
| 0ee93294dd | |||
| 73124dd597 | |||
| 759e134e3b | |||
| 1f11354188 | |||
| eaffb9d8c0 | |||
| f297b1709d | |||
| 1f637660d1 | |||
| c349523ea9 | |||
| 5cc11a170b | |||
| 21094c59d9 | |||
| 8ca5dd747e | |||
| ffe9ac6614 | |||
| d2083b9a3d | |||
| 70d1ad59c2 | |||
| 53687e6b7c | |||
| 67993071a6 | |||
| 29c98a5996 | |||
| 5b5a4649c5 | |||
| d654110fc1 | |||
| 735f308abd | |||
| f86f988d6e | |||
| a1a684cd2b | |||
| 6180f5be33 | |||
| cd58b994dd | |||
| 40854eb3d3 | |||
| 79453a6ae0 | |||
| eec4c23017 | |||
| beeba10b22 | |||
| 7a6005d0ea | |||
| 58150a296d | |||
| 7de454337d | |||
| 3e25ef9222 | |||
| 9278e43d72 | |||
| fafdc76494 | |||
| 99c47069cd | |||
| cee48d7424 | |||
| dc7062e182 | |||
| 5f6c584a58 | |||
| 5f7630e527 | |||
| 733e80737a | |||
| 64752e52c9 | |||
| 6c260ef5c9 | |||
| e0e0572ff5 | |||
| a3942ee5e2 | |||
| 40d5a94e2d | |||
| f9fe1607ff | |||
| 2a019e8090 | |||
| 901a582c6c | |||
| 29a4f78689 | |||
| f1e0375b6a | |||
| 52e354d1b8 | |||
| eef961a39e | |||
| fcab5d445f | |||
| b534895ae3 | |||
| 5b76e83965 | |||
| a21b73225a | |||
| a11a4032f6 | |||
| a09f864651 | |||
| 609fd35410 | |||
| df0c136b6c | |||
| ce6d4ea59a | |||
| 29594b50b5 | |||
| 2a2da56a0e | |||
| dd041478b3 | |||
| 650e5b0cd3 | |||
| afa0db3c7e | |||
| b9f1f3438c | |||
| 31f21dcb29 | |||
| 64a22fe5c6 | |||
| 3f85140d60 | |||
| 1b1588359f | |||
| 4c562c1af0 | |||
| a0fdb9f777 | |||
| 2c29f89fe9 | |||
| f4aac566d5 | |||
| 57521bd106 | |||
| 1413cf4f9d | |||
| 3fa74469f4 | |||
| e3d9dd747d | |||
| d71ebb8d25 | |||
| 0097d98669 | |||
| dd635340aa | |||
| 1b06cb8014 | |||
| 163e10b608 | |||
| dc67287d4b | |||
| 73f959c45a | |||
| 65e66352b7 | |||
| 205ff0914c | |||
| 2e76a800f5 | |||
| 1890bf4cb9 | |||
| 3fd472549e | |||
| 28d15a6c9f | |||
| 6026e037fa | |||
| 0ca90817d8 | |||
| 3436f3e0bb | |||
| 71f0170966 | |||
| 97cd07226d | |||
| 309ce20068 | |||
| 282c140c14 | |||
| bdf46f9431 | |||
| e79068854a | |||
| 7be93b5386 | |||
| 1b4da58a15 | |||
| 356ce48af5 | |||
| 3f24d54e8f | |||
| 1586c85314 | |||
| f9f9746a9a | |||
| 493faf22ff | |||
| bab6313313 | |||
| 618bed3889 | |||
| 8c9115c0ff | |||
| 0a561d1a30 | |||
| 443af3f7e7 | |||
| 2f6ac0be9f | |||
| 07c152f513 | |||
| 158c62bdca | |||
| 1029271e91 | |||
| 9b59d24fc7 | |||
| 1d0e8c59d2 | |||
| b479d82251 | |||
| 6b6ac38f96 | |||
| 57fe895ae0 | |||
| 4195d41e4c | |||
| 7eac5397cd | |||
| 084823164e | |||
| 87f110bae5 | |||
| 952719f19b | |||
| 2ab4e44248 | |||
| 6b9e670bf3 | |||
| 9e87f0f027 | |||
| c7272065e6 | |||
| 14bca5d199 | |||
| dbeb7d392c | |||
| 22cb7b3b67 | |||
| 018be92063 | |||
| 37f91461d2 | |||
| c73fedaaaf | |||
| 4ad4bdef3b | |||
| e50259f8fa | |||
| 7321138d57 | |||
| 319f63c861 | |||
| 662cd6232c | |||
| 3461a1e96f | |||
| 919c007509 | |||
| 5b77001568 | |||
| 1f4022a41b | |||
| c8c9262090 | |||
| 104cabfb67 | |||
| 4978a67e19 | |||
| e44e03f02e | |||
| 5a636a925b | |||
| d23f14f8aa | |||
| 1706ca5293 | |||
| ff3e2f14e1 | |||
| c4433489b7 | |||
| 4761c58571 | |||
| 1de6a7e792 | |||
| 7d6ba89496 | |||
| c4dc3934fa | |||
| 25f1d21a85 | |||
| 2bd6d9343c | |||
| bc2d76e685 | |||
| 124f786007 | |||
| b49cfe9744 | |||
| 6f5f189443 | |||
| d6f8448c83 | |||
| 0f85890c72 | |||
| bf3eb68b67 | |||
| ce3a6bba0f | |||
| f1e3d9c573 | |||
| 0a30d1a438 | |||
| 8311983d5e | |||
| 97bf4e3d0d | |||
| 15661536dc | |||
| f451c51451 | |||
| b5e27e4ced | |||
| 95881c6258 | |||
| cb8cfd7905 | |||
| 96fe1018ed | |||
| 73c006a977 | |||
| 133fa5547b | |||
| c95d263ca5 | |||
| 3507920c09 | |||
| cdabc507e1 | |||
| bcbee0659b | |||
| 053e4e1185 | |||
| d76679a45f | |||
| a1ed78e755 | |||
| d0f38bc919 | |||
| 4681d3198f | |||
| 96e4641ebf | |||
| 2ab808a216 | |||
| 1ec0e1ce19 | |||
| bc734c263e | |||
| a96bffa341 | |||
| fb3e9ad1b7 | |||
| 4c854fbb62 | |||
| 0e8d7bdc97 | |||
| a92d37cd64 | |||
| 00e46ce39e | |||
| 54b82fd149 | |||
| e508fb44f2 | |||
| 89e2ad9262 | |||
| 419901649e | |||
| 18cb767a95 | |||
| a11b51c971 | |||
| d7f913d464 | |||
| 0d982090e3 | |||
| be6617d9f9 | |||
| fc0efa2339 | |||
| c6b86e45f3 | |||
| 53b6247bb8 | |||
| eab06bda6c | |||
| 524fa58f05 | |||
| 4c4846bc8f | |||
| 0dfe1d995f | |||
| a4434413d2 | |||
| 5ca19ce2f7 | |||
| 3e1c2c4daf | |||
| 2d4e316df2 | |||
| 4b426e1fc0 | |||
| d2ad4b2b68 | |||
| 67522e03d1 | |||
| 24f307130c | |||
| 0a3e5f65fa | |||
| e302fea33b | |||
| c352181f8c | |||
| 3a1416536a | |||
| 4eec863cad | |||
| 96bcd31d99 | |||
| b09ff828de | |||
| 3cb68ca388 | |||
| c95d3693ea | |||
| 67d2ab2577 | |||
| 970b24b63b | |||
| fdaae048c2 | |||
| 4da5a38237 | |||
| 0cf2d7e05e | |||
| 3122cf21db | |||
| 4bf25d5d9c | |||
| dc10c7a360 | |||
| 94e082fc7d | |||
| 9671df8491 | |||
| 8aa38917eb | |||
| 0774100606 | |||
| 95b48b02e4 | |||
| c5de1d8727 | |||
| 7e09ab0ced | |||
| eb3e137a6e | |||
| f528ad9744 | |||
| 650f9eb7f5 | |||
| a78d6f42e5 | |||
| 4326a74c64 | |||
| 08fc62d4d9 | |||
| ae298de8dc | |||
| a24c4b269a | |||
| 083d9d3e3e | |||
| 12e7ed356c | |||
| 3f15dd71b1 | |||
| 42448cae4e | |||
| 7e28d193ec | |||
| 8f2b32532c | |||
| f0a00cbc94 | |||
| fd73b2cf0a | |||
| e5b3455533 | |||
| e677355438 | |||
| 93fd3f97e3 | |||
| 910d189f35 | |||
| c8d6d12442 | |||
| 1f3ba3d09d | |||
| f67941221e | |||
| 8252ef82ad | |||
| 3b825e35ae | |||
| 4a4912e817 | |||
| 5a38604ec2 | |||
| b15e0d0e2b | |||
| 5a5f52a5c2 | |||
| 816a690519 | |||
| 60303c6560 | |||
| cc88e6f112 | |||
| 5f1b5d0c63 | |||
| b54caa8967 | |||
| 73e048a9a8 | |||
| 316a2b7f53 | |||
| 42edcda8bd | |||
| 92432f453d | |||
| 4b6ebe7b1c | |||
| 3f6d25ed43 | |||
| d4b2cc7f2c | |||
| 53c050c6ed | |||
| 3213af2f48 | |||
| 85122ba184 | |||
| b8791d43aa | |||
| 6a82f27d48 | |||
| 4e2400952a | |||
| de1dae8b6e | |||
| dd127db969 | |||
| 194bc53bdb | |||
| 8c3e29c351 | |||
| 6e55a04550 | |||
| 9333839267 | |||
| 280236290e | |||
| c5773b5665 | |||
| 96babc4506 | |||
| f8acde0801 | |||
| 32c101cf74 | |||
| 2a1aa535ba | |||
| 02903c4929 | |||
| d80e26e4ab | |||
| a88b83306e | |||
| 89023df4e5 | |||
| f794b647e1 | |||
| 2e81b2bd17 | |||
| 8527d0fcbd | |||
| bc32481185 | |||
| 58e9f1baf9 | |||
| db5432d902 | |||
| 397b6ae5fe | |||
| 72fdca9e82 | |||
| 29b8e67bc8 | |||
| 33ec80e54d | |||
| 371e4137f1 | |||
| 555a2ce79e | |||
| 699742880e | |||
| d99348f163 | |||
| d7034c6a40 | |||
| ac632d0698 | |||
| f3696788cb |
@@ -0,0 +1,100 @@
|
||||
name: Publish-blenderbim-chocolatey package
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# ┌───────────── minute (0 - 59)
|
||||
# │ ┌───────────── hour (0 - 23)
|
||||
# │ │ ┌───────────── day of the month (1 - 31)
|
||||
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
|
||||
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
|
||||
# * * * * *
|
||||
- cron: "55 23 * * *" # 5min before utc midnight
|
||||
|
||||
env:
|
||||
major: 0
|
||||
minor: 0
|
||||
name: blenderbim
|
||||
choco_version: 1.1.0
|
||||
CHOCO_TOKEN: ${{ secrets.CHOCO_TOKEN }}
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||
steps:
|
||||
- name: Set env
|
||||
run: echo ok go
|
||||
|
||||
build:
|
||||
needs: activate
|
||||
name: ${{ matrix.config.name }}-${{ matrix.pyver }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pyver: [py310]
|
||||
config:
|
||||
- {
|
||||
name: "Windows Build",
|
||||
short_name: win,
|
||||
}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.7.7' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
- run: echo ${{ env.DATE }}
|
||||
|
||||
- name: Check in published releases if we should do a choco release
|
||||
id: do_choco
|
||||
run: |
|
||||
echo "::set-output name=choco_release::$(python3 /home/runner/work/IfcOpenShell/IfcOpenShell/choco/blenderbim/check_repo_infos.py --do_choco_release?)"
|
||||
|
||||
- name: Compile
|
||||
if: ${{steps.do_choco.outputs.choco_release}} == 'do_choco_release'
|
||||
run: |
|
||||
target_os=${{ matrix.config.short_name }} &&
|
||||
pyver=$(python3 /home/runner/work/IfcOpenShell/IfcOpenShell/choco/blenderbim/check_repo_infos.py --pyver?) &&
|
||||
cp -r src/blenderbim src/blenderbim_$target_os_$pyver &&
|
||||
cd src/blenderbim_$target_os_$pyver &&
|
||||
make dist PLATFORM=$target_os PYVERSION=$pyver
|
||||
|
||||
- name: Fill chocolatey scripts on win with latest blender python
|
||||
if: ${{steps.do_choco.outputs.choco_release}} == 'do_choco_release'
|
||||
run: |
|
||||
today_non_iso="$(date +'%y%m%d' | tr -d '\n')" &&
|
||||
target_os=${{ matrix.config.short_name }} &&
|
||||
latest_blender_python_version_maj_min=$(python3 /home/runner/work/IfcOpenShell/IfcOpenShell/choco/blenderbim/check_repo_infos.py --latest_blender_python_version_maj_min?) &&
|
||||
echo "latest_blender_python_version_maj_min?: $latest_blender_python_version_maj_min" &&
|
||||
pyver=$(python3 /home/runner/work/IfcOpenShell/IfcOpenShell/choco/blenderbim/check_repo_infos.py --pyver?) &&
|
||||
export latest_blender_version_maj_min=$(python3 /home/runner/work/IfcOpenShell/IfcOpenShell/choco/blenderbim/check_repo_infos.py --latest_blender_release_maj_min?) &&
|
||||
export latest_blender_version_maj_min_pat=$(python3 /home/runner/work/IfcOpenShell/IfcOpenShell/choco/blenderbim/check_repo_infos.py --latest_blender_release_maj_min_pat?) &&
|
||||
echo latest_blender_version_maj_min_pat?: $latest_blender_version_maj_min_pat &&
|
||||
export blenderbim_build_version="${{ env.major }}.${{ env.minor }}.$today_non_iso" &&
|
||||
export url_blenderbim_py310_win_zip="https://github.com/IfcOpenShell/IfcOpenShell/releases/download/blenderbim-$today_non_iso/blenderbim-$today_non_iso-$pyver-$target_os.zip" &&
|
||||
export sha256sum_blenderbim_py310_win_zip=$( sha256sum src/blenderbim_$target_os_$pyver/dist/blenderbim-$today_non_iso-$pyver-$target_os.zip --tag | cut -d ' ' -f 4 | tr -d '\n') &&
|
||||
echo sha256sum_blenderbim_py310_win_zip: $sha256sum_blenderbim_py310_win_zip &&
|
||||
python3 choco/blenderbim/fill_dynamic_parameters.py &&
|
||||
echo __build choco with mono &&
|
||||
wget "https://github.com/chocolatey/choco/archive/refs/tags/$choco_version.tar.gz" --quiet &&
|
||||
tar -xzf "$choco_version.tar.gz" &&
|
||||
cd choco-$choco_version &&
|
||||
chmod +x build.sh zip.sh &&
|
||||
./build.sh &&
|
||||
cp -r build_output/chocolatey /opt/chocolatey &&
|
||||
cd /home/runner/work/IfcOpenShell/IfcOpenShell/choco/blenderbim/ &&
|
||||
echo __choco pack &&
|
||||
mono /opt/chocolatey/choco.exe --version &&
|
||||
mono /opt/chocolatey/choco.exe pack --allow-unofficial &&
|
||||
echo __choco set apiKey &&
|
||||
mono /opt/chocolatey/choco.exe setapikey --key="$CHOCO_TOKEN" --source="https://push.chocolatey.org/" --allow-unofficial # &&
|
||||
echo __choco push &&
|
||||
mono /opt/chocolatey/choco.exe push --source="https://push.chocolatey.org/" --key="$CHOCO_TOKEN" --allow-unofficial --verbose
|
||||
|
||||
- name: Inform user about not packaging
|
||||
if: ${{steps.do_choco.outputs.choco_release}} != 'do_choco_release'
|
||||
run: |
|
||||
echo "no releases found today ${{ env.DATE }}, therefore choco packaging is skipped."
|
||||
|
||||
@@ -3,8 +3,18 @@ name: Publish-blenderbim-multiplatform
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'src/blenderbim/**'
|
||||
- '.github/workflows/ci-blenderbim-matrix.yml'
|
||||
- 'src/blenderbim/**'
|
||||
- 'src/ifcopenshell-python/ifcopenshell/**'
|
||||
- 'src/bcf/src/bcf/**'
|
||||
- 'src/ifcclash/ifcclash/**'
|
||||
- 'src/ifccobie/**'
|
||||
- 'src/ifcdiff/**'
|
||||
- 'src/ifccsv/**'
|
||||
- 'src/ifcpatch/ifcpatch/**'
|
||||
- 'src/ifc4d/ifc4d/**'
|
||||
- 'src/ifc5d/ifc5d/**'
|
||||
- 'src/ifccityjson/**'
|
||||
branches:
|
||||
- v0.7.0
|
||||
|
||||
@@ -43,6 +53,10 @@ jobs:
|
||||
name: "MacOS Build",
|
||||
short_name: macos
|
||||
}
|
||||
- {
|
||||
name: "MacOS ARM Build",
|
||||
short_name: macosm1
|
||||
}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
name: ci-ifcopenshell-conda-daily
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["ci"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||
steps:
|
||||
- name: Set env
|
||||
run: echo ok go
|
||||
|
||||
test:
|
||||
name: ${{ matrix.platform.distver }}-${{ matrix.pyver.name }}
|
||||
needs: activate
|
||||
runs-on: ${{ matrix.platform.distver }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -l {0}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pyver: [
|
||||
{ name: py39, distver: '3.9' },
|
||||
{ name: py310, distver: '3.10'}
|
||||
]
|
||||
platform: [
|
||||
{ name: Windows, distver: windows-2022, upload: 'false' },
|
||||
{ name: Windows, distver: windows-2019, upload: 'true' },
|
||||
{ name: Linux, distver: ubuntu-20.04, upload: 'false' },
|
||||
{ name: Linux, distver: ubuntu-18.04, upload: 'true' },
|
||||
{ name: macOS, distver: macos-11, upload: 'true' },
|
||||
{ name: macOS, distver: macos-10.15, upload: 'false' }
|
||||
]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Download MacOSX SDK
|
||||
if: ${{ matrix.platform.name == 'macOS' }}
|
||||
run: |
|
||||
curl -o MacOSX10.13.sdk.tar.xz -L https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX10.13.sdk.tar.xz && \
|
||||
tar xf MacOSX10.13.sdk.tar.xz && \
|
||||
sudo mv -v MacOSX10.13.sdk /opt/ && \
|
||||
ls /opt/
|
||||
- uses: seanmiddleditch/gha-setup-ninja@master
|
||||
- uses: conda-incubator/setup-miniconda@v2 # https://github.com/conda-incubator/setup-miniconda
|
||||
with:
|
||||
activate-environment: conda-build
|
||||
python-version: ${{ matrix.pyver.distver }}
|
||||
environment-file: conda/environment.yml
|
||||
- name: build, test and upload ifcopenshell
|
||||
if: ${{ matrix.platform.upload == 'true' }}
|
||||
run: |
|
||||
conda-build . --python ${{ matrix.pyver.distver }} -c conda-forge --token ${{ secrets.ANACONDA_TOKEN }} --user ifcopenshell --no-remove-work-dir
|
||||
- name: build & test ifcopenshell
|
||||
if: ${{ matrix.platform.upload == 'false' }}
|
||||
run: |
|
||||
conda-build . --python ${{ matrix.pyver.distver }} -c conda-forge --no-remove-work-dir
|
||||
@@ -0,0 +1,73 @@
|
||||
name: ci-ifcopenshell-pypi
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# ┌───────────── minute (0 - 59)
|
||||
# │ ┌───────────── hour (0 - 23)
|
||||
# │ │ ┌───────────── day of the month (1 - 31)
|
||||
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
|
||||
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
|
||||
# * * * * *
|
||||
- cron: "51 23 1 * *" # 9min before utc midnight
|
||||
|
||||
env:
|
||||
major: 0
|
||||
minor: 0
|
||||
name: ifcopenshell
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||
steps:
|
||||
- name: Set env
|
||||
run: echo ok go
|
||||
|
||||
build:
|
||||
needs: activate
|
||||
name: ${{ matrix.config.name }}-${{ matrix.pyver }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pyver: [py36, py37, py38, py39, py310]
|
||||
config:
|
||||
- {
|
||||
name: "Windows Build",
|
||||
short_name: win,
|
||||
}
|
||||
- {
|
||||
name: "Linux Build",
|
||||
short_name: linux
|
||||
}
|
||||
- {
|
||||
name: "MacOS Build",
|
||||
short_name: macos
|
||||
}
|
||||
- {
|
||||
name: "MacOS ARM Build",
|
||||
short_name: macosm1
|
||||
}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.7.7' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
- run: echo ${{ env.DATE }}
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%y%m%d')"
|
||||
- name: Compile
|
||||
run: |
|
||||
pip install build
|
||||
cp -r src/ifcopenshell-python src/ifcopenshell_${{ matrix.config.short_name }}_${{ matrix.pyver }} &&
|
||||
cd src/ifcopenshell_${{ matrix.config.short_name }}_${{ matrix.pyver }} &&
|
||||
make dist PLATFORM=${{ matrix.config.short_name }} PYVERSION=${{ matrix.pyver }}
|
||||
- name: Publish a Python distribution to PyPI
|
||||
uses: ortega2247/pypi-upload-action@master
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
packages_dir: src/ifcopenshell_${{ matrix.config.short_name }}_${{ matrix.pyver }}/dist
|
||||
@@ -0,0 +1,95 @@
|
||||
name: ci_py_only
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'test/**'
|
||||
- 'conda/**'
|
||||
- 'cmake/**'
|
||||
- '.github/workflows/ci_py_only.yml'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'IfcOpenShell/IfcOpenShell' &&
|
||||
!contains(github.event.head_commit.message, 'skip ci')
|
||||
steps:
|
||||
- run: echo ok go
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: activate
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install C++ dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt-get install --no-install-recommends \
|
||||
git cmake gcc g++ \
|
||||
libboost-date-time-dev \
|
||||
libboost-filesystem-dev \
|
||||
libboost-iostreams-dev \
|
||||
libboost-program-options-dev \
|
||||
libboost-regex-dev \
|
||||
libboost-system-dev \
|
||||
libboost-thread-dev \
|
||||
python3-all-dev python3-pip \
|
||||
swig libpcre3-dev libxml2-dev \
|
||||
libtbb-dev nlohmann-json3-dev \
|
||||
libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \
|
||||
libhdf5-dev libcgal-dev
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1
|
||||
|
||||
- name: Build ifcopenshell
|
||||
run: |
|
||||
mkdir build && cd build
|
||||
cmake \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_PREFIX_PATH=/usr \
|
||||
-DCMAKE_SYSTEM_PREFIX_PATH=/usr \
|
||||
-DOCC_INCLUDE_DIR=/usr/include/opencascade \
|
||||
-DOCC_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
|
||||
-DPYTHON_EXECUTABLE:FILEPATH=/usr/bin/python3 \
|
||||
-DPYTHON_INCLUDE_DIR:PATH=/usr/include/python3.8 \
|
||||
-DPYTHON_LIBRARY:FILEPATH=/usr/lib/x86_64-linux-gnu/libpython3.8.so \
|
||||
-DCOLLADA_SUPPORT=Off \
|
||||
"-DSCHEMA_VERSIONS=2x3;4" \
|
||||
-DBUILD_CONVERT=Off \
|
||||
-DGLTF_SUPPORT=On \
|
||||
-DJSON_INCLUDE_DIR=/usr/include \
|
||||
-DCGAL_INCLUDE_DIR=/usr/include \
|
||||
-DGMP_INCLUDE_DIR=/usr/include \
|
||||
-DMPFR_INCLUDE_DIR=/usr/include \
|
||||
-DGMP_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
|
||||
-DMPFR_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
|
||||
-DHDF5_INCLUDE_DIR=/usr/include/hdf5/serial \
|
||||
../cmake
|
||||
sudo make -j $(nproc)
|
||||
sudo make install
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
sudo /usr/bin/python -m pip install -U pip
|
||||
sudo /usr/bin/python -m pip install xmlschema numpy lxml
|
||||
sudo /usr/bin/python -m pip install src/bcf
|
||||
sudo /usr/bin/python -m pip install pytest
|
||||
sudo /usr/bin/python -m pip install isodate
|
||||
sudo /usr/bin/python -m pip install lark
|
||||
sudo /usr/bin/python -m pip install networkx
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
cd test
|
||||
sudo /usr/bin/python tests.py
|
||||
cd ../src/ifcopenshell-python
|
||||
mv ifcopenshell ifcopenshell-local # Force testing on installed module
|
||||
make test
|
||||
@@ -3,8 +3,19 @@ name: ci
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'test/**'
|
||||
- 'src/ifcblender/**'
|
||||
- 'src/ifcconvert/**'
|
||||
- 'src/ifcgeom/**'
|
||||
- 'src/ifcgeom_schema_agnostic/**'
|
||||
- 'src/ifcgeomserver/**'
|
||||
- 'src/ifcjni/**'
|
||||
- 'src/ifcmax/**'
|
||||
- 'src/ifcopenshell-python/**'
|
||||
- '!src/ifcopenshell-python/docs/**'
|
||||
- 'src/ifcparse/**'
|
||||
- 'src/ifcwrap/**'
|
||||
- 'src/qtviewer/**'
|
||||
- 'src/serializers/**'
|
||||
- 'conda/**'
|
||||
- 'cmake/**'
|
||||
- '.github/workflows/ci.yml'
|
||||
@@ -62,7 +73,7 @@ jobs:
|
||||
-DPYTHON_INCLUDE_DIR:PATH=/usr/include/python3.8 \
|
||||
-DPYTHON_LIBRARY:FILEPATH=/usr/lib/x86_64-linux-gnu/libpython3.8.so \
|
||||
-DCOLLADA_SUPPORT=Off \
|
||||
"-DSCHEMA_VERSIONS=2x3;4" \
|
||||
"-DSCHEMA_VERSIONS=2x3;4;4x3" \
|
||||
-DGLTF_SUPPORT=On \
|
||||
-DJSON_INCLUDE_DIR=/usr/include \
|
||||
-DCGAL_INCLUDE_DIR=/usr/include \
|
||||
|
||||
@@ -19,13 +19,23 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
-
|
||||
name: Update ubuntu
|
||||
- name: Install C++ dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt-get install git cmake gcc g++ libboost1.67-all-dev python3-all-dev swig libpcre3-dev libxml2-dev
|
||||
sudo apt-get install libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \
|
||||
libhdf5-dev libcgal-dev
|
||||
sudo apt-get install --no-install-recommends \
|
||||
git cmake gcc g++ \
|
||||
libboost-date-time-dev \
|
||||
libboost-filesystem-dev \
|
||||
libboost-iostreams-dev \
|
||||
libboost-program-options-dev \
|
||||
libboost-regex-dev \
|
||||
libboost-system-dev \
|
||||
libboost-thread-dev \
|
||||
python3-all-dev python3-pip \
|
||||
swig libpcre3-dev libxml2-dev \
|
||||
libtbb-dev nlohmann-json3-dev \
|
||||
libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \
|
||||
libhdf5-dev libcgal-dev opencollada-dev
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
@@ -44,10 +54,18 @@ jobs:
|
||||
-DPYTHON_EXECUTABLE:FILEPATH=/usr/bin/python3 \
|
||||
-DPYTHON_INCLUDE_DIR:PATH=/usr/include/python3.8 \
|
||||
-DPYTHON_LIBRARY:FILEPATH=/usr/lib/x86_64-linux-gnu/libpython3.8.so \
|
||||
-DCOLLADA_SUPPORT=Off \
|
||||
-DLIBXML2_INCLUDE_DIR=/usr/include/libxml2 \
|
||||
-DLIBXML2_LIBRARIES=/usr/lib/x86_64-linux-gnu/libxml2.so \
|
||||
-DGLTF_SUPPORT=On \
|
||||
-DJSON_INCLUDE_DIR=/usr/include \
|
||||
-DCGAL_INCLUDE_DIR=/usr/include \
|
||||
-DGMP_INCLUDE_DIR=/usr/include \
|
||||
-DMPFR_INCLUDE_DIR=/usr/include \
|
||||
-DGMP_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
|
||||
-DMPFR_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
|
||||
-DHDF5_INCLUDE_DIR=/usr/include/hdf5/serial \
|
||||
-DOPENCOLLADA_INCLUDE_DIR=/usr/include/opencollada \
|
||||
-DOPENCOLLADA_LIBRARY_DIR=/usr/lib/opencollada/ \
|
||||
../cmake
|
||||
make -j $(nproc)
|
||||
make install
|
||||
|
||||
@@ -67,3 +67,9 @@ htmlcov
|
||||
|
||||
# Blender
|
||||
*.blend1
|
||||
|
||||
# Sphinx
|
||||
_build/
|
||||
|
||||
# IDS Docs
|
||||
src/ifcopenshell-python/test/build
|
||||
|
||||
+1
-4
@@ -8,9 +8,6 @@
|
||||
[submodule "src/svgfill"]
|
||||
path = src/svgfill
|
||||
url = https://github.com/IfcOpenShell/svgfill
|
||||
[submodule "src/ifcopenshell-python/ifcopenshell/Sample-BIM-Files"]
|
||||
path = src/ifcopenshell-python/ifcopenshell/Sample-BIM-Files
|
||||
url = https://github.com/atomczak/Sample-BIM-Files
|
||||
[submodule "src/ifcopenshell-python/test/Sample-BIM-Files"]
|
||||
path = src/ifcopenshell-python/test/Sample-BIM-Files
|
||||
url = https://github.com/atomczak/Sample-BIM-Files
|
||||
url = https://github.com/IfcOpenShell/ids-test-files
|
||||
+5
-3
@@ -23,9 +23,11 @@ RUN echo "deb http://archive.ubuntu.com/ubuntu focal-proposed main restricted" |
|
||||
echo "deb http://archive.ubuntu.com/ubuntu focal-proposed multiverse" | tee -a /etc/apt/sources.list; \
|
||||
apt-get -qq update; \
|
||||
apt-get -y install tzdata dos2unix rsync; \
|
||||
apt-get -y install python3 libxml2 liboce-foundation11 liboce-modeling11 liboce-ocaf11 liboce-visualization11 \
|
||||
liboce-ocaf-lite11 libpython3.8 libboost-system1.67.0 libboost-program-options1.67.0 \
|
||||
libboost-regex1.67.0 libboost-thread1.67.0 libboost-date-time1.67.0; \
|
||||
apt-get -y install python3 libxml2 libpython3.8 \
|
||||
libboost-all-dev \
|
||||
libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev \
|
||||
libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \
|
||||
libhdf5-serial-dev ; \
|
||||
rm -rf /var/lib/apt/lists/* ;
|
||||
|
||||
COPY . /home/IfcOpenShell/
|
||||
|
||||
@@ -9,12 +9,20 @@ For more information, see
|
||||
* [http://ifcopenshell.org](http://ifcopenshell.org)
|
||||
* [http://academy.ifcopenshell.org](http://academy.ifcopenshell.org)
|
||||
|
||||
[](https://travis-ci.org/IfcOpenShell/IfcOpenShell)
|
||||
| Service | Status |
|
||||
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Anaconda Daily Build | [](https://anaconda.org/ifcopenshell/ifcopenshell) |
|
||||
| Anaconda v0.7.0 Stable | [](https://anaconda.org/conda-forge/ifcopenshell) |
|
||||
| PyPi Daily Build | [](https://pypi.org/project/ifcopenshell-python/) |
|
||||
| ArchLinux AUR Package | [](https://aur.archlinux.org/packages/ifcopenshell-git) |
|
||||
| BlenderBIM Add-on Chocolatey (under moderation) | [](https://community.chocolatey.org/packages/blenderbim-nightly/) |
|
||||
| Sponsor development on OpenCollective | [](https://opencollective.com/opensourcebim/) |
|
||||
| Docker hub | [](https://hub.docker.com/r/aecgeeks/ifcopenshell) |
|
||||
|
||||
Prerequisites
|
||||
-------------
|
||||
* Git
|
||||
* CMake (2.6 or newer)
|
||||
* CMake (3.1.3 or newer)
|
||||
* Windows: [Visual Studio] 2008 to 2019 (2022 not yet supported by dependency CMake) with C++ toolset (or [Visual C++ Build Tools]) or [MSYS2] + MinGW
|
||||
* *nix: GCC 4.7 or newer, or Clang (any version)
|
||||
|
||||
@@ -23,7 +31,7 @@ Dependencies
|
||||
* [Boost](http://www.boost.org/)
|
||||
* [Open Cascade](https://dev.opencascade.org/) - *optional*, but required for building IfcGeom
|
||||
([official](https://dev.opencascade.org/release), "OCCT", or [community edition](https://github.com/tpaviot/oce), "OCE")
|
||||
For converting IFC representation items into BRep solids and tesselated meshes
|
||||
For converting IFC representation items into BRep solids and tessellated meshes
|
||||
* [OpenCOLLADA](https://github.com/khronosGroup/OpenCOLLADA/) - *optional*
|
||||
For IfcConvert to be able to write tessellated Collada (.dae) files
|
||||
* [SWIG](http://www.swig.org/) and [Python](https://www.python.org/) - *optional*
|
||||
@@ -87,7 +95,7 @@ The following instructions are for Ubuntu, modify as required for other operatin
|
||||
can be experimented with and studied for pointers for other operating systems, but note that this script is not currently
|
||||
meant to be used for a typical IfcOpenShell workspace setup.
|
||||
|
||||
Note 1: It is recommeded to use OCCT for IfcOpenShell. You could use OCE as well, but sometimes it may lag behind OCCT and
|
||||
Note 1: It is recommended to use OCCT for IfcOpenShell. You could use OCE as well, but sometimes it may lag behind OCCT and
|
||||
therefore not compile with the latest IfcOpenShell.
|
||||
|
||||
Note 2: where `make -j` is written, add a number roughly equal to the amount of CPU cores + 1.
|
||||
@@ -161,28 +169,40 @@ If all worked out correctly you can now use IfcOpenShell. See the examples below
|
||||
**1)** Install all dependencies using [Homebrew](https://brew.sh/)
|
||||
|
||||
```{shell}
|
||||
$ brew install boost swig cmake ftgl cgal gmp libaec opencascade
|
||||
$ brew install boost cmake python3 cgal ftgl gmp libaec opencascade swig hdf5 zlib
|
||||
# homebrew automatically links most libraries, except some keg-only ones
|
||||
$ brew link zlib --force
|
||||
```
|
||||
|
||||
**2)** Clone the git repo and its submodules
|
||||
```{shell}
|
||||
$ git clone --recurse-submodules https://github.com/IfcOpenShell/IfcOpenShell.git
|
||||
```
|
||||
**3)** Build IfcOpenShell with flags for Homebrew dependencies (```/usr/local/```)
|
||||
**3)** Build IfcOpenShell with flags for Homebrew dependencies: (``/usr/local/``) for Intel machines with x84_64 architecture,
|
||||
(``/opt/homebrew/``) for Apple Silicon processors with arm64 architecture.
|
||||
```{shell}
|
||||
$ cd /path/to/IfcOpenShell
|
||||
$ mkdir build && cd build
|
||||
$ cmake ../cmake -DOCC_LIBRARY_DIR=/usr/local/lib/ \
|
||||
-DOCC_INCLUDE_DIR=/usr/local/include/opencascade/ \
|
||||
-DCOLLADA_SUPPORT=0 \
|
||||
-DCGAL_INCLUDE_DIR=/usr/local/include/ \
|
||||
-DGMP_LIBRARY_DIR=/usr/local/lib/ \
|
||||
-DMPFR_LIBRARY_DIR=/usr/local/lib/
|
||||
|
||||
$ make -j -lboost_options
|
||||
# set library flags
|
||||
$ export LDFLAGS="$LDFLAGS -Wl,-flat_namespace,-undefined,suppress"
|
||||
$ cmake ../cmake
|
||||
-DPYTHON_EXECUTABLE=/opt/homebrew/bin/python3.10 \
|
||||
-DPYTHON_LIBRARY=/opt/homebrew/opt/python@3.10/Frameworks/Python.framework/Versions/3.10/lib/libpython3.10.dylib \
|
||||
-DPYTHON_INCLUDE_DIR=/opt/homebrew/opt/python@3.10/Frameworks/Python.framework/Versions/3.10/include/python3.10/ \
|
||||
-DOCC_LIBRARY_DIR=/opt/homebrew/lib/ \
|
||||
-DOCC_INCLUDE_DIR=/opt/homebrew/include/opencascade/ \
|
||||
-DCGAL_INCLUDE_DIR=/opt/homebrew/include/ \
|
||||
-DGMP_LIBRARY_DIR=/opt/homebrew/lib/ \
|
||||
-DMPFR_LIBRARY_DIR=/opt/homebrew/lib/ \
|
||||
-DHDF5_LIBRARY_DIR=/opt/homebrew/lib/ \
|
||||
-DHDF5_INCLUDE_DIR=/opt/homebrew/include/ \
|
||||
-DCOLLADA_SUPPORT=0
|
||||
# `sysctl -n hw.ncpu` returns the number of cpu cores on macOS
|
||||
$ make -j$(sysctl -n hw.ncpu)
|
||||
```
|
||||
|
||||
Note: Make sure to compile using XCode, rather than a ```brew``` installed C/C++ compiler.
|
||||
Note: Make sure to compile using the XCode provided Apple Clang compiler, installed with `xcode-select --install
|
||||
`, rather than a ```brew``` installed C/C++ compiler.
|
||||
|
||||
Installing IfcOpenShell with Conda
|
||||
----------------------------------
|
||||
@@ -271,35 +291,35 @@ Also available are a series of utilities that are based on or related to IfcOpen
|
||||
|
||||
Those marked with an asterisk are part of IfcOpenShell.
|
||||
|
||||
Name | License
|
||||
--- | ---
|
||||
bcf | LGPL-3.0-or-later
|
||||
blenderbim | GPL-3.0-or-later
|
||||
bsdd | LGPL-3.0-or-later
|
||||
ifc2ca | LGPL-3.0-or-later
|
||||
ifc4d | LGPL-3.0-or-later
|
||||
ifc5d | LGPL-3.0-or-later
|
||||
ifcbimtester | LGPL-3.0-or-later
|
||||
ifcblender | LGPL-3.0-or-later\*
|
||||
ifccityjson | LGPL-3.0-or-later
|
||||
ifcclash | LGPL-3.0-or-later
|
||||
ifccobie | LGPL-3.0-or-later
|
||||
ifcconvert | LGPL-3.0-or-later\*
|
||||
ifccsv | LGPL-3.0-or-later
|
||||
ifcdiff | LGPL-3.0-or-later
|
||||
ifcfm | LGPL-3.0-or-later
|
||||
ifcgeom | LGPL-3.0-or-later\*
|
||||
ifcgeom\_schema\_agnostic | LGPL-3.0-or-later\*
|
||||
ifcgeomserver | LGPL-3.0-or-later\*
|
||||
ifcjni | LGPL-3.0-or-later\*
|
||||
ifcmax | LGPL-3.0-or-later\*
|
||||
ifcopenshell-python | LGPL-3.0-or-later\*
|
||||
ifcparse | LGPL-3.0-or-later\*
|
||||
ifcpatch | LGPL-3.0-or-later
|
||||
ifcsverchok | GPL-3.0-or-later
|
||||
ifcwrap | LGPL-3.0-or-later\*
|
||||
qtviewer | LGPL-3.0-or-later\*
|
||||
serializers | LGPL-3.0-or-later\*
|
||||
| Name | License |
|
||||
| ------------------------- | ------------------- |
|
||||
| bcf | LGPL-3.0-or-later |
|
||||
| blenderbim | GPL-3.0-or-later |
|
||||
| bsdd | LGPL-3.0-or-later |
|
||||
| ifc2ca | LGPL-3.0-or-later |
|
||||
| ifc4d | LGPL-3.0-or-later |
|
||||
| ifc5d | LGPL-3.0-or-later |
|
||||
| ifcbimtester | LGPL-3.0-or-later |
|
||||
| ifcblender | LGPL-3.0-or-later\* |
|
||||
| ifccityjson | LGPL-3.0-or-later |
|
||||
| ifcclash | LGPL-3.0-or-later |
|
||||
| ifccobie | LGPL-3.0-or-later |
|
||||
| ifcconvert | LGPL-3.0-or-later\* |
|
||||
| ifccsv | LGPL-3.0-or-later |
|
||||
| ifcdiff | LGPL-3.0-or-later |
|
||||
| ifcfm | LGPL-3.0-or-later |
|
||||
| ifcgeom | LGPL-3.0-or-later\* |
|
||||
| ifcgeom\_schema\_agnostic | LGPL-3.0-or-later\* |
|
||||
| ifcgeomserver | LGPL-3.0-or-later\* |
|
||||
| ifcjni | LGPL-3.0-or-later\* |
|
||||
| ifcmax | LGPL-3.0-or-later\* |
|
||||
| ifcopenshell-python | LGPL-3.0-or-later\* |
|
||||
| ifcparse | LGPL-3.0-or-later\* |
|
||||
| ifcpatch | LGPL-3.0-or-later |
|
||||
| ifcsverchok | GPL-3.0-or-later |
|
||||
| ifcwrap | LGPL-3.0-or-later\* |
|
||||
| qtviewer | LGPL-3.0-or-later\* |
|
||||
| serializers | LGPL-3.0-or-later\* |
|
||||
|
||||
[LGPL]: https://github.com/IfcOpenShell/IfcOpenShell/tree/master/COPYING.LESSER "LGPL-3.0-or-later"
|
||||
[IFC]: https://technical.buildingsmart.org/standards/ifc/ "IFC"
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Do not remove this test for UTF-8: if “Ω” doesn’t appear as greek uppercase omega letter enclosed in quotation marks, you should use an editor that supports UTF-8, not this one. -->
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>blenderbim-nightly</id>
|
||||
<version>blenderbim_build_version</version>
|
||||
<packageSourceUrl>https://github.com/IfcOpenShell/IfcOpenShell</packageSourceUrl>
|
||||
<owners>fbpyr</owners>
|
||||
<!-- == SOFTWARE SPECIFIC SECTION == -->
|
||||
<title>BlenderBIM install nightly</title>
|
||||
<authors>Dion Moult and many more, see: https://github.com/IfcOpenShell/IfcOpenShell/graphs/contributors</authors>
|
||||
<projectUrl>https://github.com/IfcOpenShell/IfcOpenShell</projectUrl>
|
||||
<iconUrl>https://rawcdn.githack.com/fbpyr/IfcOpenShell/9662598ad4a7e4b6cd8980e430d07263835d0eda/choco/blenderbim/blenderbim.png</iconUrl>
|
||||
<!-- <copyright>Year Software Vendor</copyright> -->
|
||||
<licenseUrl>https://github.com/IfcOpenShell/IfcOpenShell/blob/v0.7.0/COPYING</licenseUrl>
|
||||
<requireLicenseAcceptance>true</requireLicenseAcceptance>
|
||||
<projectSourceUrl>https://github.com/IfcOpenShell/IfcOpenShell</projectSourceUrl>
|
||||
<docsUrl>https://blenderbim.org/docs/</docsUrl>
|
||||
<!--<mailingListUrl></mailingListUrl>-->
|
||||
<bugTrackerUrl>https://github.com/IfcOpenShell/IfcOpenShell/issues</bugTrackerUrl>
|
||||
<tags>blender bim blenderbim python opensource foss</tags>
|
||||
<summary>opensource bim</summary>
|
||||
<description>opensource bim addon for blender</description>
|
||||
<!-- <releaseNotes>__REPLACE_OR_REMOVE__MarkDown_Okay</releaseNotes> -->
|
||||
|
||||
<dependencies>
|
||||
<dependency id="blender" version="latest_blender_version_maj_min_pat" />
|
||||
<dependency id="7zip" version="22.1"/>
|
||||
</dependencies>
|
||||
</metadata>
|
||||
<files>
|
||||
<file src="tools\**" target="tools" />
|
||||
</files>
|
||||
</package>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
@@ -0,0 +1,79 @@
|
||||
import datetime
|
||||
import re
|
||||
import sys
|
||||
from urllib import request
|
||||
|
||||
|
||||
def request_repo_info(url: str):
|
||||
req = request.Request(url)
|
||||
resp = request.urlopen(req)
|
||||
if not resp.status == 200:
|
||||
print(f"[ERROR] could not contact server: {url}")
|
||||
quit(1)
|
||||
return resp
|
||||
|
||||
|
||||
if sys.argv[1] == "--do_choco_release?":
|
||||
now = datetime.datetime.now()
|
||||
blenderbim_date = now.strftime("%y%m%d")
|
||||
url = "https://github.com/IfcOpenShell/IfcOpenShell/releases/latest"
|
||||
resp = request_repo_info(url)
|
||||
if blenderbim_date in resp.url:
|
||||
print("do_choco_release", end="")
|
||||
|
||||
|
||||
elif sys.argv[1] == "--latest_blender_release_maj_min_pat?":
|
||||
re_blender_version_min_maj_pat = r"Latest Version.+<span>Blender (\d+\.\d+\.\d+)</span>"
|
||||
url = "https://community.chocolatey.org/packages/blender"
|
||||
resp = request_repo_info(url)
|
||||
html_txt = str(resp.read())
|
||||
found = re.findall(re_blender_version_min_maj_pat, html_txt)
|
||||
if found:
|
||||
print(found[0], end="")
|
||||
|
||||
|
||||
elif sys.argv[1] == "--latest_blender_release_maj_min?":
|
||||
re_blender_version_min_maj = r"Latest Version.+<span>Blender (\d+\.\d+)\..+</span>"
|
||||
url = "https://community.chocolatey.org/packages/blender"
|
||||
resp = request_repo_info(url)
|
||||
html_txt = str(resp.read())
|
||||
found = re.findall(re_blender_version_min_maj, html_txt)
|
||||
if found:
|
||||
print(found[0], end="")
|
||||
|
||||
|
||||
elif sys.argv[1] == "--latest_blender_python_version_maj_min?":
|
||||
# get latest blender version first
|
||||
re_blender_version_min_maj_pat = r"Latest Version.+<span>Blender (\d+\.\d+\.\d+)</span>"
|
||||
url = "https://community.chocolatey.org/packages/blender"
|
||||
resp = request_repo_info(url)
|
||||
html_txt = str(resp.read())
|
||||
found = re.findall(re_blender_version_min_maj_pat, html_txt)
|
||||
if found:
|
||||
latest_blender_version_tag = f"v{found[0]}"
|
||||
re_blender_python_version_maj_min = r"SET\(PYTHON_VERSION (\d+.\d+) "
|
||||
url = f"https://raw.githubusercontent.com/blender/blender/{latest_blender_version_tag}/build_files/cmake/Modules/FindPythonLibsUnix.cmake"
|
||||
resp = request_repo_info(url)
|
||||
html_txt = str(resp.read())
|
||||
found = re.findall(re_blender_python_version_maj_min, html_txt)
|
||||
if found:
|
||||
print(found[0], end="")
|
||||
|
||||
|
||||
elif sys.argv[1] == "--pyver?":
|
||||
# get latest blender version first
|
||||
re_blender_version_min_maj_pat = r"Latest Version.+<span>Blender (\d+\.\d+\.\d+)</span>"
|
||||
url = "https://community.chocolatey.org/packages/blender"
|
||||
resp = request_repo_info(url)
|
||||
html_txt = str(resp.read())
|
||||
found = re.findall(re_blender_version_min_maj_pat, html_txt)
|
||||
if found:
|
||||
latest_blender_version_tag = f"v{found[0]}"
|
||||
re_blender_python_version_maj_min = r"SET\(PYTHON_VERSION (\d+.\d+) "
|
||||
url = f"https://raw.githubusercontent.com/blender/blender/{latest_blender_version_tag}/build_files/cmake/Modules/FindPythonLibsUnix.cmake"
|
||||
resp = request_repo_info(url)
|
||||
html_txt = str(resp.read())
|
||||
found = re.findall(re_blender_python_version_maj_min, html_txt)
|
||||
if found:
|
||||
print(f"py{found[0].replace('.', '')}", end="")
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
print("[INFO] inserting dynamic chocolatey package parameters")
|
||||
HERE_DIR = Path(__file__).parent.absolute()
|
||||
# print(f"[INFO] HERE_DIR: {HERE_DIR}")
|
||||
|
||||
topics = {
|
||||
"spec": {
|
||||
"path": HERE_DIR / "blenderbim.nuspec",
|
||||
"env_vars": {
|
||||
"latest_blender_version_maj_min_pat",
|
||||
"blenderbim_build_version",
|
||||
},
|
||||
},
|
||||
"install": {
|
||||
"path": HERE_DIR / "tools" / "chocolateyinstall.ps1",
|
||||
"env_vars": {
|
||||
"url_blenderbim_py310_win_zip",
|
||||
"sha256sum_blenderbim_py310_win_zip",
|
||||
"latest_blender_version_maj_min",
|
||||
},
|
||||
},
|
||||
"uninstall": {
|
||||
"path": HERE_DIR / "tools" / "chocolateyuninstall.ps1",
|
||||
"env_vars": {
|
||||
"latest_blender_version_maj_min",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for topic, info in topics.items():
|
||||
print(f"[INFO] {topic}:")
|
||||
for env_var_name in info["env_vars"]:
|
||||
print(f"[INFO] {env_var_name} exists?: {os.environ[env_var_name]}")
|
||||
|
||||
with open(info["path"], encoding="utf-8") as txt:
|
||||
content = txt.read()
|
||||
|
||||
for env_var_name in info["env_vars"]:
|
||||
# print(f"[INFO] replace: {env_var_name}")
|
||||
content = content.replace(env_var_name, os.environ[env_var_name])
|
||||
|
||||
with open(info["path"], "w", encoding="utf-8") as txt:
|
||||
txt.write(content)
|
||||
print(f"[INFO] written: {info['path']}")
|
||||
|
||||
print("[INFO] inserting dynamic chocolatey package parameters successful")
|
||||
@@ -0,0 +1,11 @@
|
||||
$processName = "blender"
|
||||
$blenderIsRunning = Get-Process -Name $processName -ErrorAction SilentlyContinue
|
||||
|
||||
if($blenderIsRunning -eq $null) {
|
||||
echo "Blender is not running ready for next action."
|
||||
}
|
||||
else {
|
||||
Write-Warning "$processName is still running - installer cannot safely proceed to next action."
|
||||
Write-Warning "Please retry after closing all running blender applications."
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$url64 = 'url_blenderbim_py310_win_zip'
|
||||
$checksum64 = 'sha256sum_blenderbim_py310_win_zip'
|
||||
$checksumType64 = 'sha256'
|
||||
|
||||
$appDataUserDir = [System.Environment]::GetEnvironmentVariable('appdata')
|
||||
$unzipTargetDir = "$appDataUserDir\Blender Foundation\Blender\latest_blender_version_maj_min\scripts\addons"
|
||||
|
||||
$chocoBaseDir = [System.Environment]::GetEnvironmentVariable('ChocolateyInstall')
|
||||
$addonEnable = "$chocoBaseDir\lib\blenderbim\tools\enable_blenderbim_addon.py"
|
||||
|
||||
$programFilesDir = [System.Environment]::GetEnvironmentVariable('ProgramFiles')
|
||||
$blenderExePath = "$env:ProgramFiles\Blender Foundation\Blender latest_blender_version_maj_min\blender.exe"
|
||||
|
||||
$processName = "blender"
|
||||
$blenderIsRunning = Get-Process -Name $processName -ErrorAction SilentlyContinue
|
||||
|
||||
|
||||
if($blenderIsRunning -eq $null) {
|
||||
echo "attempting to install blenderbim."
|
||||
Install-ChocolateyZipPackage -PackageName $env:ChocolateyPackageName -Url64 $url64 -Checksum64 $checksum64 -checksumType $checksumType64 -UnzipLocation $unzipTargetDir
|
||||
Start-Process -FilePath $blenderExePath -ArgumentList "-b", "-y", "--python", "$addonEnable"
|
||||
}
|
||||
else {
|
||||
Write-Warning "$processName is still running - blenderbim cannot be safely installed."
|
||||
Write-Warning "Please retry after closing all running blender applications."
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
$appDataUserDir = [System.Environment]::GetEnvironmentVariable('appdata')
|
||||
$unzippedDir = "$appDataUserDir\Blender Foundation\Blender\latest_blender_version_maj_min\scripts\addons\blenderbim"
|
||||
|
||||
$chocoBaseDir = [System.Environment]::GetEnvironmentVariable('ChocolateyInstall')
|
||||
$addonDisable = "$chocoBaseDir\lib\blenderbim\tools\disable_blenderbim_addon.py"
|
||||
|
||||
$programFilesDir = [System.Environment]::GetEnvironmentVariable('ProgramFiles')
|
||||
$blenderExePath = "$env:ProgramFiles\Blender Foundation\Blender latest_blender_version_maj_min\blender.exe"
|
||||
|
||||
$processName = "blender"
|
||||
$blenderIsRunning = Get-Process -Name $processName -ErrorAction SilentlyContinue
|
||||
|
||||
if($blenderIsRunning -eq $null) {
|
||||
echo "attempting to uninstall blenderbim."
|
||||
Start-Process -FilePath $blenderExePath -ArgumentList "-b", "-y", "--python", "$addonDisable"
|
||||
Remove-Item -LiteralPath $unzippedDir -Force -Recurse
|
||||
}
|
||||
else {
|
||||
Write-Warning "$processName is still running - blenderbim cannot be safely uninstalled."
|
||||
Write-Warning "Please retry after closing all running blender applications."
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import bpy
|
||||
|
||||
|
||||
bpy.ops.preferences.addon_disable(module='blenderbim')
|
||||
bpy.ops.wm.save_userpref()
|
||||
|
||||
print("disabled blenderbim addon successfully.")
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import bpy
|
||||
|
||||
|
||||
bpy.ops.preferences.addon_enable(module='blenderbim')
|
||||
bpy.ops.wm.save_userpref()
|
||||
|
||||
print("enabled blenderbim addon successfully.")
|
||||
|
||||
+85
-35
@@ -38,7 +38,7 @@ MESSAGE(STATUS "Autodesk 3ds Max SDK found at ${max_sdk}")
|
||||
set(HAS_MAX TRUE)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
OPTION(MINIMAL_BUILD "The build is to make a minimal version of IFC converter from OCCT into IFC." OFF)
|
||||
OPTION(COLLADA_SUPPORT "Build IfcConvert with COLLADA support (requires OpenCOLLADA)." ON)
|
||||
OPTION(IFCXML_SUPPORT "Build IfcParse with ifcXML support (requires libxml2)." ON)
|
||||
OPTION(ENABLE_BUILD_OPTIMIZATIONS "Enable certain compiler and linker optimizations on RelWithDebInfo and Release builds." OFF)
|
||||
@@ -53,6 +53,7 @@ OPTION(HDF5_SUPPORT "Enable HDF5 support (requires HDF5, zlib)" ON)
|
||||
OPTION(USE_VLD "Use Visual Leak Detector for debugging memory leaks, MSVC-only." OFF)
|
||||
OPTION(USE_MMAP "Adds a command line options to parse IFC files from memory mapped files using Boost.Iostreams" OFF)
|
||||
OPTION(BUILD_SHARED_LIBS "Build IfcParse and IfcGeom as shared libs (SO/DLL)." OFF)
|
||||
OPTION(MSVC_PARALLEL_BUILD "Multi-threaded compilation in Microsoft Visual Studio (/MP)" OFF)
|
||||
if (${HAS_MAX})
|
||||
OPTION(BUILD_IFCMAX "Build IfcMax, a 3ds Max plug-in, Windows-only." OFF)
|
||||
endif()
|
||||
@@ -66,6 +67,10 @@ IF(NOT CMAKE_BUILD_TYPE)
|
||||
SET(CMAKE_BUILD_TYPE "Release")
|
||||
ENDIF()
|
||||
|
||||
if(MSVC AND MSVC_PARALLEL_BUILD)
|
||||
add_definitions("/MP")
|
||||
endif()
|
||||
|
||||
# QtViewer requires Qt5
|
||||
OPTION(BUILD_QTVIEWER "Build IfcOpenShell Qt GUI Viewer" OFF)
|
||||
include(GNUInstallDirs)
|
||||
@@ -121,6 +126,7 @@ ENDMACRO()
|
||||
|
||||
UNIFY_ENVVARS_AND_CACHE(OCC_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(OCC_LIBRARY_DIR)
|
||||
if (NOT MINIMAL_BUILD)
|
||||
UNIFY_ENVVARS_AND_CACHE(OPENCOLLADA_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(OPENCOLLADA_LIBRARY_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(LIBXML2_INCLUDE_DIR)
|
||||
@@ -130,17 +136,20 @@ UNIFY_ENVVARS_AND_CACHE(PYTHON_EXECUTABLE)
|
||||
UNIFY_ENVVARS_AND_CACHE(HDF5_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARY_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARIES)
|
||||
endif()
|
||||
UNIFY_ENVVARS_AND_CACHE(BOOST_ROOT)
|
||||
UNIFY_ENVVARS_AND_CACHE(BOOST_LIBRARYDIR)
|
||||
|
||||
if (NOT MINIMAL_BUILD)
|
||||
UNIFY_ENVVARS_AND_CACHE(CGAL_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(CGAL_LIBRARY_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(GMP_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(GMP_LIBRARY_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(MPFR_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(MPFR_LIBRARY_DIR)
|
||||
endif()
|
||||
|
||||
if (GLTF_SUPPORT AND BUILD_CONVERT)
|
||||
if (NOT MINIMAL_BUILD AND GLTF_SUPPORT AND BUILD_CONVERT)
|
||||
UNIFY_ENVVARS_AND_CACHE(JSON_INCLUDE_DIR)
|
||||
FIND_FILE(json_hpp "json.hpp" ${JSON_INCLUDE_DIR}/nlohmann)
|
||||
IF(json_hpp)
|
||||
@@ -201,10 +210,12 @@ if(USE_MMAP)
|
||||
add_definitions(-DUSE_MMAP)
|
||||
endif()
|
||||
|
||||
if (NOT MINIMAL_BUILD)
|
||||
# libxml2 is required for IFCXML (optional) and SVGFILL (mandatory)
|
||||
find_package(LibXml2 REQUIRED)
|
||||
find_package(LibXml2 REQUIRED)
|
||||
endif()
|
||||
|
||||
if (IFCXML_SUPPORT)
|
||||
if (NOT MINIMAL_BUILD AND IFCXML_SUPPORT)
|
||||
add_definitions(-DWITH_IFCXML)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_IFCXML)
|
||||
endif()
|
||||
@@ -346,7 +357,7 @@ endif()
|
||||
|
||||
endif(BUILD_IFCGEOM)
|
||||
|
||||
IF(COLLADA_SUPPORT)
|
||||
IF(NOT MINIMAL_BUILD AND COLLADA_SUPPORT)
|
||||
# Find OpenCOLLADA
|
||||
IF("${OPENCOLLADA_INCLUDE_DIR}" STREQUAL "")
|
||||
MESSAGE(STATUS "No OpenCOLLADA include directory specified")
|
||||
@@ -426,7 +437,7 @@ IF(COLLADA_SUPPORT)
|
||||
ENDIF()
|
||||
ENDIF()
|
||||
|
||||
if(HDF5_SUPPORT)
|
||||
if(NOT MINIMAL_BUILD AND HDF5_SUPPORT)
|
||||
IF("${HDF5_INCLUDE_DIR}" STREQUAL "")
|
||||
MESSAGE(STATUS "No HDF5 include directory specified")
|
||||
ElSE()
|
||||
@@ -441,30 +452,60 @@ if(HDF5_SUPPORT)
|
||||
|
||||
if (HDF5_LIBRARY_DIR)
|
||||
# result of the HDF5 ctest package
|
||||
|
||||
if (WIN32)
|
||||
set(zlib_post lib)
|
||||
set(lib_ext lib)
|
||||
else()
|
||||
set(lib_ext a)
|
||||
# Find zlib using cmake find_library. How should this be implemented?
|
||||
# FIND_LIBRARY(NAMES z libz libz_debug PATHS ... NO_DEFAULT_PATH)
|
||||
|
||||
|
||||
if ("$ENV{CONDA_BUILD}" STREQUAL "")
|
||||
# result of the HDF5 ctest package
|
||||
|
||||
if (WIN32)
|
||||
set(zlib_post lib)
|
||||
set(lib_ext lib)
|
||||
else()
|
||||
set(lib_ext a)
|
||||
endif()
|
||||
|
||||
if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
|
||||
set(debug_postfix "_debug")
|
||||
endif()
|
||||
|
||||
SET(HDF5_LIBRARIES
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5_cpp${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libz${zlib_post}${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libsz${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libaec${debug_postfix}.${lib_ext}"
|
||||
)
|
||||
|
||||
ELSE()
|
||||
MESSAGE(STATUS "Packaging hdf5 and zlib for conda distribution")
|
||||
if (WIN32)
|
||||
# Windows
|
||||
set(zlib_post zlib)
|
||||
set(lib_ext lib)
|
||||
elseif(${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
|
||||
# macOS
|
||||
set(zlib_post libz)
|
||||
set(lib_ext dylib)
|
||||
else()
|
||||
# linux
|
||||
set(zlib_post libz)
|
||||
set(lib_ext so)
|
||||
endif()
|
||||
|
||||
SET(HDF5_LIBRARIES
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5_cpp.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/${zlib_post}.${lib_ext}"
|
||||
)
|
||||
endif()
|
||||
|
||||
if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
|
||||
set(debug_postfix "_debug")
|
||||
endif()
|
||||
|
||||
SET(HDF5_LIBRARIES
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5_cpp${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libz${zlib_post}${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libsz${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libaec${debug_postfix}.${lib_ext}"
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
if (NOT HDF5_LIBRARIES)
|
||||
# debian default
|
||||
|
||||
|
||||
SET(HDF5_LIBRARIES
|
||||
/usr/lib/x86_64-linux-gnu/hdf5/serial/libhdf5_cpp.so
|
||||
/usr/lib/x86_64-linux-gnu/hdf5/serial/libhdf5.so
|
||||
@@ -579,13 +620,16 @@ function(files_for_ifc_version IFC_VERSION RESULT_NAME)
|
||||
endfunction()
|
||||
|
||||
if(NOT SCHEMA_VERSIONS)
|
||||
set(SCHEMA_VERSIONS "2x3" "4" "4x1" "4x2" "4x3_rc1" "4x3_rc2" "4x3_rc3" "4x3_rc4")
|
||||
set(SCHEMA_VERSIONS "2x3" "4" "4x1" "4x2" "4x3_rc1" "4x3_rc2" "4x3_rc3" "4x3_rc4" "4x3")
|
||||
endif()
|
||||
|
||||
foreach(s ${SCHEMA_VERSIONS})
|
||||
add_definitions(-DHAS_SCHEMA_${s})
|
||||
endforeach()
|
||||
|
||||
string(REPLACE ";" ")(" schema_version_seq "(${SCHEMA_VERSIONS})")
|
||||
ADD_DEFINITIONS(-DSCHEMA_SEQ=${schema_version_seq})
|
||||
|
||||
if(COMPILE_SCHEMA)
|
||||
# @todo, this appears to be untested at the moment
|
||||
|
||||
@@ -655,7 +699,7 @@ if (BUILD_IFCGEOM)
|
||||
endforeach()
|
||||
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
|
||||
endif()
|
||||
if (BUILD_CONVERT)
|
||||
if (BUILD_CONVERT OR BUILD_IFCPYTHON)
|
||||
foreach(s ${SCHEMA_VERSIONS})
|
||||
set(SERIALIZER_SCHEMA_LIBRARIES ${SERIALIZER_SCHEMA_LIBRARIES} Serializers_ifc${s})
|
||||
endforeach()
|
||||
@@ -728,7 +772,7 @@ TARGET_LINK_LIBRARIES(IfcGeom IfcParse ${IFCGEOM_SCHEMA_LIBRARIES} ${CMAKE_THREA
|
||||
|
||||
endif(BUILD_IFCGEOM)
|
||||
|
||||
if (BUILD_CONVERT)
|
||||
if (BUILD_CONVERT OR BUILD_IFCPYTHON)
|
||||
|
||||
# Serializers
|
||||
file(GLOB SERIALIZERS_H_FILES ../src/serializers/*.h)
|
||||
@@ -749,6 +793,10 @@ set_target_properties(Serializers PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS $
|
||||
|
||||
TARGET_LINK_LIBRARIES(Serializers ${SERIALIZER_SCHEMA_LIBRARIES} ${OPENCOLLADA_LIBRARIES})
|
||||
|
||||
endif(BUILD_CONVERT or BUILD_IFCPYTHON)
|
||||
|
||||
if (BUILD_CONVERT)
|
||||
|
||||
# IfcConvert
|
||||
file(GLOB IFCCONVERT_CPP_FILES ../src/ifcconvert/*.cpp)
|
||||
file(GLOB IFCCONVERT_H_FILES ../src/ifcconvert/*.h)
|
||||
@@ -774,7 +822,7 @@ INSTALL(TARGETS IfcConvert
|
||||
endif(BUILD_CONVERT)
|
||||
|
||||
# IfcGeomServer
|
||||
if(BUILD_GEOMSERVER)
|
||||
if(NOT MINIMAL_BUILD AND BUILD_GEOMSERVER)
|
||||
|
||||
file(GLOB CPP_FILES ../src/ifcgeomserver/*.cpp)
|
||||
file(GLOB H_FILES ../src/ifcgeomserver/*.h)
|
||||
@@ -805,7 +853,8 @@ if (GIT_FOUND)
|
||||
)
|
||||
string(REPLACE "\n" ";" git_branch_list "${git_branches}")
|
||||
foreach(git_branch_candidate IN ITEMS ${git_branch_list})
|
||||
string(STRIP "${git_branch_candidate}" git_branch_candidate_2)
|
||||
string(REPLACE "*" "" git_branch_candidate_temp "${git_branch_candidate}")
|
||||
string(STRIP "${git_branch_candidate_temp}" git_branch_candidate_2)
|
||||
if (NOT git_branch_candidate_2 MATCHES "^HEAD$")
|
||||
set(git_branch ${git_branch_candidate_2})
|
||||
endif()
|
||||
@@ -828,7 +877,7 @@ IF(BUILD_DOCUMENTATION)
|
||||
ADD_SUBDIRECTORY(../docs docs)
|
||||
ENDIF()
|
||||
|
||||
IF(BUILD_IFCPYTHON)
|
||||
IF(NOT MINIMAL_BUILD AND BUILD_IFCPYTHON)
|
||||
ADD_SUBDIRECTORY(../src/ifcwrap ifcwrap)
|
||||
ENDIF()
|
||||
|
||||
@@ -836,11 +885,12 @@ IF(BUILD_EXAMPLES)
|
||||
ADD_SUBDIRECTORY(../src/examples examples)
|
||||
ENDIF()
|
||||
|
||||
IF(BUILD_IFCMAX)
|
||||
IF(NOT MINIMAL_BUILD AND BUILD_IFCMAX)
|
||||
ADD_SUBDIRECTORY(../src/ifcmax ifcmax)
|
||||
ENDIF()
|
||||
|
||||
if (NOT MINIMAL_BUILD)
|
||||
ADD_SUBDIRECTORY(../src/svgfill svgfill)
|
||||
endif()
|
||||
|
||||
# CMake installation targets
|
||||
INSTALL(FILES ${IFCPARSE_H_FILES}
|
||||
@@ -912,12 +962,12 @@ set(CPACK_SOURCE_GENERATOR "TGZ")
|
||||
|
||||
FOREACH(COMPONENT IN ITEMS ${BOOST_COMPONENTS})
|
||||
string(REPLACE "_" "-" COMP ${COMPONENT})
|
||||
set(BOOST_DEPS "${BOOST_DEPS}, libboost-${COMP}1.67.0")
|
||||
set(BOOST_DEPS "${BOOST_DEPS}, libboost-${COMP}-dev")
|
||||
endforeach(COMPONENT)
|
||||
|
||||
set(CPACK_DEBIAN_PACKAGE_NAME "${PROJECT_NAME}")
|
||||
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${CPACK_PACKAGE_CONTACT}")
|
||||
set(CPACK_DEBIAN_PACKAGE_DEPENDS "python3, libxml2, liboce-foundation11, liboce-modeling11, liboce-ocaf11, liboce-visualization11, liboce-ocaf-lite11, libpython3.8 ${BOOST_DEPS}")
|
||||
set(CPACK_DEBIAN_PACKAGE_DEPENDS "python3, libxml2, libocct-foundation-dev, libocct-modeling-algorithms-dev, libocct-modeling-data-dev, libocct-ocaf-dev, libocct-visualization-dev, libocct-data-exchange-dev, libhdf5-serial-dev, libpython3.8 ${BOOST_DEPS}")
|
||||
set(CPACK_DEBIAN_PACKAGE_DESCRIPTION_SUMMARY "${CPACK_PACKAGE_DESCRIPTION_SUMMARY}")
|
||||
set(CPACK_DEBIAN_PACKAGE_DESCRIPTION "${CPACK_PACKAGE_DESCRIPTION}")
|
||||
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
compile:
|
||||
conda activate conda-build && conda-build -c conda-forge . --keep-old-work --python 3.10.5
|
||||
|
||||
debug:
|
||||
conda activate conda-build && conda-debug -c conda-forge . --python 3.10.4
|
||||
|
||||
env:
|
||||
conda env update --f environment.yml --prune
|
||||
+38
-23
@@ -1,29 +1,44 @@
|
||||
mkdir build && cd build
|
||||
|
||||
REM Remove dot from PY_VER for use in library name
|
||||
REM From https://github.com/tpaviot/pythonocc-core/blob/master/ci/conda/bld.bat
|
||||
set MY_PY_VER=%PY_VER:.=%
|
||||
set LIBXML2="%LIBRARY_PREFIX%/lib/libxml2.lib"
|
||||
|
||||
cmake -G "NMake Makefiles" ^
|
||||
-DCMAKE_INSTALL_PREFIX="%LIBRARY_PREFIX%" ^
|
||||
-DCMAKE_BUILD_TYPE=Release ^
|
||||
-DCMAKE_PREFIX_PATH="%LIBRARY_PREFIX%" ^
|
||||
-DCMAKE_SYSTEM_PREFIX_PATH="%LIBRARY_PREFIX%" ^
|
||||
-DPYTHON_EXECUTABLE="%PYTHON%" ^
|
||||
-DPYTHON_INCLUDE_DIR="%PREFIX%"/include ^
|
||||
-DPYTHON_LIBRARY="%PREFIX%"/libs/python%MY_PY_VER%.lib ^
|
||||
-DBOOST_LIBRARYDIR="%LIBRARY_PREFIX%\lib" ^
|
||||
-DBOOST_INCLUDEDIR="%LIBRARY_PREFIX%\include" ^
|
||||
-DOCC_INCLUDE_DIR="%LIBRARY_PREFIX%\include\oce" ^
|
||||
-DOCC_LIBRARY_DIR="%LIBRARY_PREFIX%\lib" ^
|
||||
-DCOLLADA_SUPPORT=Off ^
|
||||
-DBUILD_EXAMPLES=Off ^
|
||||
-DBUILD_GEOMSERVER=Off ^
|
||||
-DBUILD_CONVERT=Off ^
|
||||
../cmake
|
||||
|
||||
if errorlevel 1 exit 1
|
||||
|
||||
cmake --build . --target INSTALL --config Release
|
||||
cmake -G "Ninja" ^
|
||||
-D CMAKE_BUILD_TYPE:STRING=Release ^
|
||||
-D CMAKE_INSTALL_PREFIX:FILEPATH="%LIBRARY_PREFIX%" ^
|
||||
-D CMAKE_PREFIX_PATH:FILEPATH="%LIBRARY_PREFIX%" ^
|
||||
-D CMAKE_SYSTEM_PREFIX_PATH:FILEPATH="%LIBRARY_PREFIX%" ^
|
||||
-D OCC_INCLUDE_DIR:FILEPATH="%LIBRARY_PREFIX%\include\opencascade" ^
|
||||
-D OCC_LIBRARY_DIR:FILEPATH="%LIBRARY_PREFIX%\lib" ^
|
||||
-D CGAL_INCLUDE_DIR:FILEPATH="%LIBRARY_PREFIX%\include" ^
|
||||
-D GMP_INCLUDE_DIR:FILEPATH="%LIBRARY_PREFIX%\include" ^
|
||||
-D GMP_LIBRARY_DIR:FILEPATH="%LIBRARY_PREFIX%\lib" ^
|
||||
-D MPFR_LIBRARY_DIR:FILEPATH="%LIBRARY_PREFIX%\lib" ^
|
||||
-D COLLADA_SUPPORT=OFF ^
|
||||
-D HDF5_SUPPORT=ON ^
|
||||
-D HDF5_INCLUDE_DIR="%LIBRARY_PREFIX%\include" ^
|
||||
-D HDF5_LIBRARY_DIR="%LIBRARY_PREFIX%\lib" ^
|
||||
-D JSON_INCLUDE_DIR="%LIBRARY_PREFIX%\include" ^
|
||||
-D PYTHON_INCLUDE_DIR=%PREFIX%\include ^
|
||||
-D PYTHON_EXECUTABLE:FILEPATH=%PREFIX%\python.exe ^
|
||||
-D PYTHON_LIBRARY:FILEPATH="%PREFIX%"\libs/python%MY_PY_VER%.lib ^
|
||||
-D BUILD_IFCPYTHON=ON ^
|
||||
-D BUILD_IFCGEOM=ON ^
|
||||
-D COLLADA_SUPPORT:BOOL=OFF ^
|
||||
-D BUILD_EXAMPLES:BOOL=OFF ^
|
||||
-D BUILD_GEOMSERVER:BOOL=OFF ^
|
||||
-D GLTF_SUPPORT:BOOL=ON ^
|
||||
-D BUILD_CONVERT:BOOL=ON ^
|
||||
-D BUILD_IFCMAX:BOOL=OFF ^
|
||||
-D IFCXML_SUPPORT:BOOL=ON ^
|
||||
-D Boost_LIBRARYDIR:FILEPATH="%LIBRARY_PREFIX%\lib" ^
|
||||
-D Boost_INCLUDEDIR:FILEPATH="%LIBRARY_PREFIX%\include" ^
|
||||
-D Boost_USE_STATIC_LIBS:BOOL=OFF ^
|
||||
%SRC_DIR%/cmake
|
||||
|
||||
if errorlevel 1 exit 1
|
||||
|
||||
:: Build and install
|
||||
cmake --build . -- install
|
||||
|
||||
if errorlevel 1 exit 1
|
||||
|
||||
+34
-24
@@ -1,34 +1,44 @@
|
||||
# From https://github.com/tpaviot/pythonocc-core/blob/master/ci/conda/build.sh
|
||||
if [ "$PY3K" == "1" ]; then
|
||||
MY_PY_VER="${PY_VER}m"
|
||||
else
|
||||
MY_PY_VER="${PY_VER}"
|
||||
#!/bin/bash
|
||||
|
||||
declare -a CMAKE_PLATFORM_FLAGS
|
||||
|
||||
if [[ ${HOST} =~ .*linux.* ]]; then
|
||||
CMAKE_PLATFORM_FLAGS+=(-DCMAKE_TOOLCHAIN_FILE=$RECIPE_DIR/cross-linux.cmake)
|
||||
fi
|
||||
|
||||
if [ `uname` == Darwin ]; then
|
||||
PY_LIB="libpython${MY_PY_VER}.dylib"
|
||||
export CFLAGS="$CFLAGS -Wl,-flat_namespace,-undefined,suppress"
|
||||
export CXXFLAGS="$CXXFLAGS -Wl,-flat_namespace,-undefined,suppress"
|
||||
export LDFLAGS="$LDFLAGS -Wl,-flat_namespace,-undefined,suppress"
|
||||
else
|
||||
PY_LIB="libpython${MY_PY_VER}.so"
|
||||
export CFLAGS="$CFLAGS -Wl,-flat_namespace,-undefined,suppress"
|
||||
export CXXFLAGS="$CXXFLAGS -Wl,-flat_namespace,-undefined,suppress"
|
||||
export LDFLAGS="$LDFLAGS -Wl,-flat_namespace,-undefined,suppress"
|
||||
fi
|
||||
|
||||
mkdir build && cd build
|
||||
|
||||
cmake \
|
||||
-DCMAKE_INSTALL_PREFIX=$PREFIX \
|
||||
cmake -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=$PREFIX \
|
||||
${CMAKE_PLATFORM_FLAGS[@]} \
|
||||
-DCMAKE_PREFIX_PATH=$PREFIX \
|
||||
-DCMAKE_SYSTEM_PREFIX_PATH=$PREFIX \
|
||||
-DOCC_INCLUDE_DIR=$PREFIX/include/oce \
|
||||
-DOCC_LIBRARY_DIR=$PREFIX/lib \
|
||||
-DPYTHON_EXECUTABLE:FILEPATH=$PYTHON \
|
||||
-DPYTHON_INCLUDE_DIR:PATH=$PREFIX/include/python$MY_PY_VER \
|
||||
-DPYTHON_LIBRARY:FILEPATH=$PREFIX/lib/${PY_LIB} \
|
||||
-DCOLLADA_SUPPORT=Off \
|
||||
../cmake
|
||||
-DGMP_LIBRARY_DIR=$PREFIX/lib \
|
||||
-DMPFR_LIBRARY_DIR=$PREFIX/lib \
|
||||
-DOCC_INCLUDE_DIR=$PREFIX/include/opencascade \
|
||||
-DOCC_LIBRARY_DIR=$PREFIX/lib \
|
||||
-DHDF5_SUPPORT:BOOL=ON \
|
||||
-DHDF5_INCLUDE_DIR=$PREFIX/include \
|
||||
-DHDF5_LIBRARY_DIR=$PREFIX/lib \
|
||||
-DJSON_INCLUDE_DIR=$PREFIX/include \
|
||||
-DCGAL_INCLUDE_DIR=$PREFIX/include \
|
||||
-DCOLLADA_SUPPORT=0 \
|
||||
-DBUILD_EXAMPLES:BOOL=OFF \
|
||||
-DIFCXML_SUPPORT:BOOL=ON \
|
||||
-DGLTF_SUPPORT:BOOL=ON \
|
||||
-DBUILD_CONVERT:BOOL=ON \
|
||||
-DBUILD_IFCPYTHON:BOOL=ON \
|
||||
-DBUILD_IFCGEOM:BOOL=ON \
|
||||
-DBUILD_GEOMSERVER:BOOL=OFF \
|
||||
-DBOOST_USE_STATIC_LIBS:BOOL=OFF \
|
||||
./cmake
|
||||
|
||||
make -j$CPU_COUNT _ifcopenshell_wrapper
|
||||
cd ifcwrap
|
||||
make install/local
|
||||
ninja
|
||||
|
||||
ninja install
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
CONDA_BUILD_SYSROOT:
|
||||
- /opt/MacOSX10.13.sdk # [osx]
|
||||
@@ -0,0 +1,16 @@
|
||||
# this one is important
|
||||
set(CMAKE_SYSTEM_NAME Linux)
|
||||
set(CMAKE_PLATFORM Linux)
|
||||
|
||||
# specify the cross compiler
|
||||
set(CMAKE_C_COMPILER $ENV{CC})
|
||||
set(CMAKE_CXX_COMPILER $ENV{CXX})
|
||||
|
||||
# where is the target environment
|
||||
set(CMAKE_FIND_ROOT_PATH $ENV{PREFIX} $ENV{BUILD_PREFIX}/$ENV{HOST}/sysroot)
|
||||
|
||||
# search for programs in the build host directories
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
# for libraries and headers in the target directories
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
@@ -0,0 +1,14 @@
|
||||
name: conda-build
|
||||
channels:
|
||||
- conda-forge
|
||||
dependencies:
|
||||
- conda-build
|
||||
- conda-verify
|
||||
- anaconda-client
|
||||
- ninja
|
||||
- doxygen
|
||||
- ripgrep
|
||||
- pip
|
||||
- pip:
|
||||
- --extra-index-url https://lief.s3-website.fr-par.scw.cloud/latest
|
||||
- lief==0.13.0.dev0
|
||||
+50
-30
@@ -1,42 +1,62 @@
|
||||
{% set name = "ifcopenshell" %}
|
||||
{% set version = "0.7.0" %}
|
||||
{% set occt_version = "7.6.2" %}
|
||||
{% set cgal_cpp_version = "5.3" %}
|
||||
{% set hdf5_version = "1.12.1" %}
|
||||
|
||||
package:
|
||||
name: ifcopenshell
|
||||
version: "0.6.0a1"
|
||||
name: {{ name }}
|
||||
version: {{ version }}
|
||||
|
||||
source:
|
||||
git_rev: "v0.6.0a1"
|
||||
git_url: https://github.com/IfcOpenShell/IfcOpenShell
|
||||
path: ..
|
||||
|
||||
build:
|
||||
number: 0
|
||||
features:
|
||||
- vc9 # [win and py27]
|
||||
- vc10 # [win and py34]
|
||||
- vc14 # [win and py35]
|
||||
- vc14 # [win and py36]
|
||||
binary_relocation: false [osx]
|
||||
|
||||
requirements:
|
||||
build:
|
||||
- gcc # [osx]
|
||||
- make
|
||||
- python
|
||||
- oce ==0.18.3
|
||||
- {{ compiler('c') }}
|
||||
- {{ compiler('cxx') }}
|
||||
- ninja >=1.10.2
|
||||
- cmake
|
||||
- swig >=3.0.9
|
||||
- libboost
|
||||
- icu
|
||||
- xmlschema
|
||||
- lxml
|
||||
- numpy
|
||||
run:
|
||||
- libgcc # [osx]
|
||||
- swig >=4.0.2
|
||||
|
||||
host:
|
||||
- python
|
||||
- oce ==0.18.3
|
||||
- libboost
|
||||
- icu
|
||||
- xmlschema
|
||||
- lxml
|
||||
- numpy
|
||||
|
||||
- boost-cpp <=1.74.0
|
||||
- occt
|
||||
- libxml2
|
||||
- cgal-cpp
|
||||
- hdf5
|
||||
- mpfr
|
||||
- gmp # [unix]
|
||||
- mpir # [win]
|
||||
- nlohmann_json
|
||||
|
||||
run:
|
||||
- python
|
||||
- boost-cpp <=1.74.0
|
||||
- occt
|
||||
- libxml2
|
||||
- cgal-cpp
|
||||
- hdf5
|
||||
- mpfr
|
||||
- gmp # [unix]
|
||||
- mpir # [win]
|
||||
- nlohmann_json
|
||||
|
||||
test:
|
||||
imports:
|
||||
- ifcopenshell
|
||||
|
||||
about:
|
||||
home: http://ifcopenshell.org
|
||||
license: LGPL
|
||||
license: LGPL-3.0-or-later
|
||||
license_file: COPYING
|
||||
summary: 'IfcOpenShell is a library to support the IFC file format'
|
||||
description: |
|
||||
IfcOpenShell is an open source (LGPL) software library for
|
||||
working with the Industry Foundation Classes (IFC) file format.
|
||||
doc_url: http://ifcopenshell.org/
|
||||
dev_url: https://github.com/IfcOpenShell/IfcOpenShell
|
||||
|
||||
+12
-8
@@ -23,7 +23,7 @@
|
||||
# This script builds IfcOpenShell and its dependencies #
|
||||
# #
|
||||
# Prerequisites for this script to function correctly: #
|
||||
# * cmake * git * bzip2 * tar * c(++) compilers * yacc * autoconf #
|
||||
# * cmake * git * bzip2 * tar * c(++) compilers * autoconf #
|
||||
# #
|
||||
# if building with USE_OCCT additionally: #
|
||||
# * freetype * glx.h #
|
||||
@@ -72,7 +72,7 @@ PROJECT_NAME = "IfcOpenShell"
|
||||
USE_CURRENT_PYTHON_VERSION = os.getenv("USE_CURRENT_PYTHON_VERSION")
|
||||
ADD_COMMIT_SHA = os.getenv("ADD_COMMIT_SHA")
|
||||
|
||||
PYTHON_VERSIONS = ["3.6.14", "3.7.12", "3.8.12", "3.9.7", "3.10.0"]
|
||||
PYTHON_VERSIONS = ["3.6.14", "3.7.13", "3.8.13", "3.9.11", "3.10.3"]
|
||||
JSON_VERSION = "v3.6.1"
|
||||
OCE_VERSION = "0.18.3"
|
||||
OCCT_VERSION = "7.5.3"
|
||||
@@ -83,8 +83,8 @@ SWIG_VERSION = "4.0.2"
|
||||
OPENCOLLADA_VERSION = "v1.6.68"
|
||||
HDF5_VERSION = "1.12.1"
|
||||
|
||||
GMP_VERSION = "6.1.2"
|
||||
MPFR_VERSION = "3.1.5" # latest is 4.1.0
|
||||
GMP_VERSION = "6.2.1"
|
||||
MPFR_VERSION = "3.1.6" # latest is 4.1.0
|
||||
CGAL_VERSION = "5.3"
|
||||
|
||||
# binaries
|
||||
@@ -97,7 +97,6 @@ cc = "cc"
|
||||
cplusplus = "c++"
|
||||
autoconf = "autoconf"
|
||||
automake = "automake"
|
||||
yacc = "yacc"
|
||||
make = "make"
|
||||
date = "date"
|
||||
curl = "curl"
|
||||
@@ -220,7 +219,7 @@ print("Building:", *sorted(targets, key=lambda t: len(list(v(t)))))
|
||||
|
||||
# Check that required tools are in PATH
|
||||
|
||||
for cmd in [git, bunzip2, tar, cc, cplusplus, autoconf, automake, yacc, make, "patch", "cmake"]:
|
||||
for cmd in [git, bunzip2, tar, cc, cplusplus, autoconf, automake, make, "patch", "cmake"]:
|
||||
if which(cmd) is None:
|
||||
raise ValueError(f"Required tool '{cmd}' not installed or not added to PATH")
|
||||
|
||||
@@ -263,6 +262,11 @@ def run(cmds, cwd=None):
|
||||
|
||||
return stdout.strip()
|
||||
|
||||
if platform.system() == "Darwin":
|
||||
if run(["sw_vers", "-productVersion"]) >= "11.":
|
||||
# Apparently not supported
|
||||
PYTHON_VERSIONS = [pv for pv in PYTHON_VERSIONS if tuple(map(int, pv.split("."))) >= (3, 7)]
|
||||
|
||||
BOOST_VERSION_UNDERSCORE = BOOST_VERSION.replace(".", "_")
|
||||
|
||||
OCE_LOCATION = f"https://github.com/tpaviot/oce/archive/OCE-{OCE_VERSION}.tar.gz"
|
||||
@@ -508,7 +512,7 @@ if USE_OCCT and "occ" in targets:
|
||||
"-DBUILD_MODULE_Draw=0",
|
||||
"-DBUILD_RELEASE_DISABLE_EXCEPTIONS=Off"
|
||||
],
|
||||
download_url = "https://git.dev.opencascade.org/repos/occt.git",
|
||||
download_url = "https://github.com/Open-Cascade-SAS/OCCT",
|
||||
download_name = "occt",
|
||||
download_tool=download_tool_git,
|
||||
patch=None if OCCT_VERSION >= "7.4" else "./patches/occt/enable-exception-handling.patch",
|
||||
@@ -577,7 +581,7 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION:
|
||||
# with the system python because of some threading initialization
|
||||
PYTHON_CONFIGURE_ARGS = []
|
||||
if platform.system() == "Darwin":
|
||||
PYTHON_CONFIGURE_ARGS = ["--disable-static", "--enable-shared"]
|
||||
PYTHON_CONFIGURE_ARGS = ["--enable-shared"]
|
||||
|
||||
for PYTHON_VERSION in PYTHON_VERSIONS:
|
||||
try:
|
||||
|
||||
@@ -646,7 +646,7 @@ class BcfXml:
|
||||
def get_comments(self, guid):
|
||||
comments = {}
|
||||
data = self._read_xml(os.path.join(guid, "markup.bcf"), "markup.xsd")
|
||||
if "Comments" not in data["Topics"]:
|
||||
if "Comments" not in data["Topic"]:
|
||||
return comments
|
||||
|
||||
for item in data["Topic"]["Comments"].get("Comment", []):
|
||||
|
||||
+109
-17
@@ -16,6 +16,24 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
PYTHON:=python
|
||||
PATCH:=patch
|
||||
SED:=sed -i
|
||||
ifeq ($(OS),Windows_NT)
|
||||
HOSTOS:=win
|
||||
else
|
||||
UNAME_S:=$(shell uname -s)
|
||||
ifeq ($(UNAME_S),Linux)
|
||||
HOSTOS:=linux
|
||||
endif
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
HOSTOS:=macos
|
||||
PYTHON:=python3
|
||||
PATCH:=patch -d
|
||||
SED:=sed -i '' -e
|
||||
endif
|
||||
endif
|
||||
|
||||
VERSION:=`date '+%y%m%d'`
|
||||
PYVERSION:=py37
|
||||
|
||||
@@ -38,18 +56,21 @@ HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/linux-64/hpp
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/linux-64/eigenpy-2.6.5-py37h95e2c48_0.tar.bz2
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/linux-64/boost-1.74.0-py37h0379df6_3.tar.bz2
|
||||
LXML_URL:=https://files.pythonhosted.org/packages/30/c0/d0526314971fc661b083ab135747dc68446a3022686da8c16d25fcf6ef07/lxml-4.6.3-cp37-cp37m-manylinux2014_x86_64.whl
|
||||
SHAPELY_URL:=https://files.pythonhosted.org/packages/a2/73/07f507b539527579c026ed5f411c35e287685ce794c4dfb7edf6675cdc24/Shapely-1.8.4-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl
|
||||
endif
|
||||
ifeq ($(PYVERSION), py39)
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/linux-64/hpp-fcl-1.7.5-py39hbcdfc36_0.tar.bz2
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/linux-64/eigenpy-2.6.5-py39h5aed9d1_0.tar.bz2
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/linux-64/boost-1.74.0-py39h5472131_3.tar.bz2
|
||||
LXML_URL:=https://files.pythonhosted.org/packages/19/d9/a69c6aff5673554df48120565a14a50eaa41d29ae03b02faa0b023666318/lxml-4.6.3-cp39-cp39-manylinux2014_x86_64.whl
|
||||
SHAPELY_URL:=https://files.pythonhosted.org/packages/08/12/5af915b2bc0470f4a652ef9978557a0fa8b705b122ccaec6626029e1674c/Shapely-1.8.4-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl
|
||||
endif
|
||||
ifeq ($(PYVERSION), py310)
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.8.0/download/linux-64/hpp-fcl-1.8.0-py310hdaf7e41_1.tar.bz2
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.11/download/linux-64/eigenpy-2.6.11-py310hf3e5c9c_0.tar.bz2
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/linux-64/boost-1.74.0-py310h7c3ba0c_5.tar.bz2
|
||||
LXML_URL:=https://files.pythonhosted.org/packages/25/1e/19b46d8e8881fe0df2e20945d51919eeb1817836d62a90efa8506530e45c/lxml-4.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl
|
||||
SHAPELY_URL:=https://files.pythonhosted.org/packages/60/20/c6321e3da92a4ad9495727c9aa8c423e47295572ccd35ece82d1ac013e28/Shapely-1.8.4-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl
|
||||
endif
|
||||
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/linux-64/assimp-5.0.1-hedfc422_6.tar.bz2
|
||||
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.7/download/linux-64/octomap-1.9.7-h4bd325d_0.tar.bz2
|
||||
@@ -62,42 +83,76 @@ HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/osx-64/hpp-f
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/osx-64/eigenpy-2.6.5-py37h0695097_0.tar.bz2
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/osx-64/boost-1.74.0-py37hd79e0ac_3.tar.bz2
|
||||
LXML_URL:=https://files.pythonhosted.org/packages/1e/3e/f0abc15d5dac50939bccc589aae336d5ead4c72e7ad1039a2e0f3630ea92/lxml-4.6.3-cp37-cp37m-macosx_10_9_x86_64.whl
|
||||
SHAPELY_URL:=https://files.pythonhosted.org/packages/47/a9/7c644e71638bff2bbaa87f05ee62fdf6b4552a55e74bbf731d6210b6d436/Shapely-1.8.4-cp37-cp37m-macosx_10_9_x86_64.whl
|
||||
endif
|
||||
ifeq ($(PYVERSION), py39)
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/osx-64/hpp-fcl-1.7.5-py39h1e32b98_0.tar.bz2
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/osx-64/eigenpy-2.6.5-py39h5405915_0.tar.bz2
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/osx-64/boost-1.74.0-py39ha641261_3.tar.bz2
|
||||
LXML_URL:=https://files.pythonhosted.org/packages/b8/74/a71f7ad72e8db54ce899efab84507b801660750cbbfa6a39e6717557d36a/lxml-4.6.3-cp39-cp39-macosx_10_9_x86_64.whl
|
||||
SHAPELY_URL:=https://files.pythonhosted.org/packages/f5/f9/bf6cc34b02b3c7d3777c5e47fa57d2f69387613bbd8decdf3577a9d83a83/Shapely-1.8.4-cp39-cp39-macosx_10_9_x86_64.whl
|
||||
endif
|
||||
ifeq ($(PYVERSION), py310)
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.8.0/download/osx-64/hpp-fcl-1.8.0-py310h651ac30_1.tar.bz2
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.11/download/osx-64/eigenpy-2.6.11-py310hc03097c_0.tar.bz2
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/osx-64/boost-1.74.0-py310h509978a_5.tar.bz2
|
||||
LXML_URL:=https://files.pythonhosted.org/packages/a1/44/17b7dac7a18807d30e2fe10c3328c152808f5464565e230bfd0e77f178c6/lxml-4.8.0-cp310-cp310-macosx_10_15_x86_64.whl
|
||||
SHAPELY_URL:=https://files.pythonhosted.org/packages/f5/a8/02347c450b5507b9a7a2c15ad6a029613149468fc55ac2100ab215666159/Shapely-1.8.4-cp310-cp310-macosx_10_9_x86_64.whl
|
||||
endif
|
||||
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/osx-64/assimp-5.0.1-h1224e73_6.tar.bz2
|
||||
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.7/download/osx-64/octomap-1.9.7-h940c156_0.tar.bz2
|
||||
ZLIB_URL:=https://anaconda.org/conda-forge/zlib/1.2.11/download/osx-64/zlib-1.2.11-h7795811_1010.tar.bz2
|
||||
endif
|
||||
|
||||
ifeq ($(PLATFORM), macosm1)
|
||||
ifeq ($(PYVERSION), py37)
|
||||
# Warning: py37 not supported on Apple M1
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/osx-64/hpp-fcl-1.7.5-py37h2d7f23a_0.tar.bz2
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/osx-64/eigenpy-2.6.5-py37h0695097_0.tar.bz2
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/osx-64/boost-1.74.0-py37hd79e0ac_3.tar.bz2
|
||||
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-64/lxml-4.9.1-py37h994c40b_0.tar.bz2
|
||||
SHAPELY_URL:=https://files.pythonhosted.org/packages/47/a9/7c644e71638bff2bbaa87f05ee62fdf6b4552a55e74bbf731d6210b6d436/Shapely-1.8.4-cp37-cp37m-macosx_10_9_x86_64.whl
|
||||
endif
|
||||
ifeq ($(PYVERSION), py39)
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/osx-arm64/hpp-fcl-1.7.5-py39ha69f3c1_2.tar.bz2
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.7/download/osx-arm64/eigenpy-2.6.7-py39ha69f3c1_0.tar.bz2
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-arm64/boost-1.78.0-py39h2550fe3_0.tar.bz2
|
||||
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-arm64/lxml-4.9.1-py39h9eb174b_0.tar.bz2
|
||||
SHAPELY_URL:=https://files.pythonhosted.org/packages/8e/60/8186995a96c5a27c9b79c015bbdd89c6d9a6df9988f86f3f1027ecd4a56a/Shapely-1.8.4-cp39-cp39-macosx_11_0_arm64.whl
|
||||
endif
|
||||
ifeq ($(PYVERSION), py310)
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.8.0/download/osx-arm64/hpp-fcl-1.8.0-py310h5699539_2.tar.bz2
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.9/download/osx-arm64/eigenpy-2.6.9-py310habfc766_1.tar.bz2
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.78.0/download/osx-arm64/boost-1.78.0-py310h4566fd1_0.tar.bz2
|
||||
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-arm64/lxml-4.9.1-py310h02f21da_0.tar.bz2
|
||||
SHAPELY_URL:=https://files.pythonhosted.org/packages/ba/d0/75cd36c81a1aa26dd6d37cf7ae1c79b0bc2d379e0f32c359bcb00b14e5c3/Shapely-1.8.4-cp310-cp310-macosx_11_0_arm64.whl
|
||||
endif
|
||||
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/osx-arm64/assimp-5.0.1-h0f81e16_7.tar.bz2
|
||||
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.7/download/osx-arm64/octomap-1.9.7-hc021e02_0.tar.bz2
|
||||
ZLIB_URL:=https://anaconda.org/conda-forge/zlib/1.2.11/download/osx-arm64/zlib-1.2.11-h90dfc92_1014.tar.bz2
|
||||
endif
|
||||
|
||||
ifeq ($(PLATFORM), win)
|
||||
ifeq ($(PYVERSION), py37)
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/win-64/hpp-fcl-1.7.5-py37h839d6b1_0.tar.bz2
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/win-64/eigenpy-2.6.5-py37h2c32e34_0.tar.bz2
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/win-64/boost-1.74.0-py37h3b38789_3.tar.bz2
|
||||
LXML_URL:=https://files.pythonhosted.org/packages/9e/5e/171ee9d40a600f565fe691ec5bf7596247ec62cfb2edc00c91afe8ea837b/lxml-4.6.3-cp37-cp37m-win_amd64.whl
|
||||
SHAPELY_URL:=https://files.pythonhosted.org/packages/ca/4d/85b912d7947261e6b3218b01946bcd7c2efb2bbafaf517c6fe1dc3bb8399/Shapely-1.8.4-cp37-cp37m-win_amd64.whl
|
||||
endif
|
||||
ifeq ($(PYVERSION), py39)
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/win-64/hpp-fcl-1.7.5-py39h2e7c763_0.tar.bz2
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/win-64/eigenpy-2.6.5-py39h3ce40e6_0.tar.bz2
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/win-64/boost-1.74.0-py39hefe7e4c_3.tar.bz2
|
||||
LXML_URL:=https://files.pythonhosted.org/packages/72/d4/426ecb8849c47c3e370c87aa0ac05d85768df917ffea27fcd6686a5e6495/lxml-4.6.3-cp39-cp39-win_amd64.whl
|
||||
SHAPELY_URL:=https://files.pythonhosted.org/packages/72/c3/0d52812db25ca71203f6203891277e6ee4f0244b21a36c062123ece15f36/Shapely-1.8.4-cp39-cp39-win_amd64.whl
|
||||
endif
|
||||
ifeq ($(PYVERSION), py310)
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.8.0/download/win-64/hpp-fcl-1.8.0-py310hc5a3c62_1.tar.bz2
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.11/download/win-64/eigenpy-2.6.11-py310hbd43d28_0.tar.bz2
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/win-64/boost-1.74.0-py310hc781a3c_5.tar.bz2
|
||||
LXML_URL:=https://files.pythonhosted.org/packages/f6/71/65c80a4caa1617a4c6e8fe1500cebb179db96232e2f623bfe6a1f4294e39/lxml-4.8.0-cp310-cp310-win_amd64.whl
|
||||
SHAPELY_URL:=https://files.pythonhosted.org/packages/24/b5/46d13799804b5fe9ede2eca8b19023410e29b78baca0bff727671a1df8c1/Shapely-1.8.4-cp310-cp310-win_amd64.whl
|
||||
endif
|
||||
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/win-64/assimp-5.0.1-hc2aa0de_6.tar.bz2
|
||||
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.7/download/win-64/octomap-1.9.7-h5362a0b_0.tar.bz2
|
||||
@@ -115,7 +170,7 @@ endif
|
||||
cp -r blenderbim/* dist/blenderbim/
|
||||
|
||||
# Provides IfcOpenShell Python functionality
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-2985bba-$(PLATFORM)64.zip
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-4fb910f-$(PLATFORM)64.zip
|
||||
cd dist/working && unzip ifcopenshell-python*
|
||||
cp -r dist/working/ifcopenshell dist/blenderbim/libs/site/packages/
|
||||
|
||||
@@ -138,11 +193,13 @@ endif
|
||||
# Provides IFCClash functionality
|
||||
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcclash/ifcclash dist/blenderbim/libs/site/packages/
|
||||
# Provides BIMTester functionality
|
||||
cd dist/working && python -m venv env
|
||||
cd dist/working && $(PYTHON) -m venv env
|
||||
cd dist/working && . env/bin/activate && pip install pybabel
|
||||
cd dist/working && . env/bin/activate && pip install babel
|
||||
cd dist/working && . env/bin/activate && ./env/bin/pybabel compile -d ./IfcOpenShell-0.7.0/src/ifcbimtester/bimtester/locale/
|
||||
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcbimtester/bimtester dist/blenderbim/libs/site/packages/
|
||||
# Provides IFCTester functionality
|
||||
cp -r dist/working/IfcOpenShell-0.7.0/src/ifctester/ifctester dist/blenderbim/libs/site/packages/
|
||||
# Provides IFCCOBie functionality
|
||||
cp -r dist/working/IfcOpenShell-0.7.0/src/ifccobie/* dist/blenderbim/libs/site/packages/
|
||||
# Provides IFCDiff functionality
|
||||
@@ -161,9 +218,9 @@ endif
|
||||
|
||||
# Provides Mustache templating in construction documentation
|
||||
mkdir dist/working
|
||||
cd dist/working && wget https://files.pythonhosted.org/packages/d6/fd/eb8c212053addd941cc90baac307c00ac246ac3fce7166b86434c6eae963/pystache-0.5.4.tar.gz
|
||||
cd dist/working && wget https://files.pythonhosted.org/packages/3f/e7/8750ba6c6101d6aa5ceeb20c013adf2c6f3554a12c71d75654b468404bfa/pystache-0.6.0.tar.gz
|
||||
cd dist/working && tar -xzvf pystache*
|
||||
cd dist/working/pystache-0.5.4/ && python setup.py build && cp -r build/lib/pystache ../../blenderbim/libs/site/packages/
|
||||
cd dist/working/pystache-0.6.0/ && $(PYTHON) setup.py build && cp -r build/lib/pystache ../../blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Provides SVG export in construction documentation
|
||||
@@ -210,9 +267,9 @@ endif
|
||||
|
||||
# Required by IFCDiff
|
||||
mkdir dist/working
|
||||
cd dist/working && wget https://github.com/Moult/deepdiff/archive/master.zip
|
||||
cd dist/working && unzip master.zip
|
||||
cp -r dist/working/deepdiff-master/deepdiff dist/blenderbim/libs/site/packages/
|
||||
cd dist/working && wget https://files.pythonhosted.org/packages/0f/ca/caead2949fbb824c7142e3774fa841aa853bb4d4331b440da8c8514dfc6f/deepdiff-5.8.1.tar.gz
|
||||
cd dist/working && tar -xzvf deepdiff*
|
||||
cp -r dist/working/deepdiff-5.8.1/deepdiff dist/blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Required by deepdiff
|
||||
@@ -238,16 +295,16 @@ endif
|
||||
|
||||
# Required by bcf
|
||||
mkdir dist/working
|
||||
cd dist/working && wget https://files.pythonhosted.org/packages/bb/41/ad9ce53bb978b68af8ae415293cafc89b165b8ad55a593725299dca76729/xmlschema-1.1.1.tar.gz
|
||||
cd dist/working && wget https://files.pythonhosted.org/packages/5b/ef/f97c3e1a7efa00e989a793fe15297214fc95ad7d9e3810586bd08ce9f0f3/xmlschema-2.0.2.tar.gz
|
||||
cd dist/working && tar -xzvf xmlschema*
|
||||
cp -r dist/working/xmlschema-1.1.1/xmlschema dist/blenderbim/libs/site/packages/
|
||||
cp -r dist/working/xmlschema-2.0.2/xmlschema dist/blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Required by bcf
|
||||
mkdir dist/working
|
||||
cd dist/working && wget https://files.pythonhosted.org/packages/12/f9/f9960222d5274944b01391749e55e4dcdf28d8f0c108b64ac931ceff6fdb/elementpath-1.4.3.tar.gz
|
||||
cd dist/working && wget https://files.pythonhosted.org/packages/11/bc/5afb61dd5d863e5cf77cd952445c50c17e65953405986f19e97e4389692a/elementpath-3.0.2.tar.gz
|
||||
cd dist/working && tar -xzvf elementpath*
|
||||
cp -r dist/working/elementpath-1.4.3/elementpath dist/blenderbim/libs/site/packages/
|
||||
cp -r dist/working/elementpath-3.0.2/elementpath dist/blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Required by bcf
|
||||
@@ -271,6 +328,19 @@ endif
|
||||
cp -r dist/working/PyP6Xer-1.13.0/xerparser dist/blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Required by QTOCalculator
|
||||
mkdir dist/working
|
||||
cd dist/working && wget $(SHAPELY_URL)
|
||||
cd dist/working && cp *.whl shapely.zip && unzip shapely.zip
|
||||
cp -r dist/working/shapely dist/blenderbim/libs/site/packages/
|
||||
ifeq ($(PLATFORM), win)
|
||||
cp -r dist/working/Shapely.libs dist/blenderbim/libs/site/packages/
|
||||
endif
|
||||
ifeq ($(PLATFORM), linux)
|
||||
cp -r dist/working/Shapely.libs dist/blenderbim/libs/site/packages/
|
||||
endif
|
||||
rm -rf dist/working
|
||||
|
||||
# Required by xerparser and IFC4D
|
||||
# TODO: remove this dependency. It's only used to show a progress bar.
|
||||
mkdir dist/working
|
||||
@@ -281,7 +351,7 @@ endif
|
||||
|
||||
# Required by IFCCityJSON
|
||||
mkdir dist/working
|
||||
cd dist/working && python -m venv env
|
||||
cd dist/working && $(PYTHON) -m venv env
|
||||
cd dist/working && mkdir site-packages
|
||||
cd dist/working && . env/bin/activate && pip install cjio --target=./site-packages
|
||||
cd dist/working/site-packages/ && rm -r *dist-info*
|
||||
@@ -389,16 +459,30 @@ endif
|
||||
# See bug #1294
|
||||
cd dist/working/ && wget https://raw.githubusercontent.com/IfcOpenShell/IfcOpenShell/v0.7.0/src/ifcbimtester/patch/model_core.patch
|
||||
cd dist/working/ && wget https://raw.githubusercontent.com/IfcOpenShell/IfcOpenShell/v0.7.0/src/ifcbimtester/patch/runner_util.patch
|
||||
cd dist/working/ && patch ../blenderbim/libs/site/packages/behave/model_core.py < model_core.patch
|
||||
cd dist/working/ && patch ../blenderbim/libs/site/packages/behave/runner_util.py < runner_util.patch
|
||||
cd dist/working/ && $(PATCH) ../blenderbim/libs/site/packages/behave/model_core.py < model_core.patch
|
||||
cd dist/working/ && $(PATCH) ../blenderbim/libs/site/packages/behave/runner_util.py < runner_util.patch
|
||||
# See bug #2159
|
||||
cd dist/working/ && wget https://raw.githubusercontent.com/IfcOpenShell/IfcOpenShell/v0.7.0/src/ifcbimtester/patch/junit.patch
|
||||
cd dist/working/ && $(PATCH) ../blenderbim/libs/site/packages/behave/reporter/junit.py < junit.patch
|
||||
rm -rf dist/working
|
||||
|
||||
# Required by ids - uh, not any more, but I'm trying it out in the drawing module
|
||||
ifeq ($(PLATFORM), macosm1)
|
||||
# No wheels available for MacOS M1, but turns out this Anaconda build works
|
||||
mkdir dist/working
|
||||
cd dist/working && wget $(LXML_URL)
|
||||
cd dist/working && tar -xf lxml*
|
||||
cp -r dist/working/lib/$(PYLIBDIR)/site-packages/lxml dist/blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
else
|
||||
# Wheels are preferred for other OSes. A first attempt at using Anaconda
|
||||
# builds for everything shows it breaks on windows. See #2422.
|
||||
mkdir dist/working
|
||||
cd dist/working && wget $(LXML_URL)
|
||||
cd dist/working && cp *.whl lxml.zip && unzip lxml.zip
|
||||
cp -r dist/working/lxml dist/blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
endif
|
||||
|
||||
# Required by behave
|
||||
mkdir dist/working
|
||||
@@ -470,14 +554,14 @@ endif
|
||||
cp -r dist/working/IFC2JSON_python-master/file_converters/ifcjson dist/blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Provides OpenLCA fucntionality
|
||||
# Provides OpenLCA functionality
|
||||
mkdir dist/working
|
||||
cd dist/working && wget https://files.pythonhosted.org/packages/85/33/f91b96e9e8608ff65a003b692e8a9cdd60f2178f60617e5b1d21334c009c/olca-ipc-0.0.10.tar.gz
|
||||
cd dist/working && tar -xzvf olca-ipc*
|
||||
cd dist/working/olca-ipc-0.0.10/ && cp -r olca ../../blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Provides Brickschema fucntionality
|
||||
# Provides Brickschema functionality
|
||||
mkdir dist/working
|
||||
cd dist/working && wget https://files.pythonhosted.org/packages/ca/37/8309da9a72407d2f5eb5489c197ac3cfe96ee3ccba1902d2a0ecc7843f92/brickschema-0.5.1.tar.gz
|
||||
cd dist/working && tar -xzvf brickschema*
|
||||
@@ -518,7 +602,7 @@ endif
|
||||
cd dist/working/ && cp -r pkg_resources ../blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
cd dist/blenderbim && sed -i "s/999999/$(VERSION)/" __init__.py
|
||||
cd dist/blenderbim && $(SED) "s/999999/$(VERSION)/" __init__.py
|
||||
cd dist && zip -r blenderbim-$(VERSION)-$(PYVERSION)-$(PLATFORM).zip ./*
|
||||
rm -rf dist/blenderbim
|
||||
|
||||
@@ -534,11 +618,19 @@ test-core:
|
||||
|
||||
.PHONY: test-bim
|
||||
test-bim:
|
||||
ifndef MODULE
|
||||
pytest test/bim
|
||||
else
|
||||
pytest test/bim -m "$(MODULE)" ./ --maxfail=1
|
||||
endif
|
||||
|
||||
.PHONY: test-tool
|
||||
test-tool:
|
||||
ifndef MODULE
|
||||
pytest test/tool
|
||||
else
|
||||
pytest test/tool/test_$(MODULE).py
|
||||
endif
|
||||
|
||||
.PHONY: qa
|
||||
qa:
|
||||
|
||||
@@ -62,6 +62,7 @@ modules = {
|
||||
"clash": None,
|
||||
"lca": None,
|
||||
"csv": None,
|
||||
"tester": None,
|
||||
"bimtester": None,
|
||||
"diff": None,
|
||||
"patch": None,
|
||||
@@ -92,7 +93,9 @@ classes = [
|
||||
operator.SelectIfcFile,
|
||||
operator.SelectSchemaDir,
|
||||
operator.SelectURIAttribute,
|
||||
operator.EditBlenderCollection,
|
||||
prop.StrProperty,
|
||||
operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty
|
||||
prop.ObjProperty,
|
||||
prop.Attribute,
|
||||
prop.ModuleVisibility,
|
||||
@@ -131,7 +134,8 @@ for mod in modules.values():
|
||||
|
||||
def on_register(scene):
|
||||
handler.setDefaultProperties(scene)
|
||||
bpy.app.handlers.depsgraph_update_post.remove(on_register)
|
||||
if not bpy.app.background:
|
||||
bpy.app.handlers.depsgraph_update_post.remove(on_register)
|
||||
|
||||
|
||||
def register():
|
||||
|
||||
@@ -5,7 +5,7 @@ FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',(),(),'EPset_Drawing','EPset
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11));
|
||||
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15));
|
||||
#2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
@@ -16,5 +16,9 @@ DATA;
|
||||
#9=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#10=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#11=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#12=IFCSIMPLEPROPERTYTEMPLATE('0c1$8NpYDEaBiJrj16jHIo',$,'Stylesheet','',.P_SINGLEVALUE.,'IfcURIReference',$,$,$,$,$,.READWRITE.);
|
||||
#13=IFCSIMPLEPROPERTYTEMPLATE('3mRF52q81FQB$h4oTh7M45',$,'Markers','',.P_SINGLEVALUE.,'IfcURIReference',$,$,$,$,$,.READWRITE.);
|
||||
#14=IFCSIMPLEPROPERTYTEMPLATE('1rhr_0N3LDtuORcEJP0KXM',$,'Symbols','',.P_SINGLEVALUE.,'IfcURIReference',$,$,$,$,$,.READWRITE.);
|
||||
#15=IFCSIMPLEPROPERTYTEMPLATE('2sHDBuW7P4TROy$hL2w7ct',$,'Patterns','',.P_SINGLEVALUE.,'IfcURIReference',$,$,$,$,$,.READWRITE.);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
|
||||
@@ -31,10 +31,11 @@ text { fill: black; stroke: none; }
|
||||
.PredefinedType-SECTIONLEVEL { marker-start: url(#section-level-marker); stroke-dasharray: 12.5, 3, 3, 3; }
|
||||
.PredefinedType-PLANLEVEL { marker-end: url(#plan-level-marker); }
|
||||
.PredefinedType-DIMENSION { marker-start: url(#dimension-marker-start); marker-end: url(#dimension-marker-end); }
|
||||
.PredefinedType-ANGLE { marker-start: url(#angle-marker-start); marker-end: url(#angle-marker-end); }
|
||||
.PredefinedType-RADIUS { marker-end: url(#radius-marker-end); }
|
||||
.PredefinedType-DIAMETER { marker-start: url(#diameter-marker-start); marker-end: url(#diameter-marker-end); }
|
||||
.PredefinedType-HIDDENLINE { stroke-dasharray: 3, 2; }
|
||||
.PredefinedType-STAIR { marker-start: url(#stair-marker-start); marker-end: url(#stair-marker-end); }
|
||||
.PredefinedType-STAIRARROW { marker-start: url(#stair-marker-start); marker-end: url(#stair-marker-end); }
|
||||
.PredefinedType-BOUNDARY { fill: none; stroke: red; stroke-width: 1; stroke-dasharray: 12, 4, 3, 4, 3, 4; }
|
||||
.PredefinedType-BATTING { marker-start: url(#batting); stroke: none; }
|
||||
.PredefinedType-SEALANT { fill: url(#crosshatch1); stroke-width: 0.25; }
|
||||
|
||||
@@ -21,6 +21,18 @@
|
||||
<path d="M 0 3.5 L 0 10.5 L 10 7" class="annotation" style="fill:black;" />
|
||||
</g>
|
||||
</marker>
|
||||
<marker id="angle-marker-start" markerHeight="14" markerWidth="11" orient="auto" refX="1" refY="7">
|
||||
<g>
|
||||
<path d="M 11 3.5 L 11 10.5 L 1 7" class="annotation" style="fill:black;" />
|
||||
<path d="M 1 0 L 1 14" class="annotation" style="stroke-width:1;" />
|
||||
</g>
|
||||
</marker>
|
||||
<marker id="angle-marker-end" markerHeight="14" markerWidth="11" orient="auto" refX="10" refY="7">
|
||||
<g>
|
||||
<path d="M 0 3.5 L 0 10.5 L 10 7" class="annotation" style="fill:black;" />
|
||||
<path d="M 10 0 L 10 14" class="annotation" style="stroke-width:1;" />
|
||||
</g>
|
||||
</marker>
|
||||
<marker id="diameter-marker-start" markerHeight="14" markerWidth="11" orient="auto" refX="1" refY="7">
|
||||
<g>
|
||||
<path d="M 11 3.5 L 11 10.5 L 1 7" class="annotation" style="fill:black;" />
|
||||
|
||||
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 5.4 KiB |
@@ -97,6 +97,7 @@ class IfcExporter:
|
||||
except:
|
||||
continue
|
||||
ifcopenshell.api.run("root.remove_product", self.file, **{"product": product})
|
||||
IfcStore.deleted_ids.clear()
|
||||
return results
|
||||
|
||||
def sync_all_objects(self):
|
||||
@@ -148,7 +149,7 @@ class IfcExporter:
|
||||
return checksum != str([s.id() for s in tool.Geometry.get_styles(obj) if s])
|
||||
|
||||
def sync_object_placement(self, obj):
|
||||
if not self.has_object_moved(obj):
|
||||
if not tool.Ifc.is_moved(obj):
|
||||
return
|
||||
blender_matrix = np.array(obj.matrix_world)
|
||||
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
@@ -162,17 +163,6 @@ class IfcExporter:
|
||||
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
return element
|
||||
|
||||
def has_object_moved(self, obj):
|
||||
if not obj.BIMObjectProperties.location_checksum:
|
||||
return True # Let's be conservative
|
||||
loc_check = np.frombuffer(eval(obj.BIMObjectProperties.location_checksum))
|
||||
rot_check = np.frombuffer(eval(obj.BIMObjectProperties.rotation_checksum))
|
||||
loc_real = np.array(obj.matrix_world.translation).flatten()
|
||||
rot_real = np.array(obj.matrix_world.to_3x3()).flatten()
|
||||
if np.allclose(loc_check, loc_real, atol=1e-4) and np.allclose(rot_check, rot_real, atol=1e-2):
|
||||
return False
|
||||
return True
|
||||
|
||||
def sync_grid_axis_object_placement(self, obj, element):
|
||||
grid = (element.PartOfU or element.PartOfV or element.PartOfW)[0]
|
||||
grid_obj = tool.Ifc.get_object(grid)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# from datetime import date
|
||||
import bpy
|
||||
import json
|
||||
import math
|
||||
@@ -38,11 +39,19 @@ def draw_attribute(attribute, layout, copy_operator=None):
|
||||
if not value_name:
|
||||
layout.label(text=attribute.name)
|
||||
return
|
||||
layout.prop(
|
||||
attribute,
|
||||
value_name,
|
||||
text=attribute.name,
|
||||
)
|
||||
if value_name == "enum_value":
|
||||
prop_with_search(layout, attribute, "enum_value", text=attribute.name)
|
||||
else:
|
||||
layout.prop(
|
||||
attribute,
|
||||
value_name,
|
||||
text=attribute.name,
|
||||
)
|
||||
if "ScheduleDuration" in attribute.name:
|
||||
layout.prop(bpy.context.scene.BIMDuration, "duration_days", text="D")
|
||||
layout.prop(bpy.context.scene.BIMDuration, "duration_hours", text="H")
|
||||
layout.prop(bpy.context.scene.BIMDuration, "duration_minutes", text="M")
|
||||
|
||||
if attribute.is_optional:
|
||||
layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
|
||||
if copy_operator:
|
||||
@@ -75,6 +84,7 @@ def import_attribute(attribute, props, data, callback=None):
|
||||
new.is_optional = attribute.optional()
|
||||
new.data_type = data_type if isinstance(data_type, str) else ""
|
||||
is_handled_by_callback = callback(attribute.name(), new, data) if callback else None
|
||||
|
||||
if is_handled_by_callback:
|
||||
pass # Our job is done
|
||||
elif is_handled_by_callback is False:
|
||||
@@ -105,6 +115,64 @@ def export_attributes(props, callback=None):
|
||||
return attributes
|
||||
|
||||
|
||||
def prop_with_search(layout, data, prop_name, **kwargs):
|
||||
# kwargs are layout.prop arguments (text, icon, etc.)
|
||||
row = layout.row(align=True)
|
||||
# Magick courtesy of https://blender.stackexchange.com/a/203443/86891
|
||||
row.context_pointer_set(name="data", data=data)
|
||||
row.prop(data, prop_name, **kwargs)
|
||||
op = row.operator("bim.enum_property_search", text="", icon="VIEWZOOM")
|
||||
op.prop_name = prop_name
|
||||
|
||||
|
||||
def get_enum_items(data, prop_name, context):
|
||||
# Retrieve items from a dynamic EnumProperty, which is otherwise not supported
|
||||
# Or throws an error in the console when the items callback returns an empty list
|
||||
# See https://blender.stackexchange.com/q/215781/86891
|
||||
prop = data.__annotations__[prop_name]
|
||||
items = prop.keywords.get("items")
|
||||
if items is None:
|
||||
return
|
||||
if not isinstance(items, (list, tuple)):
|
||||
# items are retrieved through a callback, not a static list :
|
||||
items = items(data, context)
|
||||
return items
|
||||
|
||||
|
||||
def get_obj_ifc_definition_id(context, obj, obj_type):
|
||||
if obj_type == "Object":
|
||||
return bpy.data.objects.get(obj).BIMObjectProperties.ifc_definition_id
|
||||
elif obj_type == "Material":
|
||||
return bpy.data.materials.get(obj).BIMObjectProperties.ifc_definition_id
|
||||
elif obj_type == "Task":
|
||||
return context.scene.BIMTaskTreeProperties.tasks[
|
||||
context.scene.BIMWorkScheduleProperties.active_task_index
|
||||
].ifc_definition_id
|
||||
elif obj_type == "Cost":
|
||||
return context.scene.BIMCostProperties.cost_items[
|
||||
context.scene.BIMCostProperties.active_cost_item_index
|
||||
].ifc_definition_id
|
||||
elif obj_type == "Resource":
|
||||
return context.scene.BIMResourceTreeProperties.resources[
|
||||
context.scene.BIMResourceProperties.active_resource_index
|
||||
].ifc_definition_id
|
||||
elif obj_type == "Profile":
|
||||
return context.scene.BIMProfileProperties.profiles[
|
||||
context.scene.BIMProfileProperties.active_profile_index
|
||||
].ifc_definition_id
|
||||
elif obj_type == "WorkSchedule":
|
||||
return context.scene.BIMWorkScheduleProperties.active_work_schedule_id
|
||||
|
||||
|
||||
# hack to close popup
|
||||
# https://blender.stackexchange.com/a/202576/130742
|
||||
def close_operator_panel(event):
|
||||
x, y = event.mouse_x, event.mouse_y
|
||||
bpy.context.window.cursor_warp(10, 10)
|
||||
move_back = lambda: bpy.context.window.cursor_warp(x, y)
|
||||
bpy.app.timers.register(move_back, first_interval=0.01)
|
||||
|
||||
|
||||
class IfcHeaderExtractor:
|
||||
def __init__(self, filepath: str):
|
||||
self.filepath = filepath
|
||||
|
||||
@@ -39,6 +39,8 @@ class IfcStore:
|
||||
edited_objs = set()
|
||||
pset_template_path = ""
|
||||
pset_template_file = None
|
||||
classification_path = ""
|
||||
classification_file = None
|
||||
library_path = ""
|
||||
library_file = None
|
||||
element_listeners = set()
|
||||
@@ -48,7 +50,7 @@ class IfcStore:
|
||||
last_transaction = ""
|
||||
history = []
|
||||
future = []
|
||||
schema_identifiers = ["IFC4", "IFC2X3"]
|
||||
schema_identifiers = ["IFC4", "IFC2X3", "IFC4X3"]
|
||||
|
||||
@staticmethod
|
||||
def purge():
|
||||
@@ -65,7 +67,10 @@ class IfcStore:
|
||||
IfcStore.pset_template_file = None
|
||||
IfcStore.library_path = ""
|
||||
IfcStore.library_file = None
|
||||
IfcStore.schema_identifiers = ["IFC4", "IFC2X3"]
|
||||
IfcStore.last_transaction = ""
|
||||
IfcStore.history = []
|
||||
IfcStore.future = []
|
||||
IfcStore.schema_identifiers = ["IFC4", "IFC2X3", "IFC4X3"]
|
||||
|
||||
@staticmethod
|
||||
def get_file():
|
||||
@@ -229,7 +234,6 @@ class IfcStore:
|
||||
data["guid"] = element.GlobalId
|
||||
IfcStore.commit_link_element(data)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def relink_all_objects():
|
||||
if not IfcStore.get_file():
|
||||
@@ -319,6 +323,21 @@ class IfcStore:
|
||||
# TODO Listeners are not re-registered. Does this cause nasty problems to debug later on?
|
||||
# TODO We're handling id_map and guid_map, but what about edited_objs? This might cause big problems.
|
||||
|
||||
@staticmethod
|
||||
def rollback_unlink_element(data):
|
||||
if "id" not in data or "obj" not in data:
|
||||
return
|
||||
obj = bpy.data.objects.get(data["obj"])
|
||||
IfcStore.id_map[data["id"]] = obj
|
||||
if data["guid"]:
|
||||
IfcStore.guid_map[data["guid"]] = obj
|
||||
|
||||
@staticmethod
|
||||
def commit_unlink_element(data):
|
||||
del IfcStore.id_map[data["id"]]
|
||||
if data["guid"]:
|
||||
del IfcStore.guid_map[data["guid"]]
|
||||
|
||||
@staticmethod
|
||||
def unlink_element(element=None, obj=None):
|
||||
if element is None:
|
||||
@@ -354,6 +373,17 @@ class IfcStore:
|
||||
elif obj:
|
||||
obj.BIMObjectProperties.ifc_definition_id = 0
|
||||
|
||||
if IfcStore.history:
|
||||
data = {}
|
||||
if element:
|
||||
data["id"] = element.id()
|
||||
data["guid"] = getattr(element, "GlobalId", None)
|
||||
if obj:
|
||||
data["obj"] = obj.name
|
||||
IfcStore.history[-1]["operations"].append(
|
||||
{"rollback": IfcStore.rollback_unlink_element, "commit": IfcStore.commit_unlink_element, "data": data}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def execute_ifc_operator(operator, context):
|
||||
is_top_level_operator = not bool(IfcStore.current_transaction)
|
||||
|
||||
@@ -121,7 +121,14 @@ class MaterialCreator:
|
||||
def parse_representation_item(self, item):
|
||||
if not item.StyledByItem:
|
||||
return
|
||||
style_ids = [e.id() for e in self.ifc_importer.file.traverse(item.StyledByItem[0]) if e.is_a("IfcSurfaceStyle")]
|
||||
style_ids = []
|
||||
styles = list(item.StyledByItem[0].Styles)
|
||||
while styles:
|
||||
style = styles.pop()
|
||||
if style.is_a("IfcSurfaceStyle"):
|
||||
style_ids.append(style.id())
|
||||
elif style.is_a("IfcPresentationStyleAssignment"):
|
||||
styles.extend(style.Styles)
|
||||
if not style_ids:
|
||||
return
|
||||
for style_id in style_ids:
|
||||
@@ -168,12 +175,12 @@ class IfcImporter:
|
||||
self.settings_native.set(self.settings_native.INCLUDE_CURVES, True)
|
||||
self.settings_2d = ifcopenshell.geom.settings()
|
||||
self.settings_2d.set(self.settings_2d.INCLUDE_CURVES, True)
|
||||
self.settings_2d.set(self.settings.STRICT_TOLERANCE, True)
|
||||
self.project = None
|
||||
self.collections = {}
|
||||
self.elements = set()
|
||||
self.type_collection = None
|
||||
self.type_products = {}
|
||||
self.openings = {}
|
||||
self.meshes = {}
|
||||
self.mesh_shapes = {}
|
||||
self.time = 0
|
||||
@@ -181,6 +188,7 @@ class IfcImporter:
|
||||
self.added_data = {}
|
||||
self.native_elements = set()
|
||||
self.native_data = {}
|
||||
self.progress = 0
|
||||
|
||||
self.material_creator = MaterialCreator(ifc_import_settings, self)
|
||||
|
||||
@@ -198,7 +206,6 @@ class IfcImporter:
|
||||
|
||||
def execute(self):
|
||||
bpy.context.window_manager.progress_begin(0, 100)
|
||||
self.progress = 0
|
||||
self.profile_code("Starting import process")
|
||||
self.load_file()
|
||||
self.profile_code("Loading file")
|
||||
@@ -216,8 +223,6 @@ class IfcImporter:
|
||||
self.profile_code("Process context filter")
|
||||
self.create_collections()
|
||||
self.profile_code("Create collections")
|
||||
self.create_openings_collection()
|
||||
self.profile_code("Create opening collection")
|
||||
self.create_materials()
|
||||
self.profile_code("Create materials")
|
||||
self.create_styles()
|
||||
@@ -236,24 +241,19 @@ class IfcImporter:
|
||||
self.profile_code("Create spatial elements")
|
||||
self.create_structural_items()
|
||||
self.profile_code("Create structural items")
|
||||
self.create_type_products()
|
||||
self.profile_code("Create type products")
|
||||
if not self.ifc_import_settings.is_coordinating:
|
||||
self.create_element_types()
|
||||
self.profile_code("Create element types")
|
||||
self.place_objects_in_collections()
|
||||
self.profile_code("Place objects in collections")
|
||||
if self.ifc_import_settings.should_merge_by_class:
|
||||
self.merge_by_class()
|
||||
self.profile_code("Merging by class")
|
||||
elif self.ifc_import_settings.should_merge_by_material:
|
||||
self.merge_by_material()
|
||||
self.profile_code("Merging by material")
|
||||
if self.ifc_import_settings.should_merge_materials_by_colour or len(self.material_creator.materials) > 300:
|
||||
self.merge_materials_by_colour()
|
||||
self.profile_code("Merging by colour")
|
||||
self.add_project_to_scene()
|
||||
self.profile_code("Add project to scene")
|
||||
if self.ifc_import_settings.should_clean_mesh and len(self.file.by_type("IfcElement")) < 1000:
|
||||
self.clean_mesh()
|
||||
self.profile_code("Mesh cleaning")
|
||||
if self.ifc_import_settings.should_merge_materials_by_colour or len(self.material_creator.materials) > 300:
|
||||
self.merge_materials_by_colour()
|
||||
self.profile_code("Merging by colour")
|
||||
self.set_default_context()
|
||||
self.profile_code("Setting default context")
|
||||
self.update_progress(100)
|
||||
@@ -269,7 +269,9 @@ class IfcImporter:
|
||||
|
||||
def is_point_far_away(self, point, is_meters=True):
|
||||
# Locations greater than 1km are not considered "small sites" according to the georeferencing guide
|
||||
limit = 1000 if is_meters else (1000 / self.unit_scale)
|
||||
# Users can configure this if they have to handle larger sites but beware of surveying precision
|
||||
limit = self.ifc_import_settings.distance_limit
|
||||
limit = limit if is_meters else (limit / self.unit_scale)
|
||||
coords = point
|
||||
if hasattr(point, "Coordinates"):
|
||||
coords = point.Coordinates
|
||||
@@ -304,14 +306,30 @@ class IfcImporter:
|
||||
self.settings_2d.set_context_ids(self.plan_contexts)
|
||||
|
||||
def process_element_filter(self):
|
||||
offset = self.ifc_import_settings.element_offset
|
||||
offset_limit = offset + self.ifc_import_settings.element_limit
|
||||
|
||||
if self.ifc_import_settings.has_filter:
|
||||
self.elements = set(self.ifc_import_settings.elements)
|
||||
self.elements = self.ifc_import_settings.elements
|
||||
if isinstance(self.elements, set):
|
||||
self.elements = list(self.elements)
|
||||
# TODO: enable filtering for annotations
|
||||
self.annotations = set(self.file.by_type("IfcAnnotation"))
|
||||
else:
|
||||
self.elements = set(self.file.by_type("IfcElement"))
|
||||
self.elements = self.file.by_type("IfcElement")
|
||||
self.annotations = set(self.file.by_type("IfcAnnotation"))
|
||||
|
||||
self.elements = [e for e in self.elements if not e.is_a("IfcFeatureElement")]
|
||||
if self.ifc_import_settings.is_coordinating:
|
||||
self.elements = [e for e in self.elements if e.Representation]
|
||||
|
||||
self.elements = set(self.elements[offset:offset_limit])
|
||||
|
||||
if self.ifc_import_settings.has_filter or offset or offset_limit < len(self.elements):
|
||||
self.element_types = set([ifcopenshell.util.element.get_type(e) for e in self.elements])
|
||||
else:
|
||||
self.element_types = set(self.file.by_type("IfcElementType"))
|
||||
|
||||
if self.ifc_import_settings.has_filter and self.ifc_import_settings.should_filter_spatial_elements:
|
||||
self.spatial_elements = self.get_spatial_elements_filtered_by_elements(self.elements)
|
||||
else:
|
||||
@@ -354,21 +372,38 @@ class IfcImporter:
|
||||
"type": "IfcSweptDiskSolid",
|
||||
}
|
||||
return True
|
||||
|
||||
if not self.ifc_import_settings.should_use_native_meshes:
|
||||
return False # Performance improvements only occur on edge cases currently
|
||||
|
||||
# FacetedBreps (without voids) are meshes. See #841.
|
||||
# Commented out as seems currently too slow.
|
||||
# if self.is_native_faceted_brep(representations):
|
||||
# self.native_data[element.GlobalId] = {
|
||||
# "representations": representations,
|
||||
# "representation": self.get_body_representation(element.Representation.Representations),
|
||||
# "type": "IfcFacetedBrep",
|
||||
# }
|
||||
# return True
|
||||
if self.is_native_faceted_brep(representations):
|
||||
self.native_data[element.GlobalId] = {
|
||||
"representations": representations,
|
||||
"representation": self.get_body_representation(element.Representation.Representations),
|
||||
"type": "IfcFacetedBrep",
|
||||
}
|
||||
return True
|
||||
|
||||
if self.is_native_face_based_surface_model(representations):
|
||||
self.native_data[element.GlobalId] = {
|
||||
"representations": representations,
|
||||
"representation": self.get_body_representation(element.Representation.Representations),
|
||||
"type": "IfcFaceBasedSurfaceModel",
|
||||
}
|
||||
return True
|
||||
|
||||
def is_native_swept_disk_solid(self, representations):
|
||||
for representation in representations:
|
||||
items = representation["raw"].Items or [] # Be forgiving of invalid IFCs because Revit :(
|
||||
if len(items) == 1 and items[0].is_a("IfcSweptDiskSolid"):
|
||||
return True
|
||||
elif (
|
||||
items[0].is_a("IfcSweptDiskSolid")
|
||||
and len({i.is_a() for i in items}) == 1
|
||||
and len({i.Radius for i in items}) == 1
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_native_faceted_brep(self, representations):
|
||||
@@ -378,6 +413,13 @@ class IfcImporter:
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_native_face_based_surface_model(self, representations):
|
||||
for representation in representations:
|
||||
for i in representation["raw"].Items:
|
||||
if i.is_a() != "IfcFaceBasedSurfaceModel":
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_products_from_shape_representation(self, element):
|
||||
products = [pr.ShapeOfProduct[0] for pr in element.OfProductRepresentation]
|
||||
for rep_map in element.RepresentationMap:
|
||||
@@ -391,7 +433,7 @@ class IfcImporter:
|
||||
props = bpy.context.scene.BIMGeoreferenceProperties
|
||||
if props.has_blender_offset:
|
||||
return
|
||||
if self.ifc_import_settings.should_offset_model:
|
||||
if self.ifc_import_settings.false_origin:
|
||||
return self.set_manual_blender_offset()
|
||||
if self.file.schema == "IFC2X3":
|
||||
project = self.file.by_type("IfcProject")[0]
|
||||
@@ -407,9 +449,9 @@ class IfcImporter:
|
||||
|
||||
def set_manual_blender_offset(self):
|
||||
props = bpy.context.scene.BIMGeoreferenceProperties
|
||||
props.blender_eastings = str(self.ifc_import_settings.model_offset_coordinates[0])
|
||||
props.blender_northings = str(self.ifc_import_settings.model_offset_coordinates[1])
|
||||
props.blender_orthogonal_height = str(self.ifc_import_settings.model_offset_coordinates[2])
|
||||
props.blender_eastings = str(self.ifc_import_settings.false_origin[0])
|
||||
props.blender_northings = str(self.ifc_import_settings.false_origin[1])
|
||||
props.blender_orthogonal_height = str(self.ifc_import_settings.false_origin[2])
|
||||
props.has_blender_offset = True
|
||||
|
||||
def guess_georeferencing(self, element):
|
||||
@@ -518,6 +560,9 @@ class IfcImporter:
|
||||
if grid.Representation:
|
||||
shape = ifcopenshell.geom.create_shape(self.settings_2d, grid)
|
||||
grid_obj = self.create_product(grid, shape)
|
||||
if bpy.context.preferences.addons["blenderbim"].preferences.lock_grids_on_import:
|
||||
grid_obj.lock_location = (True, True, True)
|
||||
grid_obj.lock_rotation = (True, True, True)
|
||||
collection = bpy.data.collections.new(self.get_name(grid))
|
||||
u_axes = bpy.data.collections.new("UAxes")
|
||||
collection.children.link(u_axes)
|
||||
@@ -535,23 +580,20 @@ class IfcImporter:
|
||||
shape = ifcopenshell.geom.create_shape(self.settings_2d, axis.AxisCurve)
|
||||
mesh = self.create_mesh(axis, shape)
|
||||
obj = bpy.data.objects.new(f"IfcGridAxis/{axis.AxisTag}", mesh)
|
||||
if bpy.context.preferences.addons["blenderbim"].preferences.lock_grids_on_import:
|
||||
obj.lock_location = (True, True, True)
|
||||
obj.lock_rotation = (True, True, True)
|
||||
self.link_element(axis, obj)
|
||||
self.set_matrix_world(obj, grid_obj.matrix_world)
|
||||
grid_collection.objects.link(obj)
|
||||
|
||||
def create_type_products(self):
|
||||
# TODO allow filtering of spatial elements too
|
||||
if self.ifc_import_settings.has_filter:
|
||||
type_products = set([ifcopenshell.util.element.get_type(e) for e in self.elements])
|
||||
else:
|
||||
type_products = self.file.by_type("IfcTypeProduct")
|
||||
|
||||
for type_product in type_products:
|
||||
if not type_product:
|
||||
def create_element_types(self):
|
||||
for element_type in self.element_types:
|
||||
if not element_type:
|
||||
continue
|
||||
self.create_type_product(type_product)
|
||||
self.create_element_type(element_type)
|
||||
|
||||
def create_type_product(self, element):
|
||||
def create_element_type(self, element):
|
||||
self.ifc_import_settings.logger.info("Creating object %s", element)
|
||||
representation_map = self.get_type_product_body_representation_map(element)
|
||||
mesh = None
|
||||
@@ -585,13 +627,20 @@ class IfcImporter:
|
||||
return representation_map
|
||||
|
||||
def create_native_elements(self):
|
||||
total = 0
|
||||
progress = 0
|
||||
checkpoint = time.time()
|
||||
total = len(self.native_elements)
|
||||
for element in self.native_elements:
|
||||
total += 1
|
||||
if total % 250 == 0:
|
||||
print("{} elements processed in {:.2f}s ...".format(total, time.time() - checkpoint))
|
||||
progress += 1
|
||||
if progress % 250 == 0:
|
||||
percent = round(progress / total * 100)
|
||||
print(
|
||||
"{} / {} ({}%) elements processed in {:.2f}s ...".format(
|
||||
progress, total, percent, time.time() - checkpoint
|
||||
)
|
||||
)
|
||||
checkpoint = time.time()
|
||||
self.incrementally_merge_objects()
|
||||
native_data = self.native_data[element.GlobalId]
|
||||
representation = native_data["representation"]
|
||||
if not representation:
|
||||
@@ -604,6 +653,8 @@ class IfcImporter:
|
||||
mesh = self.create_native_swept_disk_solid(element, mesh_name)
|
||||
elif native_data["type"] == "IfcFacetedBrep":
|
||||
mesh = self.create_native_faceted_brep(element, mesh_name)
|
||||
elif native_data["type"] == "IfcFaceBasedSurfaceModel":
|
||||
mesh = self.create_native_faceted_brep(element, mesh_name)
|
||||
mesh.BIMMeshProperties.ifc_definition_id = representation.id()
|
||||
mesh.name = mesh_name
|
||||
self.meshes[mesh_name] = mesh
|
||||
@@ -643,26 +694,32 @@ class IfcImporter:
|
||||
)
|
||||
else:
|
||||
iterator = ifcopenshell.geom.iterator(self.settings, self.file, include=products)
|
||||
cache = IfcStore.get_cache()
|
||||
if cache:
|
||||
iterator.set_cache(cache)
|
||||
if self.ifc_import_settings.should_cache:
|
||||
cache = IfcStore.get_cache()
|
||||
if cache:
|
||||
iterator.set_cache(cache)
|
||||
valid_file = iterator.initialize()
|
||||
if not valid_file:
|
||||
return results
|
||||
checkpoint = time.time()
|
||||
total = 0
|
||||
progress = 0
|
||||
total = len(products)
|
||||
start_progress = self.progress
|
||||
progress_range = 85 - start_progress
|
||||
while True:
|
||||
total += 1
|
||||
if total % 250 == 0:
|
||||
progress += 1
|
||||
if progress % 250 == 0:
|
||||
percent_created = round(progress / total * 100)
|
||||
percent_preprocessed = iterator.progress()
|
||||
percent_average = (percent_created + percent_preprocessed) / 2
|
||||
print(
|
||||
"{} ({}%) elements processed in {:.2f}s ...".format(
|
||||
total, iterator.progress(), time.time() - checkpoint
|
||||
"{} / {} ({}% created, {}% preprocessed) elements processed in {:.2f}s ...".format(
|
||||
progress, total, percent_created, percent_preprocessed, time.time() - checkpoint
|
||||
)
|
||||
)
|
||||
checkpoint = time.time()
|
||||
self.update_progress((iterator.progress() / 100 * progress_range) + start_progress)
|
||||
self.update_progress((percent_average / 100 * progress_range) + start_progress)
|
||||
self.incrementally_merge_objects()
|
||||
shape = iterator.get()
|
||||
if shape:
|
||||
product = self.file.by_id(shape.guid)
|
||||
@@ -681,6 +738,19 @@ class IfcImporter:
|
||||
print("Done creating geometry")
|
||||
return results
|
||||
|
||||
def incrementally_merge_objects(self):
|
||||
if not self.ifc_import_settings.is_coordinating:
|
||||
return
|
||||
if self.ifc_import_settings.merge_mode == "IFC_CLASS":
|
||||
self.merge_by_class()
|
||||
self.profile_code("Merging by class")
|
||||
elif self.ifc_import_settings.merge_mode == "IFC_TYPE":
|
||||
self.merge_by_type()
|
||||
self.profile_code("Merging by type")
|
||||
elif self.ifc_import_settings.merge_mode == "MATERIAL":
|
||||
self.merge_by_material()
|
||||
self.profile_code("Merging by material")
|
||||
|
||||
def create_structural_items(self):
|
||||
# Create structural collections
|
||||
self.structural_member_collection = bpy.data.collections.new("Members")
|
||||
@@ -791,9 +861,10 @@ class IfcImporter:
|
||||
)
|
||||
else:
|
||||
iterator = ifcopenshell.geom.iterator(self.settings_2d, self.file, include=products)
|
||||
cache = IfcStore.get_cache()
|
||||
if cache:
|
||||
iterator.set_cache(cache)
|
||||
if self.ifc_import_settings.should_cache:
|
||||
cache = IfcStore.get_cache()
|
||||
if cache:
|
||||
iterator.set_cache(cache)
|
||||
valid_file = iterator.initialize()
|
||||
if not valid_file:
|
||||
return results
|
||||
@@ -807,7 +878,7 @@ class IfcImporter:
|
||||
shape = iterator.get()
|
||||
if shape:
|
||||
product = self.file.by_id(shape.guid)
|
||||
self.create_product(self.file.by_id(shape.guid), shape)
|
||||
self.create_product(product, shape)
|
||||
results.add(product)
|
||||
if not iterator.next():
|
||||
break
|
||||
@@ -825,7 +896,7 @@ class IfcImporter:
|
||||
elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
|
||||
mesh = self.create_camera(element, shape)
|
||||
self.link_mesh(shape, mesh)
|
||||
elif element.is_a("IfcAnnotation") and self.is_curve_annotation(element):
|
||||
elif element.is_a("IfcAnnotation") and self.is_curve_annotation(element) and shape:
|
||||
mesh = self.create_curve(element, shape)
|
||||
self.link_mesh(shape, mesh)
|
||||
elif shape:
|
||||
@@ -855,17 +926,18 @@ class IfcImporter:
|
||||
elif hasattr(element, "ObjectPlacement"):
|
||||
self.set_matrix_world(obj, self.apply_blender_offset_to_matrix_world(obj, self.get_element_matrix(element)))
|
||||
|
||||
self.add_opening_relation(element, obj)
|
||||
|
||||
if element.is_a("IfcOpeningElement"):
|
||||
obj.display_type = "WIRE"
|
||||
return obj
|
||||
|
||||
def get_representation_item_material_name(self, item):
|
||||
if not item.StyledByItem:
|
||||
return
|
||||
style_ids = [e.id() for e in self.file.traverse(item.StyledByItem[0]) if e.is_a("IfcSurfaceStyle")]
|
||||
return style_ids[0] if style_ids else None
|
||||
styles = list(item.StyledByItem[0].Styles)
|
||||
while styles:
|
||||
style = styles.pop()
|
||||
if style.is_a("IfcSurfaceStyle"):
|
||||
return style.id()
|
||||
elif style.is_a("IfcPresentationStyleAssignment"):
|
||||
styles.extend(style.Styles)
|
||||
|
||||
def create_native_faceted_brep(self, element, mesh_name):
|
||||
# TODO: georeferencing?
|
||||
@@ -914,22 +986,32 @@ class IfcImporter:
|
||||
self.convert_representation(item.MappingSource.MappedRepresentation)
|
||||
elif item.is_a() == "IfcFacetedBrep":
|
||||
self.convert_representation_item_faceted_brep(item)
|
||||
elif item.is_a() == "IfcFaceBasedSurfaceModel":
|
||||
self.convert_representation_item_face_based_surface_model(item)
|
||||
|
||||
def convert_representation_item_face_based_surface_model(self, item):
|
||||
mesh = item.get_info_2(recursive=True)
|
||||
for face_set in mesh["FbsmFaces"]:
|
||||
self.convert_representation_item_face_set(item, face_set)
|
||||
|
||||
def convert_representation_item_faceted_brep(self, item):
|
||||
mesh = item.get_info_2(recursive=True)
|
||||
return self.convert_representation_item_face_set(item, mesh["Outer"])
|
||||
|
||||
def convert_representation_item_face_set(self, item, mesh):
|
||||
# On a few occasions, we flatten a list. This seems to be the most efficient way to do it.
|
||||
# https://stackoverflow.com/questions/20112776/how-do-i-flatten-a-list-of-lists-nested-lists
|
||||
mesh = item.get_info_2(recursive=True)
|
||||
|
||||
# For huge face sets it might be better to do a "flatmap" instead of sum()
|
||||
# bounds = sum((f["Bounds"] for f in mesh["Outer"]["CfsFaces"] if len(f["Bounds"]) == 1), ())
|
||||
bounds = tuple(chain.from_iterable(f["Bounds"] for f in mesh["Outer"]["CfsFaces"] if len(f["Bounds"]) == 1))
|
||||
bounds = tuple(chain.from_iterable(f["Bounds"] for f in mesh["CfsFaces"] if len(f["Bounds"]) == 1))
|
||||
# Here are some untested alternatives, are they faster?
|
||||
# bounds = tuple((f["Bounds"] for f in mesh["Outer"]["CfsFaces"] if len(f["Bounds"]) == 1))[0]
|
||||
# bounds = chain.from_iterable(f["Bounds"] for f in mesh["Outer"]["CfsFaces"] if len(f["Bounds"]) == 1)
|
||||
|
||||
polygons = [[(p["id"], p["Coordinates"]) for p in b["Bound"]["Polygon"]] for b in bounds]
|
||||
|
||||
for face in mesh["Outer"]["CfsFaces"]:
|
||||
for face in mesh["CfsFaces"]:
|
||||
# Blender cannot handle faces with holes.
|
||||
if len(face["Bounds"]) > 1:
|
||||
inner_bounds = []
|
||||
@@ -1027,6 +1109,7 @@ class IfcImporter:
|
||||
polyline.points[-1].co = representation["matrix"] @ mathutils.Vector(v2)
|
||||
|
||||
curve.bevel_depth = self.unit_scale * item.Radius
|
||||
curve.use_fill_caps = True
|
||||
return curve
|
||||
|
||||
def create_native_annotation(self, element, mesh_name):
|
||||
@@ -1059,33 +1142,74 @@ class IfcImporter:
|
||||
|
||||
def merge_by_class(self):
|
||||
merge_set = {}
|
||||
for obj in self.added_data.values():
|
||||
if not isinstance(obj, bpy.types.Object):
|
||||
id_set = {}
|
||||
for ifc_definition_id, obj in self.added_data.items():
|
||||
if not isinstance(obj, bpy.types.Object) or not obj.data:
|
||||
continue
|
||||
if "/" not in obj.name or "IfcRelAggregates" in obj.users_collection[0].name:
|
||||
element = self.file.by_id(ifc_definition_id)
|
||||
if not element.is_a("IfcElement"):
|
||||
continue
|
||||
merge_set.setdefault(obj.name.split("/")[0], []).append(obj)
|
||||
self.merge_objects(merge_set)
|
||||
merge_set.setdefault(element.is_a(), []).append(obj)
|
||||
id_set.setdefault(element.is_a(), []).append(ifc_definition_id)
|
||||
self.merge_objects(merge_set, id_set)
|
||||
|
||||
def merge_by_type(self):
|
||||
merge_set = {}
|
||||
id_set = {}
|
||||
for ifc_definition_id, obj in self.added_data.items():
|
||||
if not isinstance(obj, bpy.types.Object) or not obj.data:
|
||||
continue
|
||||
element = self.file.by_id(ifc_definition_id)
|
||||
if not element.is_a("IfcElement"):
|
||||
continue
|
||||
element_type = ifcopenshell.util.element.get_type(element)
|
||||
if not element_type:
|
||||
continue
|
||||
merge_key = str(element_type.id()) + "-" + element_type.Name or "Unnamed"
|
||||
merge_set.setdefault(merge_key, []).append(obj)
|
||||
id_set.setdefault(merge_key, []).append(ifc_definition_id)
|
||||
self.merge_objects(merge_set, id_set)
|
||||
|
||||
def merge_by_material(self):
|
||||
merge_set = {}
|
||||
for obj in self.added_data.values():
|
||||
if not isinstance(obj, bpy.types.Object):
|
||||
id_set = {}
|
||||
for ifc_definition_id, obj in self.added_data.items():
|
||||
if not isinstance(obj, bpy.types.Object) or not obj.data:
|
||||
continue
|
||||
if "/" not in obj.name or "IfcRelAggregates" in obj.users_collection[0].name:
|
||||
element = self.file.by_id(ifc_definition_id)
|
||||
if not element.is_a("IfcElement"):
|
||||
continue
|
||||
if not obj.material_slots:
|
||||
merge_set.setdefault("no-material", []).append(obj)
|
||||
else:
|
||||
merge_set.setdefault(obj.material_slots[0].name, []).append(obj)
|
||||
self.merge_objects(merge_set)
|
||||
merge_key = obj.material_slots[0].name if obj.material_slots else "no-material"
|
||||
merge_set.setdefault(merge_key, []).append(obj)
|
||||
id_set.setdefault(merge_key, []).append(ifc_definition_id)
|
||||
self.merge_objects(merge_set, id_set)
|
||||
|
||||
def merge_objects(self, merge_set):
|
||||
for ifc_class, objs in merge_set.items():
|
||||
context_override = {}
|
||||
context_override["object"] = context_override["active_object"] = objs[0]
|
||||
context_override["selected_objects"] = context_override["selected_editable_objects"] = objs
|
||||
bpy.ops.object.join(context_override)
|
||||
def merge_objects(self, merge_set, id_set):
|
||||
total_objs = sum([len(o) for o in merge_set.values()])
|
||||
cumulative_total = 0
|
||||
merge_set = {k: v for k, v in sorted(merge_set.items(), key=lambda i: len(i[1]), reverse=True)}
|
||||
for group_name, objs in merge_set.items():
|
||||
total_group_objs = len(objs)
|
||||
if total_group_objs < 10:
|
||||
continue
|
||||
merge_potential = total_objs - cumulative_total
|
||||
if merge_potential < 250:
|
||||
print("Merge target achieved")
|
||||
return
|
||||
cumulative_total += total_group_objs
|
||||
print(f"Merging {total_group_objs} objects, {merge_potential} potentially remaining -", group_name)
|
||||
try:
|
||||
target = objs[0]
|
||||
target.data = target.data.copy()
|
||||
context_override = {}
|
||||
context_override["object"] = context_override["active_object"] = target
|
||||
context_override["selected_objects"] = context_override["selected_editable_objects"] = objs
|
||||
bpy.ops.object.join(context_override)
|
||||
target.data.name += "-merge"
|
||||
for ifc_definition_id in id_set[group_name][1:]:
|
||||
del self.added_data[ifc_definition_id]
|
||||
except:
|
||||
print("Merge failed")
|
||||
|
||||
def merge_materials_by_colour(self):
|
||||
cleaned_materials = {}
|
||||
@@ -1117,7 +1241,6 @@ class IfcImporter:
|
||||
# Occurs when reloading a project
|
||||
pass
|
||||
project_collection = bpy.context.view_layer.layer_collection.children[self.project["blender"].name]
|
||||
project_collection.children[self.opening_collection.name].hide_viewport = True
|
||||
project_collection.children[self.type_collection.name].hide_viewport = True
|
||||
|
||||
def clean_mesh(self):
|
||||
@@ -1140,11 +1263,6 @@ class IfcImporter:
|
||||
bpy.ops.object.editmode_toggle(context_override)
|
||||
IfcStore.edited_objs.clear()
|
||||
|
||||
def add_opening_relation(self, element, obj):
|
||||
if not element.is_a("IfcOpeningElement"):
|
||||
return
|
||||
self.openings[element.GlobalId] = obj
|
||||
|
||||
def load_file(self):
|
||||
self.ifc_import_settings.logger.info("loading file %s", self.ifc_import_settings.input_file)
|
||||
if not bpy.context.scene.BIMProperties.ifc_file:
|
||||
@@ -1275,10 +1393,6 @@ class IfcImporter:
|
||||
if parent:
|
||||
self.collections[parent.GlobalId].children.link(aggregate["collection"])
|
||||
|
||||
def create_openings_collection(self):
|
||||
self.opening_collection = bpy.data.collections.new("IfcOpeningElements")
|
||||
self.project["blender"].children.link(self.opening_collection)
|
||||
|
||||
def create_materials(self):
|
||||
for material in self.file.by_type("IfcMaterial"):
|
||||
self.create_material(material)
|
||||
@@ -1295,11 +1409,18 @@ class IfcImporter:
|
||||
|
||||
for material_definition_representation in self.file.by_type("IfcMaterialDefinitionRepresentation"):
|
||||
material = material_definition_representation.RepresentedMaterial
|
||||
blender_material = self.material_creator.materials[material.id()]
|
||||
for representation in material_definition_representation.Representations:
|
||||
for style in [e for e in self.file.traverse(representation) if e.is_a("IfcSurfaceStyle")]:
|
||||
blender_material = self.material_creator.materials[material.id()]
|
||||
self.create_style(style, blender_material)
|
||||
parsed_styles.add(style.id())
|
||||
styles = []
|
||||
for styled_item in representation.Items:
|
||||
styles.extend(styled_item.Styles)
|
||||
while styles:
|
||||
style = styles.pop()
|
||||
if style.is_a("IfcSurfaceStyle"):
|
||||
self.create_style(style, blender_material)
|
||||
parsed_styles.add(style.id())
|
||||
elif style.is_a("IfcPresentationStyleAssignment"):
|
||||
styles.extend(style.Styles)
|
||||
|
||||
for style in self.file.by_type("IfcSurfaceStyle"):
|
||||
if style.id() in parsed_styles:
|
||||
@@ -1516,8 +1637,6 @@ class IfcImporter:
|
||||
return self.collections[element.GlobalId].objects.link(obj)
|
||||
elif element.is_a("IfcTypeObject"):
|
||||
return self.type_collection.objects.link(obj)
|
||||
elif element.is_a("IfcOpeningElement"):
|
||||
return self.opening_collection.objects.link(obj)
|
||||
elif element.is_a("IfcStructuralMember"):
|
||||
return self.structural_member_collection.objects.link(obj)
|
||||
elif element.is_a("IfcStructuralConnection"):
|
||||
@@ -1828,7 +1947,7 @@ class IfcImporter:
|
||||
def set_default_context(self):
|
||||
for subcontext in self.file.by_type("IfcGeometricRepresentationSubContext"):
|
||||
if subcontext.ContextIdentifier == "Body":
|
||||
bpy.context.scene.BIMProperties.contexts = str(subcontext.id())
|
||||
bpy.context.scene.BIMRootProperties.contexts = str(subcontext.id())
|
||||
break
|
||||
|
||||
def link_element(self, element, obj):
|
||||
@@ -1857,14 +1976,18 @@ class IfcImportSettings:
|
||||
self.input_file = None
|
||||
self.diff_file = None
|
||||
self.should_use_cpu_multiprocessing = True
|
||||
self.should_merge_by_class = False
|
||||
self.should_merge_by_material = False
|
||||
self.merge_mode = None
|
||||
self.should_merge_materials_by_colour = False
|
||||
self.should_use_native_meshes = False
|
||||
self.should_clean_mesh = True
|
||||
self.should_cache = True
|
||||
self.is_coordinating = True
|
||||
self.deflection_tolerance = 0.001
|
||||
self.angular_tolerance = 0.5
|
||||
self.should_offset_model = False
|
||||
self.model_offset_coordinates = (0, 0, 0)
|
||||
self.distance_limit = 1000
|
||||
self.false_origin = None
|
||||
self.element_offset = 0
|
||||
self.element_limit = 30000
|
||||
self.has_filter = None
|
||||
self.should_filter_spatial_elements = True
|
||||
self.elements = set()
|
||||
@@ -1882,16 +2005,18 @@ class IfcImportSettings:
|
||||
settings.diff_file = scene_diff.diff_json_file
|
||||
settings.collection_mode = props.collection_mode
|
||||
settings.should_use_cpu_multiprocessing = props.should_use_cpu_multiprocessing
|
||||
settings.should_merge_by_class = props.should_merge_by_class
|
||||
settings.should_merge_by_material = props.should_merge_by_material
|
||||
settings.merge_mode = props.merge_mode
|
||||
settings.should_merge_materials_by_colour = props.should_merge_materials_by_colour
|
||||
settings.should_use_native_meshes = props.should_use_native_meshes
|
||||
settings.should_clean_mesh = props.should_clean_mesh
|
||||
settings.should_cache = props.should_cache
|
||||
settings.is_coordinating = props.is_coordinating
|
||||
settings.deflection_tolerance = props.deflection_tolerance
|
||||
settings.angular_tolerance = props.angular_tolerance
|
||||
settings.should_offset_model = props.should_offset_model
|
||||
settings.model_offset_coordinates = (
|
||||
[float(o) for o in props.model_offset_coordinates.split(",")]
|
||||
if props.model_offset_coordinates
|
||||
else (0, 0, 0)
|
||||
)
|
||||
settings.distance_limit = props.distance_limit
|
||||
settings.false_origin = [float(o) for o in props.false_origin.split(",")] if props.false_origin else None
|
||||
if settings.false_origin == [0, 0, 0]:
|
||||
settings.false_origin = None
|
||||
settings.element_offset = props.element_offset
|
||||
settings.element_limit = props.element_limit
|
||||
return settings
|
||||
|
||||
@@ -768,6 +768,8 @@ class ActivateBcfViewpoint(bpy.types.Operator):
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
props = context.scene.BCFProperties
|
||||
blender_topic = props.active_topic
|
||||
if blender_topic is None:
|
||||
return False
|
||||
topic = bcfxml.topics[blender_topic.name]
|
||||
return topic.viewpoints
|
||||
|
||||
@@ -880,7 +882,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
|
||||
# Operators with context overrides are used because they are
|
||||
# significantly faster than looping through all objects
|
||||
|
||||
exception_global_ids = [v.ifc_guid for v in viewpoint.components.visibility.exceptions]
|
||||
exception_global_ids = {v.ifc_guid for v in viewpoint.components.visibility.exceptions}
|
||||
|
||||
if viewpoint.components.visibility.default_visibility:
|
||||
old = context.area.type
|
||||
|
||||
@@ -95,7 +95,7 @@ def getBcfViewpoints(self, context, force_update=False):
|
||||
props = context.scene.BCFProperties
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
topic = props.active_topic
|
||||
viewpoints = bcfxml.get_viewpoints(topic.name)
|
||||
viewpoints = bcfxml.get_viewpoints(topic.name) if topic else {}
|
||||
bcfviewpoints_enum.extend([(v, f"Viewpoint {i+1}", "") for i, v in enumerate(viewpoints.keys())])
|
||||
return bcfviewpoints_enum
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ def refreshScenarios(self, context):
|
||||
|
||||
|
||||
class BimTesterProperties(PropertyGroup):
|
||||
feature: StringProperty(default="", name="Feature / IDS", update=refreshScenarios)
|
||||
feature: StringProperty(default="", name="Feature", update=refreshScenarios)
|
||||
steps: StringProperty(default="", name="Custom Steps")
|
||||
ifc_file: StringProperty(default="", name="IFC File")
|
||||
audit_ifc_class: EnumProperty(items=get_ifc_classes, name="Audit Class")
|
||||
|
||||
@@ -49,7 +49,7 @@ class Loader:
|
||||
if not boundary.ConnectionGeometry:
|
||||
return None
|
||||
surface = boundary.ConnectionGeometry.SurfaceOnRelatingElement
|
||||
# workaround for unvalid geometry provided by Revit. See https://github.com/IfcOpenShell/IfcOpenShell/issues/635#issuecomment-770366838
|
||||
# workaround for invalid geometry provided by Revit. See https://github.com/IfcOpenShell/IfcOpenShell/issues/635#issuecomment-770366838
|
||||
if surface.is_a("IfcCurveBoundedPlane") and not getattr(surface, "InnerBoundaries", None):
|
||||
surface.InnerBoundaries = ()
|
||||
shape = ifcopenshell.geom.create_shape(self.settings, surface)
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import blenderbim.tool as tool
|
||||
from bpy.types import Panel, UIList
|
||||
from blenderbim.bim.helper import prop_with_search
|
||||
from blenderbim.bim.module.brick.data import BrickschemaData, BrickschemaReferencesData
|
||||
|
||||
|
||||
@@ -48,7 +49,8 @@ class BIM_PT_brickschema(Panel):
|
||||
row.operator("bim.close_brick_project", text="", icon="CANCEL")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "namespace", text="")
|
||||
prop_with_search(row, self.props, "namespace", text="")
|
||||
prop_with_search(row, self.props, "brick_equipment_class", text="")
|
||||
row.prop(self.props, "brick_equipment_class", text="")
|
||||
row.operator("bim.add_brick", text="", icon="ADD")
|
||||
|
||||
@@ -101,7 +103,7 @@ class BIM_PT_ifc_brickschema_references(Panel):
|
||||
return
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "libraries")
|
||||
prop_with_search(row, self.props, "libraries")
|
||||
row.operator("bim.convert_brick_project", text="", icon="ADD")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
|
||||
@@ -17,12 +17,20 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from . import operator, workspace
|
||||
from . import operator, prop, workspace
|
||||
|
||||
classes = (
|
||||
operator.CadTrimExtend,
|
||||
operator.CadMitre,
|
||||
operator.AlignViewToProfile,
|
||||
operator.AddIfcArcIndexFillet,
|
||||
operator.AddIfcCircle,
|
||||
operator.AddRectangle,
|
||||
operator.CadArcFrom2Points,
|
||||
operator.CadArcFrom3Points,
|
||||
operator.CadFillet,
|
||||
operator.CadMitre,
|
||||
operator.CadOffset,
|
||||
operator.CadTrimExtend,
|
||||
prop.BIMCadProperties,
|
||||
workspace.CadHotkey,
|
||||
)
|
||||
|
||||
@@ -30,8 +38,10 @@ classes = (
|
||||
def register():
|
||||
if not bpy.app.background:
|
||||
bpy.utils.register_tool(workspace.CadTool, after={"builtin.transform"}, separator=True, group=True)
|
||||
bpy.types.Scene.BIMCadProperties = bpy.props.PointerProperty(type=prop.BIMCadProperties)
|
||||
|
||||
|
||||
def unregister():
|
||||
if not bpy.app.background:
|
||||
bpy.utils.unregister_tool(workspace.CadTool)
|
||||
del bpy.types.Scene.BIMCadProperties
|
||||
|
||||
@@ -20,7 +20,10 @@ import bpy
|
||||
import math
|
||||
import bmesh
|
||||
import mathutils
|
||||
import bpy_extras
|
||||
import blenderbim.tool as tool
|
||||
from mathutils import Vector, Matrix
|
||||
from math import pi, radians, sin, cos, sqrt
|
||||
|
||||
messages = {
|
||||
"SHARED_VERTEX": "Shared Vertex, no intersection possible",
|
||||
@@ -39,7 +42,6 @@ class CadTrimExtend(bpy.types.Operator):
|
||||
return bool(obj) and obj.type == "MESH"
|
||||
|
||||
def cancel_message(self, msg):
|
||||
print(msg)
|
||||
self.report({"WARNING"}, msg)
|
||||
return {"CANCELLED"}
|
||||
|
||||
@@ -68,10 +70,10 @@ class CadTrimExtend(bpy.types.Operator):
|
||||
else:
|
||||
return self.cancel_message("select two edges!")
|
||||
|
||||
bmesh.ops.remove_doubles(bm, verts=list(set(edges[0].verts) | set(edges[1].verts)), dist=1e-5)
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
bmesh.update_edit_mesh(me, loop_triangles=True)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -85,7 +87,6 @@ class CadMitre(bpy.types.Operator):
|
||||
return bool(obj) and obj.type == "MESH"
|
||||
|
||||
def cancel_message(self, msg):
|
||||
print(msg)
|
||||
self.report({"WARNING"}, msg)
|
||||
return {"CANCELLED"}
|
||||
|
||||
@@ -114,10 +115,10 @@ class CadMitre(bpy.types.Operator):
|
||||
else:
|
||||
return self.cancel_message("select two edges!")
|
||||
|
||||
bmesh.ops.remove_doubles(bm, verts=list(set(edges[0].verts) | set(edges[1].verts)), dist=1e-5)
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
bmesh.update_edit_mesh(me, loop_triangles=True)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -139,8 +140,9 @@ class CadFillet(bpy.types.Operator):
|
||||
layout.prop(self, prop)
|
||||
|
||||
def execute(self, context):
|
||||
if bpy.context.mode != "EDIT_MESH":
|
||||
return {"CANCELLED"}
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
obj = bpy.context.active_object
|
||||
cursor = obj.matrix_world.inverted() @ bpy.context.scene.cursor.location
|
||||
mesh = obj.data
|
||||
@@ -202,6 +204,610 @@ class CadFillet(bpy.types.Operator):
|
||||
chamfer_edge = [e for e in bm.edges if shared_vert in e.verts and v2 in e.verts][0]
|
||||
bm.edges.remove(chamfer_edge)
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
bmesh.update_edit_mesh(mesh)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CadArcFrom2Points(bpy.types.Operator):
|
||||
bl_idname = "bim.cad_arc_from_2_points"
|
||||
bl_label = "CAD Arc from 2 Points"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1)
|
||||
should_flip: bpy.props.BoolProperty(name="Flip", description="Flip arc", default=False)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
for prop in self.__class__.__annotations__.keys():
|
||||
layout.prop(self, prop)
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
if bpy.context.mode != "EDIT_MESH":
|
||||
return {"CANCELLED"}
|
||||
obj = bpy.context.active_object
|
||||
region = bpy.context.region
|
||||
region_3d = bpy.context.area.spaces.active.region_3d
|
||||
cursor = bpy.context.scene.cursor.location
|
||||
center = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, cursor)
|
||||
if not center:
|
||||
return {"CANCELLED"}
|
||||
mesh = obj.data
|
||||
mw = obj.matrix_world
|
||||
bm = bmesh.from_edit_mesh(mesh)
|
||||
selected_verts = [v for v in bm.verts if v.select]
|
||||
if len(selected_verts) != 2:
|
||||
return {"CANCELLED"}
|
||||
v1 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, mw @ selected_verts[0].co)
|
||||
v2 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, mw @ selected_verts[1].co)
|
||||
l1 = v1 - center
|
||||
l2 = v2 - center
|
||||
angle = l1.angle_signed(l2)
|
||||
|
||||
if self.should_flip:
|
||||
if angle > 0:
|
||||
angle = -((math.pi * 2) - angle)
|
||||
else:
|
||||
angle = (math.pi * 2) + angle
|
||||
|
||||
v = selected_verts[0]
|
||||
bm.verts.remove(selected_verts[1])
|
||||
axis = region_3d.view_rotation @ Vector((0, 0, 1))
|
||||
bmesh.ops.spin(
|
||||
bm,
|
||||
geom=[v],
|
||||
axis=mw.inverted().to_quaternion() @ axis,
|
||||
cent=mw.inverted() @ cursor,
|
||||
steps=self.resolution * 4,
|
||||
angle=-angle,
|
||||
)
|
||||
bmesh.update_edit_mesh(mesh)
|
||||
mesh.update()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CadArcFrom3Points(bpy.types.Operator):
|
||||
bl_idname = "bim.cad_arc_from_3_points"
|
||||
bl_label = "CAD Arc from 3 Points"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1)
|
||||
only_recalculate_center: bpy.props.BoolProperty(name="Only Recalculate Center", default=False)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
return obj and obj.type == "MESH" and context.mode == "EDIT_MESH"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
for prop in self.__class__.__annotations__.keys():
|
||||
layout.prop(self, prop)
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
if bpy.context.mode != "EDIT_MESH":
|
||||
return {"CANCELLED"}
|
||||
|
||||
obj = bpy.context.edit_object
|
||||
mesh = obj.data
|
||||
bm = bmesh.from_edit_mesh(mesh)
|
||||
|
||||
arc = [v for v in bm.verts if v.select]
|
||||
if len(arc) != 3:
|
||||
return {"CANCELLED"}
|
||||
elif len([e for e in bm.edges if e.select]) != 2:
|
||||
return {"CANCELLED"}
|
||||
|
||||
sorted_arc = [None, None, None]
|
||||
for v1 in arc:
|
||||
connections = 0
|
||||
for link_edge in v1.link_edges:
|
||||
v2 = link_edge.other_vert(v1)
|
||||
if v2 in arc:
|
||||
connections += 1
|
||||
if connections == 2: # Midpoint
|
||||
sorted_arc[1] = v1
|
||||
else:
|
||||
sorted_arc[2 if sorted_arc[2] is None else 0] = v1
|
||||
|
||||
points = [tuple(v.co) for v in sorted_arc]
|
||||
verts, _ = tool.Cad.create_arc_segments(points, num_verts=(self.resolution * 4) + 1, make_edges=False)
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
index_offset = len(bm.verts)
|
||||
new_verts = [bm.verts.new(v) for v in verts]
|
||||
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
|
||||
|
||||
bm.verts.remove(sorted_arc[1])
|
||||
bmesh.ops.remove_doubles(bm, verts=new_verts + [sorted_arc[0], sorted_arc[2]], dist=1e-5)
|
||||
|
||||
bmesh.update_edit_mesh(mesh)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CadOffset(bpy.types.Operator):
|
||||
bl_idname = "bim.cad_offset"
|
||||
bl_label = "CAD Offset"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
distance: bpy.props.FloatProperty(name="Distance", default=0.1)
|
||||
|
||||
@classmethod
|
||||
def poll(self, context):
|
||||
return context.mode == "EDIT_MESH"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
for prop in self.__class__.__annotations__.keys():
|
||||
layout.prop(self, prop)
|
||||
|
||||
def cancel_message(self, msg):
|
||||
self.report({"WARNING"}, msg)
|
||||
return {"CANCELLED"}
|
||||
|
||||
def execute(self, context):
|
||||
# This is not the same as Mesh Tools or Sverchok. Mesh Tools is based on
|
||||
# a 3D "rail" which allows fancy 3D edge offsets, but will get different
|
||||
# results to traditional CAD, which expects offsets to occur on a
|
||||
# defined 2D workplane. This CAD offset does a pure 2D operation, so it
|
||||
# always looks correct from the desired "2D workplane". This also
|
||||
# reduces the settings down to one parameter instead of the huge config
|
||||
# dialog that Mesh Tools has. Sverchok is 2D based, but has a weird side
|
||||
# effect of converting an unclosed loop into a closed loop which is also
|
||||
# not what users expect.
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
obj = context.active_object
|
||||
mw = obj.matrix_world
|
||||
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
bm.verts.ensure_lookup_table()
|
||||
bm.edges.ensure_lookup_table()
|
||||
|
||||
edges = [e for e in bm.edges if e.select and not e.hide]
|
||||
|
||||
if len(edges) < 1:
|
||||
return self.cancel_message("NO_EDGES")
|
||||
|
||||
verts = set()
|
||||
[verts.update(e.verts) for e in edges]
|
||||
|
||||
# Use the viewport angle to determine the offset direction
|
||||
for area in bpy.context.screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
# Don't ask me, I don't know.
|
||||
# view_matrix = area.spaces.active.region_3d.view_matrix
|
||||
x = area.spaces.active.region_3d.view_rotation @ Vector((1, 0, 0))
|
||||
y = area.spaces.active.region_3d.view_rotation @ Vector((0, 1, 0))
|
||||
z = area.spaces.active.region_3d.view_rotation @ Vector((0, 0, 1))
|
||||
wp = Matrix([x, y, z, Vector((0, 0, 0))]).to_4x4().transposed()
|
||||
break
|
||||
|
||||
rotation = Matrix.Rotation(pi / 2, 2, "Z")
|
||||
rotation_i = Matrix.Rotation(-pi / 2, 2, "Z")
|
||||
|
||||
# Create loops from edges
|
||||
loop_edges = set(edges)
|
||||
loops = []
|
||||
while loop_edges:
|
||||
edge = loop_edges.pop()
|
||||
loop = [edge]
|
||||
has_found_connected_edge = True
|
||||
while has_found_connected_edge:
|
||||
has_found_connected_edge = False
|
||||
for edge in loop_edges.copy():
|
||||
edge_verts = set(edge.verts)
|
||||
if edge_verts & set(loop[0].verts):
|
||||
loop.insert(0, edge)
|
||||
loop_edges.remove(edge)
|
||||
has_found_connected_edge = True
|
||||
elif edge_verts & set(loop[-1].verts):
|
||||
loop.append(edge)
|
||||
loop_edges.remove(edge)
|
||||
has_found_connected_edge = True
|
||||
loops.append(loop)
|
||||
|
||||
for loop in loops:
|
||||
all_verts = {v.index for e in loop for v in e.verts}
|
||||
possible_v1s = []
|
||||
is_closed = True
|
||||
for edge in loop:
|
||||
# If we have an open loop, start at either end instead
|
||||
v = edge.verts[0]
|
||||
if len(v.link_edges) == 1:
|
||||
possible_v1s.append(v)
|
||||
is_closed = False
|
||||
else:
|
||||
for link_edge in v.link_edges:
|
||||
if link_edge.other_vert(v).index not in all_verts:
|
||||
possible_v1s.append(v)
|
||||
is_closed = False
|
||||
break
|
||||
v = edge.verts[1]
|
||||
if len(v.link_edges) == 1:
|
||||
possible_v1s.append(v)
|
||||
is_closed = False
|
||||
else:
|
||||
for link_edge in v.link_edges:
|
||||
if link_edge.other_vert(v).index not in all_verts:
|
||||
possible_v1s.append(v)
|
||||
is_closed = False
|
||||
break
|
||||
|
||||
# Always start at the same vertex, so that when the operator is
|
||||
# refreshed with new parameters the axes don't randomly get flipped.
|
||||
if is_closed:
|
||||
bm.verts.ensure_lookup_table()
|
||||
bm.edges.ensure_lookup_table()
|
||||
start = bm.verts[min(all_verts)]
|
||||
else:
|
||||
start = possible_v1s[0] if possible_v1s[0].index < possible_v1s[1].index else possible_v1s[1]
|
||||
|
||||
v1 = start
|
||||
|
||||
new_verts = []
|
||||
processed_verts = set()
|
||||
# [v0] --> v1 --> v2
|
||||
while v1:
|
||||
if v1.index in processed_verts:
|
||||
break
|
||||
|
||||
link_verts = []
|
||||
if len(v1.link_edges) == 1:
|
||||
v = v1.link_edges[0].other_vert(v1)
|
||||
link_verts.append(v) if v.index in all_verts else None
|
||||
else:
|
||||
v = v1.link_edges[0].other_vert(v1)
|
||||
link_verts.append(v) if v.index in all_verts else None
|
||||
v = v1.link_edges[1].other_vert(v1)
|
||||
link_verts.append(v) if v.index in all_verts else None
|
||||
|
||||
if len(link_verts) == 1:
|
||||
v2 = link_verts[0]
|
||||
v0 = None if v2.index not in processed_verts else v2
|
||||
v2 = None if v2.index in processed_verts else v2
|
||||
else:
|
||||
v2 = link_verts[0]
|
||||
v0 = link_verts[1]
|
||||
if v2.index in processed_verts:
|
||||
if is_closed and v0.index in processed_verts:
|
||||
v3 = start
|
||||
v0 = v0 if v2 == v3 else v2
|
||||
v2 = v3
|
||||
else:
|
||||
v0, v2 = v2, v0
|
||||
|
||||
normals = []
|
||||
v1co = (wp.inverted() @ mw @ v1.co).to_2d()
|
||||
if v2:
|
||||
v2co = (wp.inverted() @ mw @ v2.co).to_2d()
|
||||
direction = (v2co - v1co).normalized()
|
||||
local_direction = (rotation @ direction).normalized()
|
||||
normals.append(local_direction)
|
||||
|
||||
if v0:
|
||||
v0co = (wp.inverted() @ mw @ v0.co).to_2d()
|
||||
direction = (v0co - v1co).normalized()
|
||||
local_direction = (rotation_i @ direction).normalized()
|
||||
normals.append(local_direction)
|
||||
|
||||
if len(normals) == 2:
|
||||
# https://stackoverflow.com/a/54042831/9627415
|
||||
new_normal = (normals[0].lerp(normals[1], 0.5)).normalized()
|
||||
offset_length = self.distance / sqrt((1 + normals[0].dot(normals[1])) / 2)
|
||||
offset = mw.inverted().to_quaternion() @ (wp.to_quaternion() @ (new_normal * offset_length).to_3d())
|
||||
new_vert = v1.co + offset
|
||||
new_verts.append(bm.verts.new(new_vert))
|
||||
else:
|
||||
normal = (normals[0] * self.distance).to_3d()
|
||||
offset = mw.inverted().to_quaternion() @ (wp.to_quaternion() @ normal)
|
||||
new_vert = v1.co + offset
|
||||
new_verts.append(bm.verts.new(new_vert))
|
||||
|
||||
processed_verts.add(v1.index)
|
||||
|
||||
if v2 in processed_verts:
|
||||
break
|
||||
|
||||
v1 = v2
|
||||
|
||||
[bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
|
||||
if is_closed:
|
||||
bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0]))
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
bmesh.update_edit_mesh(obj.data)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddIfcCircle(bpy.types.Operator):
|
||||
bl_idname = "bim.add_ifccircle"
|
||||
bl_label = "Add IfcCircle"
|
||||
radius: bpy.props.FloatProperty(name="Radius", default=0.1)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
return bool(obj) and obj.type == "MESH"
|
||||
|
||||
def execute(self, context):
|
||||
if self.has_selected_existing_circle(context):
|
||||
self.change_radius(context)
|
||||
else:
|
||||
self.create_circle(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
def has_selected_existing_circle(self, context):
|
||||
obj = context.active_object
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
verts = [v for v in bm.verts if v.select and not v.hide]
|
||||
if len(verts) != 2:
|
||||
return False
|
||||
groups = []
|
||||
for i, group in enumerate(obj.vertex_groups):
|
||||
if "IFCCIRCLE" in group.name:
|
||||
groups.append(i)
|
||||
bm.verts.layers.deform.verify()
|
||||
deform_layer = bm.verts.layers.deform.active
|
||||
for group in groups:
|
||||
if group in verts[0][deform_layer] and group in verts[1][deform_layer]:
|
||||
return True
|
||||
|
||||
def change_radius(self, context):
|
||||
obj = context.active_object
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
verts = [v for v in bm.verts if v.select and not v.hide]
|
||||
center = verts[0].co.lerp(verts[1].co, 0.5)
|
||||
verts[0].co = center + ((verts[0].co - center).normalized() * self.radius)
|
||||
verts[1].co = center + ((verts[1].co - center).normalized() * self.radius)
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
bmesh.update_edit_mesh(obj.data)
|
||||
|
||||
def create_circle(self, context):
|
||||
obj = context.active_object
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
# The last group may be the result of a prior run of this operator.
|
||||
# I tried looping through all groups but Blender group indices seem to behave unpredictably.
|
||||
if len(obj.vertex_groups):
|
||||
last_group = obj.vertex_groups[-1]
|
||||
verts_in_group = [v for v in obj.data.vertices if last_group.index in [vg.group for vg in v.groups]]
|
||||
if "IFCCIRCLE" in last_group.name and len(verts_in_group) != 2:
|
||||
obj.vertex_groups.remove(last_group)
|
||||
group = obj.vertex_groups.new(name="IFCCIRCLE")
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
|
||||
bm.verts.layers.deform.verify()
|
||||
deform_layer = bm.verts.layers.deform.active
|
||||
|
||||
center = obj.matrix_world.inverted() @ context.scene.cursor.location
|
||||
|
||||
v1 = bm.verts.new((center[0], center[1] + self.radius, 0.0))
|
||||
v2 = bm.verts.new((center[0], center[1] - self.radius, 0.0))
|
||||
bm.verts.index_update()
|
||||
bm.edges.new((v1, v2))
|
||||
|
||||
for vert in [v1, v2]:
|
||||
vert[deform_layer][group.index] = 1
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
bmesh.update_edit_mesh(obj.data)
|
||||
|
||||
|
||||
class AddIfcArcIndexFillet(bpy.types.Operator):
|
||||
bl_idname = "bim.add_ifcarcindex_fillet"
|
||||
bl_label = "Add Arc Index Fillet"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
radius: bpy.props.FloatProperty(name="Radius", default=0.1)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
return obj and obj.type == "MESH" and context.mode == "EDIT_MESH"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
for prop in self.__class__.__annotations__.keys():
|
||||
layout.prop(self, prop)
|
||||
|
||||
def execute(self, context):
|
||||
if self.has_selected_existing_arc(context):
|
||||
self.change_radius(context)
|
||||
else:
|
||||
self.create_arc(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
def has_selected_existing_arc(self, context):
|
||||
obj = context.active_object
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
verts = [v for v in bm.verts if v.select and not v.hide]
|
||||
if len(verts) != 3:
|
||||
return False
|
||||
groups = []
|
||||
for i, group in enumerate(obj.vertex_groups):
|
||||
if "IFCARCINDEX" in group.name:
|
||||
groups.append(i)
|
||||
bm.verts.layers.deform.verify()
|
||||
deform_layer = bm.verts.layers.deform.active
|
||||
for group in groups:
|
||||
try:
|
||||
if group in verts[0][deform_layer] and group in verts[1][deform_layer]:
|
||||
return True
|
||||
except:
|
||||
pass # Potentially fail if the vert has been removed in the previous operation
|
||||
|
||||
def change_radius(self, context):
|
||||
obj = context.active_object
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
edges = [e for e in bm.edges if e.select and not e.hide]
|
||||
|
||||
mid = list(set(edges[0].verts) & set(edges[1].verts))[0]
|
||||
v1 = edges[0].other_vert(mid)
|
||||
v2 = edges[1].other_vert(mid)
|
||||
center = tool.Cad.get_center_of_arc([v1.co, mid.co, v2.co])
|
||||
|
||||
if len(v1.link_edges) != 2 or len(v2.link_edges) != 2:
|
||||
return
|
||||
|
||||
v3 = v1.link_edges[1].other_vert(v1) if mid in v1.link_edges[0].verts else v1.link_edges[0].other_vert(v1)
|
||||
v4 = v2.link_edges[1].other_vert(v2) if mid in v2.link_edges[0].verts else v2.link_edges[0].other_vert(v2)
|
||||
dir1 = (v3.co - v1.co).normalized()
|
||||
dir2 = (v4.co - v2.co).normalized()
|
||||
|
||||
intersect = mathutils.geometry.intersect_line_line(v3.co, v1.co, v4.co, v2.co)[0]
|
||||
|
||||
edge_angle = dir1.angle(dir2)
|
||||
slide_distance = self.radius / math.tan(edge_angle / 2)
|
||||
|
||||
v1.co = intersect + (dir1 * slide_distance)
|
||||
v2.co = intersect + (dir2 * slide_distance)
|
||||
|
||||
normal = mathutils.geometry.normal([v1.co, intersect, v2.co])
|
||||
center = mathutils.geometry.intersect_line_line(
|
||||
v1.co, v1.co + normal.cross(dir1), v2.co, v2.co + normal.cross(dir2)
|
||||
)[0]
|
||||
|
||||
mid.co = center + ((v1.co.lerp(v2.co, 0.5) - center).normalized() * self.radius)
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
bmesh.update_edit_mesh(obj.data)
|
||||
|
||||
def create_arc(self, context):
|
||||
obj = bpy.context.active_object
|
||||
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
# The last group may be the result of a prior run of this operator.
|
||||
# I tried looping through all groups but Blender group indices seem to behave unpredictably.
|
||||
if len(obj.vertex_groups):
|
||||
last_group = obj.vertex_groups[-1]
|
||||
verts_in_group = [v for v in obj.data.vertices if last_group.index in [vg.group for vg in v.groups]]
|
||||
if "IFCARCINDEX" in last_group.name and len(verts_in_group) != 3:
|
||||
obj.vertex_groups.remove(last_group)
|
||||
group = obj.vertex_groups.new(name="IFCARCINDEX")
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
|
||||
bm.verts.layers.deform.verify()
|
||||
deform_layer = bm.verts.layers.deform.active
|
||||
|
||||
selected_edges = [e for e in bm.edges if e.select]
|
||||
if len(selected_edges) != 2:
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Assume the user has selected two edges sharing a vert, but merge verts
|
||||
# just in case the vertex is coincident but not shared.
|
||||
all_verts = list(set(selected_edges[0].verts) | set(selected_edges[1].verts))
|
||||
|
||||
bmesh.ops.remove_doubles(bm, verts=all_verts, dist=1e-4)
|
||||
|
||||
# Calculate the distance to slide each edge to make space for the fillet arc
|
||||
shared_vert = list(set(selected_edges[0].verts) & set(selected_edges[1].verts))[0]
|
||||
v1 = selected_edges[0].other_vert(shared_vert)
|
||||
v2 = selected_edges[1].other_vert(shared_vert)
|
||||
dir1 = (v1.co - shared_vert.co).normalized()
|
||||
dir2 = (v2.co - shared_vert.co).normalized()
|
||||
edge_angle = dir1.angle(dir2)
|
||||
slide_distance = self.radius / math.tan(edge_angle / 2)
|
||||
|
||||
angle = math.pi - edge_angle
|
||||
fillet_v1co = shared_vert.co + (dir1 * slide_distance)
|
||||
fillet_v2co = shared_vert.co + (dir2 * slide_distance)
|
||||
|
||||
normal = mathutils.geometry.normal([v1.co, shared_vert.co, v2.co])
|
||||
|
||||
center = mathutils.geometry.intersect_line_line(
|
||||
fillet_v1co, fillet_v1co + normal.cross(dir1), fillet_v2co, fillet_v2co + normal.cross(dir2)
|
||||
)[0]
|
||||
|
||||
fillet_v1 = bm.verts.new(fillet_v1co)
|
||||
fillet_v2 = bm.verts.new(fillet_v2co)
|
||||
|
||||
midpointco = center + ((fillet_v1co.lerp(fillet_v2.co, 0.5) - center).normalized() * self.radius)
|
||||
midpoint = bm.verts.new(midpointco)
|
||||
|
||||
bm.edges.new((v1, fillet_v1))
|
||||
bm.edges.new((v2, fillet_v2))
|
||||
bm.edges.new((fillet_v1, midpoint))
|
||||
bm.edges.new((fillet_v2, midpoint))
|
||||
|
||||
bmesh.ops.delete(bm, geom=[shared_vert], context="VERTS")
|
||||
|
||||
for vert in [fillet_v1, midpoint, fillet_v2]:
|
||||
vert[deform_layer][group.index] = 1
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
bmesh.update_edit_mesh(obj.data)
|
||||
|
||||
|
||||
class AlignViewToProfile(bpy.types.Operator):
|
||||
bl_idname = "bim.align_view_to_profile"
|
||||
bl_label = "Align View To Profile"
|
||||
# __ ___
|
||||
# .'. -- . '.
|
||||
# /U) __ (O|
|
||||
# /.' ()() '.\._
|
||||
# .',/;,_.--._.;;) . '--..__
|
||||
# / ,///|.__.|.\\\ \ '. '.''---..___
|
||||
# /'._ '' || || '' _'\ : \ ' . '.
|
||||
# / || || '., ) ) : \
|
||||
# :'-.__ _ || || _ __.' _\_ .' ' ' ,)
|
||||
# ( ' |' ( __= ___..-._ ( (.\\
|
||||
# ('\ .___ ___. /'.___= \.\.\
|
||||
# \\\-..____________..-'///
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
return bool(obj) and obj.type == "MESH"
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.ops.view3d.view_axis(type="TOP", align_active=True)
|
||||
region_3d = bpy.context.area.spaces.active.region_3d
|
||||
region_3d.view_perspective = "ORTHO"
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddRectangle(bpy.types.Operator):
|
||||
bl_idname = "bim.add_rectangle"
|
||||
bl_label = "Add Rectangle"
|
||||
x: bpy.props.FloatProperty(name="X", default=0.1)
|
||||
y: bpy.props.FloatProperty(name="Y", default=0.1)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
return bool(obj) and obj.type == "MESH"
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
cursor = obj.matrix_world.inverted() @ context.scene.cursor.location
|
||||
new_verts = [
|
||||
bm.verts.new((cursor[0], cursor[1], 0.0)),
|
||||
bm.verts.new((cursor[0] + self.x, cursor[1], 0.0)),
|
||||
bm.verts.new((cursor[0] + self.x, cursor[1] + self.y, 0.0)),
|
||||
bm.verts.new((cursor[0], cursor[1] + self.y, 0.0)),
|
||||
]
|
||||
[bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
|
||||
bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0]))
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
bmesh.update_edit_mesh(obj.data)
|
||||
return {"FINISHED"}
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,30 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from blenderbim.bim.module.model.data import AuthoringData
|
||||
from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound
|
||||
from bpy.types import PropertyGroup
|
||||
|
||||
|
||||
class BIMCadProperties(PropertyGroup):
|
||||
resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1)
|
||||
radius: bpy.props.FloatProperty(name="Radius", default=0.1)
|
||||
distance: bpy.props.FloatProperty(name="Distance", default=0.1)
|
||||
x: bpy.props.FloatProperty(name="X", default=0.2)
|
||||
y: bpy.props.FloatProperty(name="Y", default=0.1)
|
||||
@@ -20,7 +20,6 @@ import os
|
||||
import bpy
|
||||
import blenderbim.bim.module.type.prop as type_prop
|
||||
from bpy.types import WorkSpaceTool
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.model.data import AuthoringData
|
||||
|
||||
|
||||
@@ -35,29 +34,95 @@ class CadTool(WorkSpaceTool):
|
||||
bl_widget = None
|
||||
# https://docs.blender.org/api/current/bpy.types.KeyMapItems.html
|
||||
bl_keymap = (
|
||||
("bim.cad_hotkey", {"type": "C", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_C")]}),
|
||||
("bim.cad_hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_E")]}),
|
||||
("bim.cad_hotkey", {"type": "F", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_F")]}),
|
||||
("bim.cad_hotkey", {"type": "O", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_O")]}),
|
||||
("bim.cad_hotkey", {"type": "Q", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Q")]}),
|
||||
("bim.cad_hotkey", {"type": "R", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_R")]}),
|
||||
("bim.cad_hotkey", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_T")]}),
|
||||
("bim.cad_fillet", {"type": "F", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_F")]}),
|
||||
# Enable Mesh Tools add-on to get this amazing tool
|
||||
("mesh.offset_edges", {"type": "O", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_O")]}),
|
||||
("bim.cad_hotkey", {"type": "V", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_V")]}),
|
||||
("bim.cad_hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}),
|
||||
)
|
||||
|
||||
def draw_settings(context, layout, tool):
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="Extend", icon="EVENT_E")
|
||||
obj = context.active_object
|
||||
if obj and obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.is_profile:
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_Q")
|
||||
row.operator("bim.edit_extrusion_profile", text="Save Profile")
|
||||
row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT")
|
||||
row.operator("bim.disable_editing_extrusion_profile", text="", icon="CANCEL")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="Mitre", icon="EVENT_T")
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_E")
|
||||
row.operator("bim.cad_hotkey", text="Extend").hotkey = "S_E"
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="Fillet", icon="EVENT_F")
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_T")
|
||||
row.operator("bim.cad_hotkey", text="Mitre").hotkey = "S_T"
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="Offset", icon="EVENT_O")
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_F")
|
||||
row.operator("bim.cad_hotkey", text="Fillet").hotkey = "S_F"
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_O")
|
||||
row.operator("bim.cad_hotkey", text="Offset").hotkey = "S_O"
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_R")
|
||||
row.operator("bim.add_rectangle", text="Rectangle")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_C")
|
||||
row.operator("bim.add_ifccircle", text="Circle")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_V")
|
||||
row.operator("bim.set_arc_index", text="3-Point Arc")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_X")
|
||||
row.operator("bim.reset_vertex", text="Reset Vertex")
|
||||
else:
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_E")
|
||||
row.operator("bim.cad_hotkey", text="Extend").hotkey = "S_E"
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_T")
|
||||
row.operator("bim.cad_hotkey", text="Mitre").hotkey = "S_T"
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_F")
|
||||
row.operator("bim.cad_hotkey", text="Fillet").hotkey = "S_F"
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_O")
|
||||
row.operator("bim.cad_hotkey", text="Offset").hotkey = "S_O"
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="2-Point Arc", icon="EVENT_C")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_V")
|
||||
row.operator("bim.cad_hotkey", text="3-Point Arc").hotkey = "S_V"
|
||||
|
||||
|
||||
class CadHotkey(bpy.types.Operator):
|
||||
@@ -67,17 +132,73 @@ class CadHotkey(bpy.types.Operator):
|
||||
hotkey: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
self.props = context.scene.BIMModelProperties
|
||||
self.has_ifc_class = True
|
||||
try:
|
||||
self.has_ifc_class = bool(self.props.ifc_class)
|
||||
except:
|
||||
pass
|
||||
self.props = context.scene.BIMCadProperties
|
||||
getattr(self, f"hotkey_{self.hotkey}")()
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context):
|
||||
props = context.scene.BIMCadProperties
|
||||
if self.hotkey == "S_C":
|
||||
if self.is_profile():
|
||||
row = self.layout.row()
|
||||
row.prop(props, "radius")
|
||||
elif self.hotkey == "S_F":
|
||||
if not self.is_profile():
|
||||
row = self.layout.row()
|
||||
row.prop(props, "resolution")
|
||||
row = self.layout.row()
|
||||
row.prop(props, "radius")
|
||||
elif self.hotkey == "S_O":
|
||||
row = self.layout.row()
|
||||
row.prop(props, "distance")
|
||||
elif self.hotkey == "S_R":
|
||||
row = self.layout.row()
|
||||
row.prop(props, "x")
|
||||
row = self.layout.row()
|
||||
row.prop(props, "y")
|
||||
elif self.hotkey == "S_V":
|
||||
if not self.is_profile():
|
||||
row = self.layout.row()
|
||||
row.prop(props, "resolution")
|
||||
|
||||
def hotkey_S_C(self):
|
||||
if self.is_profile():
|
||||
bpy.ops.bim.add_ifccircle(radius=self.props.radius)
|
||||
else:
|
||||
bpy.ops.bim.cad_arc_from_2_points()
|
||||
|
||||
def hotkey_S_E(self):
|
||||
bpy.ops.bim.cad_trim_extend()
|
||||
|
||||
def hotkey_S_F(self):
|
||||
if self.is_profile():
|
||||
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius)
|
||||
else:
|
||||
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius)
|
||||
|
||||
def hotkey_S_O(self):
|
||||
bpy.ops.bim.cad_offset(distance=self.props.distance)
|
||||
|
||||
def hotkey_S_Q(self):
|
||||
bpy.ops.bim.edit_extrusion_profile()
|
||||
|
||||
def hotkey_S_R(self):
|
||||
if self.is_profile():
|
||||
bpy.ops.bim.add_rectangle(x=self.props.x, y=self.props.y)
|
||||
|
||||
def hotkey_S_T(self):
|
||||
bpy.ops.bim.cad_mitre()
|
||||
|
||||
def hotkey_S_V(self):
|
||||
if self.is_profile():
|
||||
bpy.ops.bim.set_arc_index()
|
||||
else:
|
||||
bpy.ops.bim.cad_arc_from_3_points(resolution=self.resolution)
|
||||
|
||||
def hotkey_S_X(self):
|
||||
if self.is_profile():
|
||||
bpy.ops.bim.reset_vertex()
|
||||
|
||||
def is_profile(self):
|
||||
obj = bpy.context.active_object
|
||||
return obj and obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.is_profile
|
||||
|
||||
@@ -20,23 +20,26 @@ import bpy
|
||||
from . import ui, prop, operator
|
||||
|
||||
classes = (
|
||||
operator.LoadClassificationLibrary,
|
||||
operator.AddClassification,
|
||||
operator.RemoveClassification,
|
||||
operator.EnableEditingClassification,
|
||||
operator.DisableEditingClassification,
|
||||
operator.EditClassification,
|
||||
operator.RemoveClassificationReference,
|
||||
operator.EnableEditingClassificationReference,
|
||||
operator.DisableEditingClassificationReference,
|
||||
operator.EditClassificationReference,
|
||||
operator.AddClassificationReference,
|
||||
operator.ChangeClassificationLevel,
|
||||
operator.DisableEditingClassification,
|
||||
operator.DisableEditingClassificationReference,
|
||||
operator.DisableEditingClassificationReferences,
|
||||
operator.EditClassification,
|
||||
operator.EditClassificationReference,
|
||||
operator.EnableEditingClassification,
|
||||
operator.EnableEditingClassificationReference,
|
||||
operator.LoadClassificationLibrary,
|
||||
operator.RemoveClassification,
|
||||
operator.RemoveClassificationReference,
|
||||
prop.ClassificationReference,
|
||||
prop.BIMClassificationProperties,
|
||||
prop.BIMClassificationReferenceProperties,
|
||||
ui.BIM_PT_classifications,
|
||||
ui.BIM_PT_classification_references,
|
||||
ui.BIM_PT_material_classifications,
|
||||
ui.BIM_PT_cost_classifications,
|
||||
ui.BIM_UL_classifications,
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.date
|
||||
import ifcopenshell.util.classification
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
|
||||
|
||||
def refresh():
|
||||
ClassificationsData.is_loaded = False
|
||||
ClassificationReferencesData.is_loaded = False
|
||||
MaterialClassificationsData.is_loaded = False
|
||||
CostClassificationsData.is_loaded = False
|
||||
|
||||
|
||||
class ClassificationsData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.data["has_classification_file"] = cls.has_classification_file()
|
||||
cls.data["classifications"] = cls.classifications()
|
||||
cls.data["available_classifications"] = cls.available_classifications()
|
||||
|
||||
@classmethod
|
||||
def has_classification_file(cls):
|
||||
return bool(IfcStore.classification_file)
|
||||
|
||||
@classmethod
|
||||
def classifications(cls):
|
||||
results = []
|
||||
for element in tool.Ifc.get().by_type("IfcClassification"):
|
||||
data = element.get_info()
|
||||
if tool.Ifc.get().schema == "IFC2X3" and element.EditionDate:
|
||||
data["EditionDate"] = ifcopenshell.util.date.ifc2datetime(data["EditionDate"])
|
||||
results.append(data)
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def available_classifications(cls):
|
||||
if not IfcStore.classification_file:
|
||||
return []
|
||||
return [(str(e.id()), e.Name, "") for e in IfcStore.classification_file.by_type("IfcClassification")]
|
||||
|
||||
|
||||
class ReferencesData:
|
||||
@classmethod
|
||||
def is_available_classification_added(cls):
|
||||
if not IfcStore.classification_file or not IfcStore.classification_file.by_type("IfcClassification"):
|
||||
return False
|
||||
props = bpy.context.scene.BIMClassificationProperties
|
||||
name = IfcStore.classification_file.by_id(int(props.available_classifications)).Name
|
||||
return name in [e.Name for e in tool.Ifc.get().by_type("IfcClassification")]
|
||||
|
||||
|
||||
class ClassificationReferencesData(ReferencesData):
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.data["references"] = cls.references()
|
||||
cls.data["is_available_classification_added"] = cls.is_available_classification_added()
|
||||
|
||||
@classmethod
|
||||
def references(cls):
|
||||
results = []
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
if element:
|
||||
for reference in ifcopenshell.util.classification.get_references(element):
|
||||
data = reference.get_info()
|
||||
del data["ReferencedSource"]
|
||||
results.append(data)
|
||||
return results
|
||||
|
||||
|
||||
class MaterialClassificationsData(ReferencesData):
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.data["references"] = cls.references()
|
||||
cls.data["is_available_classification_added"] = cls.is_available_classification_added()
|
||||
|
||||
@classmethod
|
||||
def references(cls):
|
||||
results = []
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object.active_material)
|
||||
if element:
|
||||
for reference in ifcopenshell.util.classification.get_references(element):
|
||||
data = reference.get_info()
|
||||
del data["ReferencedSource"]
|
||||
results.append(data)
|
||||
return results
|
||||
|
||||
|
||||
class CostClassificationsData(ReferencesData):
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.data["references"] = cls.references()
|
||||
cls.data["is_available_classification_added"] = cls.is_available_classification_added()
|
||||
|
||||
@classmethod
|
||||
def references(cls):
|
||||
results = []
|
||||
element = tool.Ifc.get().by_id(
|
||||
bpy.context.scene.BIMCostProperties.cost_items[
|
||||
bpy.context.scene.BIMCostProperties.active_cost_item_index
|
||||
].ifc_definition_id
|
||||
)
|
||||
if element:
|
||||
for reference in ifcopenshell.util.classification.get_references(element):
|
||||
data = reference.get_info()
|
||||
del data["ReferencedSource"]
|
||||
results.append(data)
|
||||
return results
|
||||
@@ -1,5 +1,5 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
# Copyright (C) 2020-2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
@@ -18,46 +18,40 @@
|
||||
|
||||
import bpy
|
||||
import json
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.bim.helper
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from ifcopenshell.api.classification.data import Data
|
||||
from blenderbim.bim.module.classification.prop import getClassifications, getReferences
|
||||
|
||||
|
||||
class LoadClassificationLibrary(bpy.types.Operator):
|
||||
class LoadClassificationLibrary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.load_classification_library"
|
||||
bl_label = "Load Classification Library"
|
||||
filename_ext = ".ifc"
|
||||
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def execute(self, context):
|
||||
Data.load_library(self.filepath)
|
||||
getClassifications(self, context)
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def _execute(self, context):
|
||||
IfcStore.classification_file = ifcopenshell.open(self.filepath)
|
||||
|
||||
class AddClassification(bpy.types.Operator):
|
||||
|
||||
class AddClassification(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_classification"
|
||||
bl_label = "Add Classification"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMClassificationProperties
|
||||
ifcopenshell.api.run(
|
||||
"classification.add_classification",
|
||||
IfcStore.get_file(),
|
||||
**{"classification": Data.library_file.by_id(int(props.available_classifications))},
|
||||
tool.Ifc.get(),
|
||||
classification=IfcStore.classification_file.by_id(int(props.available_classifications)),
|
||||
)
|
||||
Data.load(IfcStore.get_file())
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EnableEditingClassification(bpy.types.Operator):
|
||||
@@ -67,19 +61,21 @@ class EnableEditingClassification(bpy.types.Operator):
|
||||
classification: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
def callback(name, prop, data):
|
||||
if name == "ReferenceTokens":
|
||||
new = bpy.context.scene.BIMGeoreferenceProperties.projected_crs.add()
|
||||
new.name = name
|
||||
new.data_type = "string"
|
||||
new.is_null = data[name] is None
|
||||
new.is_optional = True
|
||||
new.string_value = "" if new.is_null else json.dumps(data[name])
|
||||
return True
|
||||
|
||||
props = context.scene.BIMClassificationProperties
|
||||
props.classification_attributes.clear()
|
||||
classification_data = Data.classifications[self.classification]
|
||||
for attribute in IfcStore.get_schema().declaration_by_name("IfcClassification").all_attributes():
|
||||
new = props.classification_attributes.add()
|
||||
new.name = attribute.name()
|
||||
new.is_null = classification_data[attribute.name()] is None
|
||||
new.is_optional = attribute.optional()
|
||||
new.data_type = "string"
|
||||
if attribute.name() == "ReferenceTokens":
|
||||
new.string_value = "" if new.is_null else json.dumps(classification_data[attribute.name()])
|
||||
else:
|
||||
new.string_value = "" if new.is_null else classification_data[attribute.name()]
|
||||
blenderbim.bim.helper.import_attributes2(
|
||||
tool.Ifc.get().by_id(self.classification), props.classification_attributes, callback
|
||||
)
|
||||
props.active_classification_id = self.classification
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -94,34 +90,26 @@ class DisableEditingClassification(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemoveClassification(bpy.types.Operator):
|
||||
class RemoveClassification(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_classification"
|
||||
bl_label = "Remove Classification"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
classification: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
ifcopenshell.api.run(
|
||||
"classification.remove_classification",
|
||||
self.file,
|
||||
**{"classification": self.file.by_id(self.classification)},
|
||||
tool.Ifc.get(),
|
||||
classification=self.file.by_id(self.classification),
|
||||
)
|
||||
Data.load(IfcStore.get_file())
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EditClassification(bpy.types.Operator):
|
||||
class EditClassification(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.edit_classification"
|
||||
bl_label = "Edit Classification"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMClassificationProperties
|
||||
attributes = {}
|
||||
@@ -138,9 +126,7 @@ class EditClassification(bpy.types.Operator):
|
||||
self.file,
|
||||
**{"classification": self.file.by_id(props.active_classification_id), "attributes": attributes},
|
||||
)
|
||||
Data.load(IfcStore.get_file())
|
||||
bpy.ops.bim.disable_editing_classification()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EnableEditingClassificationReference(bpy.types.Operator):
|
||||
@@ -154,16 +140,7 @@ class EnableEditingClassificationReference(bpy.types.Operator):
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
props = obj.BIMClassificationReferenceProperties
|
||||
props.reference_attributes.clear()
|
||||
reference_data = Data.references[self.reference]
|
||||
for attribute in IfcStore.get_schema().declaration_by_name("IfcClassificationReference").all_attributes():
|
||||
if attribute.name() == "ReferencedSource":
|
||||
continue
|
||||
new = props.reference_attributes.add()
|
||||
new.name = attribute.name()
|
||||
new.is_null = reference_data[attribute.name()] is None
|
||||
new.is_optional = attribute.optional()
|
||||
new.data_type = "string"
|
||||
new.string_value = "" if new.is_null else reference_data[attribute.name()]
|
||||
blenderbim.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.reference), props.reference_attributes)
|
||||
props.active_reference_id = self.reference
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -180,41 +157,47 @@ class DisableEditingClassificationReference(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemoveClassificationReference(bpy.types.Operator):
|
||||
class RemoveClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_classification_reference"
|
||||
bl_label = "Remove Classification Reference"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
reference: bpy.props.IntProperty()
|
||||
obj: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
obj_type: bpy.props.StringProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
self.file = IfcStore.get_file()
|
||||
ifcopenshell.api.run(
|
||||
"classification.remove_reference",
|
||||
self.file,
|
||||
**{
|
||||
"reference": self.file.by_id(self.reference),
|
||||
"product": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id),
|
||||
},
|
||||
)
|
||||
Data.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id)
|
||||
Data.load(IfcStore.get_file())
|
||||
return {"FINISHED"}
|
||||
if self.obj_type == "Object":
|
||||
if context.selected_objects:
|
||||
objects = [o.name for o in context.selected_objects]
|
||||
else:
|
||||
objects = [context.active_object.name]
|
||||
else:
|
||||
objects = [self.obj]
|
||||
|
||||
identification = tool.Ifc.get().by_id(self.reference)[0]
|
||||
|
||||
for obj in objects:
|
||||
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
|
||||
element = tool.Ifc.get().by_id(ifc_definition_id)
|
||||
references = ifcopenshell.util.classification.get_references(element, should_inherit=False)
|
||||
for reference in references:
|
||||
if reference[0] == identification:
|
||||
ifcopenshell.api.run(
|
||||
"classification.remove_reference",
|
||||
tool.Ifc.get(),
|
||||
reference=reference,
|
||||
product=element,
|
||||
)
|
||||
break
|
||||
|
||||
|
||||
class EditClassificationReference(bpy.types.Operator):
|
||||
class EditClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.edit_classification_reference"
|
||||
bl_label = "Edit Classification Reference"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
props = obj.BIMClassificationReferenceProperties
|
||||
@@ -228,48 +211,47 @@ class EditClassificationReference(bpy.types.Operator):
|
||||
ifcopenshell.api.run(
|
||||
"classification.edit_reference",
|
||||
self.file,
|
||||
**{"reference": self.file.by_id(props.active_reference_id), "attributes": attributes},
|
||||
reference=self.file.by_id(props.active_reference_id),
|
||||
attributes=attributes,
|
||||
)
|
||||
Data.load(IfcStore.get_file())
|
||||
bpy.ops.bim.disable_editing_classification_reference()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddClassificationReference(bpy.types.Operator):
|
||||
class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_classification_reference"
|
||||
bl_label = "Add Classification Reference"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
reference: bpy.props.IntProperty()
|
||||
obj: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
obj_type: bpy.props.StringProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
self.file = IfcStore.get_file()
|
||||
|
||||
classification = None
|
||||
|
||||
if self.obj_type == "Object":
|
||||
if context.selected_objects:
|
||||
objects = [o.name for o in context.selected_objects]
|
||||
else:
|
||||
objects = [context.active_object.name]
|
||||
else:
|
||||
objects = [self.obj]
|
||||
props = context.scene.BIMClassificationProperties
|
||||
classification_name = Data.library_classifications[int(props.available_classifications)]
|
||||
for classification_id, classification in Data.classifications.items():
|
||||
if classification["Name"] == classification_name:
|
||||
classification = self.file.by_id(classification_id)
|
||||
classification = None
|
||||
classification_name = IfcStore.classification_file.by_id(int(props.available_classifications)).Name
|
||||
for element in tool.Ifc.get().by_type("IfcClassification"):
|
||||
if element.Name == classification_name:
|
||||
classification = element
|
||||
break
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"classification.add_reference",
|
||||
self.file,
|
||||
**{
|
||||
"reference": Data.library_file.by_id(self.reference),
|
||||
"product": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id),
|
||||
"classification": classification,
|
||||
},
|
||||
)
|
||||
Data.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id)
|
||||
Data.load(IfcStore.get_file())
|
||||
return {"FINISHED"}
|
||||
for obj in objects:
|
||||
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
|
||||
if not ifc_definition_id:
|
||||
continue
|
||||
ifcopenshell.api.run(
|
||||
"classification.add_reference",
|
||||
tool.Ifc.get(),
|
||||
reference=IfcStore.classification_file.by_id(self.reference),
|
||||
product=tool.Ifc.get().by_id(ifc_definition_id),
|
||||
classification=classification,
|
||||
)
|
||||
|
||||
|
||||
class ChangeClassificationLevel(bpy.types.Operator):
|
||||
@@ -279,5 +261,28 @@ class ChangeClassificationLevel(bpy.types.Operator):
|
||||
parent_id: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
getReferences(self, context, parent_id=self.parent_id)
|
||||
props = context.scene.BIMClassificationProperties
|
||||
props.available_library_references.clear()
|
||||
for reference in IfcStore.classification_file.by_id(self.parent_id).HasReferences:
|
||||
new = props.available_library_references.add()
|
||||
new.identification = reference.Identification or ""
|
||||
new.name = reference.Name or ""
|
||||
new.ifc_definition_id = reference.id()
|
||||
new.has_references = bool(reference.HasReferences)
|
||||
new.referenced_source
|
||||
if reference.ReferencedSource.is_a("IfcClassificationReference"):
|
||||
props.active_library_referenced_source = reference.ReferencedSource.ReferencedSource.id()
|
||||
else:
|
||||
props.active_library_referenced_source = 0
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisableEditingClassificationReferences(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_editing_classification_references"
|
||||
bl_label = "Disable Editing Classification References"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMClassificationProperties
|
||||
props.available_library_references.clear()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.prop import StrProperty, Attribute
|
||||
from ifcopenshell.api.classification.data import Data
|
||||
from blenderbim.bim.module.classification.data import ClassificationsData
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
@@ -31,42 +32,11 @@ from bpy.props import (
|
||||
CollectionProperty,
|
||||
)
|
||||
|
||||
classification_enum = []
|
||||
|
||||
|
||||
def purge():
|
||||
global classification_enum
|
||||
classification_enum = []
|
||||
|
||||
|
||||
def getClassifications(self, context):
|
||||
global classification_enum
|
||||
if len(classification_enum) < 1:
|
||||
classification_enum.clear()
|
||||
classification_enum.extend([(str(i), n, "") for i, n in Data.library_classifications.items()])
|
||||
if classification_enum:
|
||||
getReferences(self, context, parent_id=int(classification_enum[0][0]))
|
||||
return classification_enum
|
||||
|
||||
|
||||
def updateClassification(self, context):
|
||||
getReferences(self, context, parent_id=int(self.available_classifications))
|
||||
|
||||
|
||||
def getReferences(self, context, parent_id=None):
|
||||
props = context.scene.BIMClassificationProperties
|
||||
props.available_library_references.clear()
|
||||
for reference in Data.library_file.by_id(parent_id).HasReferences:
|
||||
new = props.available_library_references.add()
|
||||
new.identification = reference.Identification or ""
|
||||
new.name = reference.Name or ""
|
||||
new.ifc_definition_id = reference.id()
|
||||
new.has_references = bool(reference.HasReferences)
|
||||
new.referenced_source
|
||||
if reference.ReferencedSource.is_a("IfcClassificationReference"):
|
||||
props.active_library_referenced_source = reference.ReferencedSource.ReferencedSource.id()
|
||||
else:
|
||||
props.active_library_referenced_source = 0
|
||||
def get_available_classifications(self, context):
|
||||
if not ClassificationsData.is_loaded:
|
||||
ClassificationsData.load()
|
||||
return ClassificationsData.data["available_classifications"]
|
||||
|
||||
|
||||
class ClassificationReference(PropertyGroup):
|
||||
@@ -78,9 +48,7 @@ class ClassificationReference(PropertyGroup):
|
||||
|
||||
|
||||
class BIMClassificationProperties(PropertyGroup):
|
||||
available_classifications: EnumProperty(
|
||||
items=getClassifications, name="Available Classifications", update=updateClassification
|
||||
)
|
||||
available_classifications: EnumProperty(items=get_available_classifications, name="Available Classifications")
|
||||
classification_attributes: CollectionProperty(name="Classification Attributes", type=Attribute)
|
||||
active_classification_id: IntProperty(name="Active Classification Id")
|
||||
available_library_references: CollectionProperty(name="Available Library References", type=ClassificationReference)
|
||||
|
||||
@@ -16,10 +16,17 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import blenderbim.bim.helper
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.bim.module.classification.prop as classification_prop
|
||||
from bpy.types import Panel, UIList
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from ifcopenshell.api.classification.data import Data
|
||||
from blenderbim.bim.module.classification.data import (
|
||||
ClassificationsData,
|
||||
ClassificationReferencesData,
|
||||
MaterialClassificationsData,
|
||||
CostClassificationsData,
|
||||
)
|
||||
|
||||
|
||||
class BIM_PT_classifications(Panel):
|
||||
@@ -33,15 +40,15 @@ class BIM_PT_classifications(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return IfcStore.get_file()
|
||||
return tool.Ifc.get()
|
||||
|
||||
def draw(self, context):
|
||||
if not Data.is_loaded:
|
||||
Data.load(IfcStore.get_file())
|
||||
if not ClassificationsData.is_loaded:
|
||||
ClassificationsData.load()
|
||||
|
||||
self.props = context.scene.BIMClassificationProperties
|
||||
|
||||
if Data.library_file:
|
||||
if ClassificationsData.data["has_classification_file"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "available_classifications", text="")
|
||||
row.operator("bim.load_classification_library", text="", icon="IMPORT")
|
||||
@@ -51,36 +58,100 @@ class BIM_PT_classifications(Panel):
|
||||
row.label(text="No Active Classification Library")
|
||||
row.operator("bim.load_classification_library", text="", icon="IMPORT")
|
||||
|
||||
for classification_id, classification in Data.classifications.items():
|
||||
if self.props.active_classification_id == classification_id:
|
||||
self.draw_editable_ui(classification)
|
||||
for classification in ClassificationsData.data["classifications"]:
|
||||
if self.props.active_classification_id == classification["id"]:
|
||||
self.draw_editable_ui()
|
||||
else:
|
||||
self.draw_ui(classification_id, classification)
|
||||
self.draw_ui(classification)
|
||||
|
||||
def draw_editable_ui(self, classification):
|
||||
def draw_editable_ui(self):
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props.classification_attributes.get("Name"), "string_value", text="", icon="ASSET_MANAGER")
|
||||
row.operator("bim.edit_classification", text="", icon="CHECKMARK")
|
||||
row.operator("bim.edit_classification", text="Save changes", icon="CHECKMARK")
|
||||
row.operator("bim.disable_editing_classification", text="", icon="CANCEL")
|
||||
blenderbim.bim.helper.draw_attributes(self.props.classification_attributes, self.layout)
|
||||
|
||||
for attribute in self.props.classification_attributes:
|
||||
if attribute.name == "Name":
|
||||
continue
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(attribute, "string_value", text=attribute.name)
|
||||
if attribute.is_optional:
|
||||
row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
|
||||
|
||||
def draw_ui(self, classification_id, classification):
|
||||
def draw_ui(self, classification):
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=classification["Name"], icon="ASSET_MANAGER")
|
||||
if not self.props.active_classification_id:
|
||||
op = row.operator("bim.enable_editing_classification", text="", icon="GREASEPENCIL")
|
||||
op.classification = classification_id
|
||||
row.operator("bim.remove_classification", text="", icon="X").classification = classification_id
|
||||
op.classification = classification["id"]
|
||||
row.operator("bim.remove_classification", text="", icon="X").classification = classification["id"]
|
||||
|
||||
|
||||
class BIM_PT_classification_references(Panel):
|
||||
class ReferenceUI:
|
||||
def draw_ui(self, context):
|
||||
obj = context.active_object
|
||||
self.oprops = obj.BIMObjectProperties
|
||||
self.sprops = context.scene.BIMClassificationProperties
|
||||
self.props = obj.BIMClassificationReferenceProperties
|
||||
self.file = IfcStore.get_file()
|
||||
|
||||
self.draw_add_ui(context)
|
||||
|
||||
if not self.data.data["references"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="No References")
|
||||
|
||||
for reference in self.data.data["references"]:
|
||||
if self.props.active_reference_id == reference["id"]:
|
||||
self.draw_editable_ui()
|
||||
else:
|
||||
self.draw_reference_ui(reference)
|
||||
|
||||
def draw_add_ui(self, context):
|
||||
if not self.data.data["is_available_classification_added"]:
|
||||
return
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.sprops, "available_classifications", text="")
|
||||
if not self.sprops.available_library_references:
|
||||
op = row.operator("bim.change_classification_level", text="", icon="IMPORT")
|
||||
op.parent_id = int(self.sprops.available_classifications)
|
||||
return
|
||||
if self.sprops.active_library_referenced_source:
|
||||
op = row.operator("bim.change_classification_level", text="", icon="FRAME_PREV")
|
||||
op.parent_id = self.sprops.active_library_referenced_source
|
||||
if self.sprops.active_library_reference_index < len(self.sprops.available_library_references):
|
||||
op = row.operator("bim.add_classification_reference", text="", icon="ADD")
|
||||
op.obj = self.obj
|
||||
op.obj_type = self.obj_type
|
||||
op.reference = self.sprops.available_library_references[
|
||||
self.sprops.active_library_reference_index
|
||||
].ifc_definition_id
|
||||
row.operator("bim.disable_editing_classification_references", text="", icon="CANCEL")
|
||||
self.layout.template_list(
|
||||
"BIM_UL_classifications",
|
||||
"",
|
||||
self.sprops,
|
||||
"available_library_references",
|
||||
self.sprops,
|
||||
"active_library_reference_index",
|
||||
)
|
||||
|
||||
def draw_editable_ui(self):
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.edit_classification_reference", text="Save changes", icon="CHECKMARK")
|
||||
row.operator("bim.disable_editing_classification_reference", text="", icon="CANCEL")
|
||||
blenderbim.bim.helper.draw_attributes(self.props.reference_attributes, self.layout)
|
||||
|
||||
def draw_reference_ui(self, reference):
|
||||
row = self.layout.row(align=True)
|
||||
if self.file.schema == "IFC2X3":
|
||||
name = reference["ItemReference"] or "No Identification"
|
||||
else:
|
||||
name = reference["Identification"] or "No Identification"
|
||||
row.label(text=name, icon="ASSET_MANAGER")
|
||||
row.label(text=reference["Name"] or "")
|
||||
if not self.props.active_reference_id:
|
||||
op = row.operator("bim.enable_editing_classification_reference", text="", icon="GREASEPENCIL")
|
||||
op.reference = reference["id"]
|
||||
op = row.operator("bim.remove_classification_reference", text="", icon="X")
|
||||
op.reference = reference["id"]
|
||||
op.obj = self.obj
|
||||
op.obj_type = self.obj_type
|
||||
|
||||
|
||||
class BIM_PT_classification_references(Panel, ReferenceUI):
|
||||
bl_label = "IFC Classification References"
|
||||
bl_idname = "BIM_PT_classification_references"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
@@ -93,85 +164,65 @@ class BIM_PT_classification_references(Panel):
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
return False
|
||||
if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id):
|
||||
return False
|
||||
return bool(context.active_object.BIMObjectProperties.ifc_definition_id)
|
||||
return bool(tool.Ifc.get_entity(context.active_object))
|
||||
|
||||
def draw(self, context):
|
||||
obj = context.active_object
|
||||
self.oprops = obj.BIMObjectProperties
|
||||
self.sprops = context.scene.BIMClassificationProperties
|
||||
self.props = obj.BIMClassificationReferenceProperties
|
||||
self.file = IfcStore.get_file()
|
||||
if not Data.is_loaded:
|
||||
Data.load(IfcStore.get_file())
|
||||
if self.oprops.ifc_definition_id not in Data.products:
|
||||
Data.load(IfcStore.get_file(), self.oprops.ifc_definition_id)
|
||||
if not ClassificationReferencesData.is_loaded:
|
||||
ClassificationReferencesData.load()
|
||||
self.data = ClassificationReferencesData
|
||||
self.obj = context.active_object.name
|
||||
self.obj_type = "Object"
|
||||
self.draw_ui(context)
|
||||
|
||||
self.draw_add_ui(context)
|
||||
|
||||
reference_ids = Data.products[self.oprops.ifc_definition_id]
|
||||
if not reference_ids:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="No References")
|
||||
class BIM_PT_material_classifications(Panel, ReferenceUI):
|
||||
bl_label = "IFC Material Classifications"
|
||||
bl_idname = "BIM_PT_material_classifications"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "material"
|
||||
|
||||
for reference_id in reference_ids:
|
||||
reference = Data.references[reference_id]
|
||||
if self.props.active_reference_id == reference_id:
|
||||
self.draw_editable_ui(reference)
|
||||
else:
|
||||
self.draw_ui(reference_id, reference)
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not tool.Ifc.get():
|
||||
return False
|
||||
try:
|
||||
return bool(context.active_object.active_material.BIMObjectProperties.ifc_definition_id)
|
||||
except:
|
||||
return False
|
||||
|
||||
def draw_add_ui(self, context):
|
||||
if not classification_prop.getClassifications(self.sprops, context):
|
||||
return
|
||||
def draw(self, context):
|
||||
if not MaterialClassificationsData.is_loaded:
|
||||
MaterialClassificationsData.load()
|
||||
self.data = MaterialClassificationsData
|
||||
self.obj = context.active_object.active_material.name
|
||||
self.obj_type = "Material"
|
||||
self.draw_ui(context)
|
||||
|
||||
name = Data.library_classifications[int(self.sprops.available_classifications)]
|
||||
if name in [c["Name"] for c in Data.classifications.values()]:
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.sprops, "available_classifications", text="")
|
||||
if self.sprops.active_library_referenced_source:
|
||||
op = row.operator("bim.change_classification_level", text="", icon="FRAME_PREV")
|
||||
op.parent_id = self.sprops.active_library_referenced_source
|
||||
op = row.operator("bim.add_classification_reference", text="", icon="ADD")
|
||||
op.reference = self.sprops.available_library_references[
|
||||
self.sprops.active_library_reference_index
|
||||
].ifc_definition_id
|
||||
self.layout.template_list(
|
||||
"BIM_UL_classifications",
|
||||
"",
|
||||
self.sprops,
|
||||
"available_library_references",
|
||||
self.sprops,
|
||||
"active_library_reference_index",
|
||||
)
|
||||
|
||||
def draw_editable_ui(self, reference):
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props.reference_attributes.get("Name"), "string_value", text="", icon="ASSET_MANAGER")
|
||||
row.operator("bim.edit_classification_reference", text="", icon="CHECKMARK")
|
||||
row.operator("bim.disable_editing_classification_reference", text="", icon="CANCEL")
|
||||
class BIM_PT_cost_classifications(Panel, ReferenceUI):
|
||||
bl_label = "IFC Cost Classifications"
|
||||
bl_idname = "BIM_PT_cost_classifications"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_cost_schedules"
|
||||
|
||||
for attribute in self.props.reference_attributes:
|
||||
if attribute.name == "Name":
|
||||
continue
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(attribute, "string_value", text=attribute.name)
|
||||
if attribute.is_optional:
|
||||
row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not tool.Ifc.get():
|
||||
return False
|
||||
return bool(context.scene.BIMCostProperties.cost_items)
|
||||
|
||||
def draw_ui(self, reference_id, reference):
|
||||
row = self.layout.row(align=True)
|
||||
if self.file.schema == "IFC2X3":
|
||||
name = reference["ItemReference"] or "No Identification"
|
||||
else:
|
||||
name = reference["Identification"] or "No Identification"
|
||||
row.label(text=name, icon="ASSET_MANAGER")
|
||||
row.label(text=reference["Name"] or "")
|
||||
if not self.props.active_reference_id:
|
||||
op = row.operator("bim.enable_editing_classification_reference", text="", icon="GREASEPENCIL")
|
||||
op.reference = reference_id
|
||||
row.operator("bim.remove_classification_reference", text="", icon="X").reference = reference_id
|
||||
def draw(self, context):
|
||||
if not CostClassificationsData.is_loaded:
|
||||
CostClassificationsData.load()
|
||||
self.data = CostClassificationsData
|
||||
self.obj = ""
|
||||
self.obj_type = "Cost"
|
||||
self.draw_ui(context)
|
||||
|
||||
|
||||
class BIM_UL_classifications(UIList):
|
||||
|
||||
@@ -64,8 +64,8 @@ class BIM_PT_context(bpy.types.Panel):
|
||||
row.operator("bim.remove_context", icon="X", text="").context = ifc_context["id"]
|
||||
|
||||
row = box.row(align=True)
|
||||
row.prop(props, "subcontexts", text="")
|
||||
row.prop(props, "target_views", text="")
|
||||
blenderbim.bim.helper.prop_with_search(row, props, "subcontexts", text="")
|
||||
blenderbim.bim.helper.prop_with_search(row, props, "target_views", text="")
|
||||
op = row.operator("bim.add_context", icon="ADD", text="")
|
||||
op.context_type = ifc_context["context_type"]
|
||||
op.context_identifier = props.subcontexts
|
||||
|
||||
@@ -27,7 +27,7 @@ from blenderbim.bim.ifc import IfcStore
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
from ifcopenshell.api.cost.data import Data
|
||||
from ifcopenshell.api.unit.data import Data as UnitData
|
||||
from ifcopenshell.api.resource.data import Data as ResourceData
|
||||
from blenderbim.bim.module.resource.data import ResourceData
|
||||
|
||||
|
||||
class AddCostSchedule(bpy.types.Operator):
|
||||
@@ -604,7 +604,7 @@ class AddCostValue(bpy.types.Operator):
|
||||
value = ifcopenshell.api.run("cost.add_cost_value", self.file, parent=parent)
|
||||
ifcopenshell.api.run("cost.edit_cost_value", self.file, cost_value=value, attributes=attributes)
|
||||
if parent.is_a("IfcConstructionResource"):
|
||||
ResourceData.load(self.file)
|
||||
ResourceData.load()
|
||||
else:
|
||||
Data.load(self.file)
|
||||
return {"FINISHED"}
|
||||
@@ -630,7 +630,7 @@ class RemoveCostItemValue(bpy.types.Operator):
|
||||
cost_value=self.file.by_id(self.cost_value),
|
||||
)
|
||||
if parent.is_a("IfcConstructionResource"):
|
||||
ResourceData.load(self.file)
|
||||
ResourceData.load()
|
||||
else:
|
||||
Data.load(self.file)
|
||||
return {"FINISHED"}
|
||||
@@ -923,24 +923,26 @@ class LoadCostItemQuantities(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
self.props = context.scene.BIMCostProperties
|
||||
self.file = IfcStore.get_file()
|
||||
Data.load(self.file)
|
||||
self.props.cost_item_products.clear()
|
||||
self.props.cost_item_processes.clear()
|
||||
self.props.cost_item_resources.clear()
|
||||
ifc_definition_id = self.props.cost_items[self.props.active_cost_item_index].ifc_definition_id
|
||||
for control_id, quantity_ids in Data.cost_items[ifc_definition_id]["Controls"].items():
|
||||
related_object = self.file.by_id(control_id)
|
||||
if related_object.is_a("IfcProduct"):
|
||||
new = self.props.cost_item_products.add()
|
||||
elif related_object.is_a("IfcProcess"):
|
||||
new = self.props.cost_item_processes.add()
|
||||
elif related_object.is_a("IfcResource"):
|
||||
new = self.props.cost_item_resources.add()
|
||||
new.ifc_definition_id = control_id
|
||||
new.name = related_object.Name or "Unnamed"
|
||||
total_quantity = 0
|
||||
for quantity_id in quantity_ids:
|
||||
total_quantity += self.file.by_id(quantity_id)[3]
|
||||
new.total_quantity = total_quantity
|
||||
if self.props.cost_items:
|
||||
ifc_definition_id = self.props.cost_items[self.props.active_cost_item_index].ifc_definition_id
|
||||
for control_id, quantity_ids in Data.cost_items[ifc_definition_id]["Controls"].items() or {}:
|
||||
related_object = self.file.by_id(control_id)
|
||||
if related_object.is_a("IfcProduct"):
|
||||
new = self.props.cost_item_products.add()
|
||||
elif related_object.is_a("IfcProcess"):
|
||||
new = self.props.cost_item_processes.add()
|
||||
elif related_object.is_a("IfcResource"):
|
||||
new = self.props.cost_item_resources.add()
|
||||
new.ifc_definition_id = control_id
|
||||
new.name = related_object.Name or "Unnamed"
|
||||
total_quantity = 0
|
||||
for quantity_id in quantity_ids:
|
||||
total_quantity += self.file.by_id(quantity_id)[3]
|
||||
new.total_quantity = total_quantity
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import bpy
|
||||
import ifcopenshell.api
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.classification.data import CostClassificationsData
|
||||
from ifcopenshell.api.cost.data import Data
|
||||
from ifcopenshell.api.pset.data import Data as PsetData
|
||||
from blenderbim.bim.prop import StrProperty, Attribute
|
||||
@@ -164,6 +165,7 @@ def update_active_cost_item_index(self, context):
|
||||
bpy.ops.bim.load_cost_item_types()
|
||||
else:
|
||||
bpy.ops.bim.load_cost_item_quantities()
|
||||
CostClassificationsData.load()
|
||||
|
||||
|
||||
def update_cost_item_identification(self, context):
|
||||
|
||||
@@ -343,6 +343,11 @@ class BIM_PT_cost_item_quantities(Panel):
|
||||
return False
|
||||
|
||||
def draw(self, context):
|
||||
if not Data.is_loaded:
|
||||
Data.load(IfcStore.get_file())
|
||||
|
||||
if not UnitData.is_loaded:
|
||||
UnitData.load(IfcStore.get_file())
|
||||
self.props = context.scene.BIMCostProperties
|
||||
|
||||
cost_item = self.props.cost_items[self.props.active_cost_item_index]
|
||||
|
||||
@@ -112,6 +112,7 @@ class CreateAllShapes(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
elements = self.file.by_type("IfcElement") + self.file.by_type("IfcSpace")
|
||||
|
||||
total = len(elements)
|
||||
settings = ifcopenshell.geom.settings()
|
||||
failures = []
|
||||
|
||||
@@ -25,6 +25,7 @@ classes = (
|
||||
operator.SelectDiffOldFile,
|
||||
operator.SelectDiffNewFile,
|
||||
operator.ExecuteIfcDiff,
|
||||
prop.Relationships,
|
||||
prop.DiffProperties,
|
||||
ui.BIM_PT_diff,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import json
|
||||
import blenderbim.tool as tool
|
||||
|
||||
|
||||
def refresh():
|
||||
DiffData.is_loaded = False
|
||||
|
||||
|
||||
class DiffData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
diff_json_file = None
|
||||
diff = None
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data["diff_json"] = cls.diff_json()
|
||||
cls.data["total_added"] = cls.total_added()
|
||||
cls.data["total_deleted"] = cls.total_deleted()
|
||||
cls.data["total_changed"] = cls.total_changed()
|
||||
cls.data["changes"] = cls.changes()
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def diff_json(cls):
|
||||
props = bpy.context.scene.DiffProperties
|
||||
if not props.diff_json_file:
|
||||
cls.diff = None
|
||||
return
|
||||
if props.diff_json_file != cls.diff_json_file:
|
||||
cls.diff_json_file = props.diff_json_file
|
||||
with open(props.diff_json_file, "r") as file:
|
||||
cls.diff = json.load(file)
|
||||
return cls.diff
|
||||
|
||||
@classmethod
|
||||
def total_added(cls):
|
||||
diff = cls.diff_json()
|
||||
if diff:
|
||||
return len(diff["added"])
|
||||
|
||||
@classmethod
|
||||
def total_deleted(cls):
|
||||
diff = cls.diff_json()
|
||||
if diff:
|
||||
return len(diff["deleted"])
|
||||
|
||||
@classmethod
|
||||
def total_changed(cls):
|
||||
diff = cls.diff_json()
|
||||
if diff:
|
||||
return len(diff["changed"].keys())
|
||||
|
||||
@classmethod
|
||||
def changes(cls):
|
||||
diff = cls.diff_json()
|
||||
if not diff:
|
||||
return {}
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
if not element or not hasattr(element, "GlobalId"):
|
||||
return {}
|
||||
changes = {k.upper().replace("_", " "): str(v) for k, v in diff["changed"].get(element.GlobalId, {}).items()}
|
||||
if element.GlobalId in diff["added"]:
|
||||
changes["Added"] = True
|
||||
elif element.GlobalId in diff["deleted"]:
|
||||
changes["Deleted"] = True
|
||||
return changes
|
||||
@@ -17,10 +17,11 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import json
|
||||
import ifccsv
|
||||
import ifcopenshell
|
||||
import json
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
import blenderbim.bim.handler
|
||||
import blenderbim.tool as tool
|
||||
|
||||
|
||||
class SelectDiffJsonFile(bpy.types.Operator):
|
||||
@@ -28,6 +29,7 @@ class SelectDiffJsonFile(bpy.types.Operator):
|
||||
bl_label = "Select Diff JSON File"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.DiffProperties.diff_json_file = self.filepath
|
||||
@@ -44,21 +46,23 @@ class VisualiseDiff(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
# ifc_file = IfcStore.get_file() # In case we get from Store
|
||||
ifc_file = ifcopenshell.open(context.scene.DiffProperties.diff_new_file) # for Now refer to the new file
|
||||
ifc_file = tool.Ifc.get()
|
||||
with open(context.scene.DiffProperties.diff_json_file, "r") as file:
|
||||
diff = json.load(file)
|
||||
for obj in context.visible_objects:
|
||||
obj.color = (1.0, 1.0, 1.0, 0.2)
|
||||
global_id = ifc_file.by_id(obj.BIMObjectProperties.ifc_definition_id).GlobalId
|
||||
obj.color = (1.0, 1.0, 1.0, 1.0)
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
global_id = getattr(element, "GlobalId", None)
|
||||
if not global_id:
|
||||
continue
|
||||
if global_id in diff["deleted"]:
|
||||
obj.color = (1.0, 0.0, 0.0, 0.2)
|
||||
obj.color = (1.0, 0.0, 0.0, 1.0)
|
||||
elif global_id in diff["added"]:
|
||||
obj.color = (0.0, 1.0, 0.0, 0.2)
|
||||
obj.color = (0.0, 1.0, 0.0, 1.0)
|
||||
elif global_id in diff["changed"]:
|
||||
obj.color = (0.0, 0.0, 1.0, 0.2)
|
||||
obj.color = (0.0, 0.0, 1.0, 1.0)
|
||||
area = next(area for area in context.screen.areas if area.type == "VIEW_3D")
|
||||
area.spaces[0].shading.color_type = "OBJECT"
|
||||
return {"FINISHED"}
|
||||
@@ -69,9 +73,10 @@ class SelectDiffOldFile(bpy.types.Operator):
|
||||
bl_label = "Select Diff Old File"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.DiffProperties.diff_old_file = self.filepath
|
||||
context.scene.DiffProperties.old_file = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
@@ -84,9 +89,10 @@ class SelectDiffNewFile(bpy.types.Operator):
|
||||
bl_label = "Select Diff New File"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.DiffProperties.diff_new_file = self.filepath
|
||||
context.scene.DiffProperties.new_file = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
@@ -110,12 +116,14 @@ class ExecuteIfcDiff(bpy.types.Operator):
|
||||
import ifcdiff
|
||||
|
||||
ifc_diff = ifcdiff.IfcDiff(
|
||||
context.scene.DiffProperties.diff_old_file,
|
||||
context.scene.DiffProperties.diff_new_file,
|
||||
context.scene.DiffProperties.old_file,
|
||||
context.scene.DiffProperties.new_file,
|
||||
self.filepath,
|
||||
context.scene.DiffProperties.diff_relationships.split(),
|
||||
[r.relationship for r in context.scene.DiffProperties.diff_relationships],
|
||||
context.scene.DiffProperties.diff_filter_elements,
|
||||
)
|
||||
ifc_diff.diff()
|
||||
ifc_diff.export()
|
||||
context.scene.DiffProperties.diff_json_file = self.filepath
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import bpy
|
||||
from blenderbim.bim.prop import StrProperty
|
||||
from blenderbim.bim.module.diff.data import DiffData
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
@@ -31,8 +32,20 @@ from bpy.props import (
|
||||
)
|
||||
|
||||
|
||||
def update_diff_json_file(self, context):
|
||||
DiffData.data["diff_json"] = DiffData.diff_json()
|
||||
|
||||
|
||||
class Relationships(PropertyGroup):
|
||||
relationship: EnumProperty(
|
||||
name="Relationship",
|
||||
items=[(r, r.capitalize(), r) for r in ["type", "property", "container", "aggregate", "classification"]],
|
||||
)
|
||||
|
||||
|
||||
class DiffProperties(PropertyGroup):
|
||||
diff_json_file: StringProperty(default="", name="Diff JSON File")
|
||||
diff_old_file: StringProperty(default="", name="Diff Old IFC File")
|
||||
diff_new_file: StringProperty(default="", name="Diff New IFC File")
|
||||
diff_relationships: StringProperty(default="", name="Diff Relationships")
|
||||
diff_json_file: StringProperty(default="", name="JSON Output", update=update_diff_json_file)
|
||||
old_file: StringProperty(default="", name="Old IFC File")
|
||||
new_file: StringProperty(default="", name="New IFC File")
|
||||
diff_relationships: CollectionProperty(type=Relationships, name="Relationships")
|
||||
diff_filter_elements: StringProperty(default="", name="Filter")
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
|
||||
from bpy.types import Panel
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.diff.data import DiffData
|
||||
import blenderbim.tool as tool
|
||||
import json
|
||||
|
||||
|
||||
class BIM_PT_diff(Panel):
|
||||
@@ -30,30 +33,70 @@ class BIM_PT_diff(Panel):
|
||||
bl_parent_id = "BIM_PT_quality_control"
|
||||
|
||||
def draw(self, context):
|
||||
if not DiffData.is_loaded:
|
||||
DiffData.load()
|
||||
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
|
||||
scene = context.scene
|
||||
bim_properties = scene.DiffProperties
|
||||
props = scene.DiffProperties
|
||||
|
||||
layout.label(text="IFC Diff Setup:")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(bim_properties, "diff_old_file")
|
||||
row.prop(props, "old_file")
|
||||
row.operator("bim.select_diff_old_file", icon="FILE_FOLDER", text="")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(bim_properties, "diff_new_file")
|
||||
row.prop(props, "new_file")
|
||||
row.operator("bim.select_diff_new_file", icon="FILE_FOLDER", text="")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(bim_properties, "diff_relationships")
|
||||
row.prop(props, "diff_relationships")
|
||||
row.context_pointer_set("bim_prop_group", props)
|
||||
add = row.operator("bim.edit_blender_collection", icon="ADD", text="")
|
||||
add.option = "add"
|
||||
add.collection = "diff_relationships"
|
||||
|
||||
for index, r in enumerate(props.diff_relationships):
|
||||
row = layout.row(align=True)
|
||||
row.context_pointer_set("bim_prop_group", props)
|
||||
row.prop(r, "relationship", text=" ")
|
||||
remove = row.operator("bim.edit_blender_collection", icon="REMOVE", text="")
|
||||
remove.option = "remove"
|
||||
remove.collection = "diff_relationships"
|
||||
remove.index = index
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "diff_filter_elements")
|
||||
row.operator("bim.ifc_selector", icon="FILTER", text="")
|
||||
|
||||
row = layout.row()
|
||||
row.operator("bim.execute_ifc_diff")
|
||||
|
||||
# TODO: show if there ifc diff operation is sucessful
|
||||
row = layout.row(align=True)
|
||||
row.prop(bim_properties, "diff_json_file")
|
||||
row.prop(props, "diff_json_file")
|
||||
row.operator("bim.select_diff_json_file", icon="FILE_FOLDER", text="")
|
||||
row.operator("bim.visualise_diff", icon="HIDE_OFF", text="")
|
||||
|
||||
if DiffData.data["diff_json"]:
|
||||
row = layout.row()
|
||||
row.alignment = "CENTER"
|
||||
row.label(text=f"{DiffData.data['total_added']} added")
|
||||
row.label(text=f"{DiffData.data['total_deleted']} deleted")
|
||||
row.label(text=f"{DiffData.data['total_changed']} changed")
|
||||
|
||||
if DiffData.data["changes"]:
|
||||
box = layout.box()
|
||||
row = box.row()
|
||||
row.label(text="Active Object Changes:")
|
||||
for key, value in DiffData.data["changes"].items():
|
||||
row = box.row()
|
||||
if key == "Added":
|
||||
icon = "ADD"
|
||||
elif key == "Deleted":
|
||||
icon = "X"
|
||||
else:
|
||||
icon = "GREASEPENCIL"
|
||||
row.label(text=key, icon=icon)
|
||||
|
||||
@@ -34,20 +34,19 @@ classes = (
|
||||
operator.BuildSchedule,
|
||||
operator.CleanWireframes,
|
||||
operator.ContractSheet,
|
||||
operator.ConvertEdgeToArc,
|
||||
operator.CopyGrid,
|
||||
operator.CreateDrawing,
|
||||
operator.CreateSheets,
|
||||
operator.DisableEditingAssignedProduct,
|
||||
operator.DisableEditingDrawings,
|
||||
operator.DisableEditingSchedules,
|
||||
operator.DisableEditingSheets,
|
||||
operator.DisableEditingText,
|
||||
operator.DisableEditingTextProduct,
|
||||
operator.EditAssignedProduct,
|
||||
operator.EditText,
|
||||
operator.EditTextProduct,
|
||||
operator.EditVectorStyle,
|
||||
operator.EnableEditingAssignedProduct,
|
||||
operator.EnableEditingText,
|
||||
operator.EnableEditingTextProduct,
|
||||
operator.ExpandSheet,
|
||||
operator.LoadDrawings,
|
||||
operator.LoadSchedules,
|
||||
@@ -72,12 +71,14 @@ classes = (
|
||||
prop.DocProperties,
|
||||
prop.BIMCameraProperties,
|
||||
prop.BIMTextProperties,
|
||||
prop.BIMAssignedProductProperties,
|
||||
ui.BIM_PT_camera,
|
||||
ui.BIM_PT_drawing_underlay,
|
||||
ui.BIM_PT_annotation_utilities,
|
||||
ui.BIM_PT_sheets,
|
||||
ui.BIM_PT_drawings,
|
||||
ui.BIM_PT_schedules,
|
||||
ui.BIM_PT_product_assignments,
|
||||
ui.BIM_PT_text,
|
||||
ui.BIM_UL_drawinglist,
|
||||
ui.BIM_UL_sheets,
|
||||
@@ -92,6 +93,7 @@ classes = (
|
||||
def register():
|
||||
bpy.types.Scene.DocProperties = bpy.props.PointerProperty(type=prop.DocProperties)
|
||||
bpy.types.Camera.BIMCameraProperties = bpy.props.PointerProperty(type=prop.BIMCameraProperties)
|
||||
bpy.types.Object.BIMAssignedProductProperties = bpy.props.PointerProperty(type=prop.BIMAssignedProductProperties)
|
||||
bpy.types.Object.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties)
|
||||
bpy.types.TextCurve.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties)
|
||||
bpy.app.handlers.load_post.append(handler.toggleDecorationsOnLoad)
|
||||
@@ -101,6 +103,8 @@ def register():
|
||||
def unregister():
|
||||
del bpy.types.Scene.DocProperties
|
||||
del bpy.types.Camera.BIMCameraProperties
|
||||
del bpy.types.Object.BIMAssignedProductProperties
|
||||
del bpy.types.Object.BIMTextProperties
|
||||
del bpy.types.TextCurve.BIMTextProperties
|
||||
bpy.app.handlers.load_post.remove(handler.toggleDecorationsOnLoad)
|
||||
bpy.app.handlers.depsgraph_update_pre.remove(handler.depsgraph_update_pre_handler)
|
||||
|
||||
@@ -151,10 +151,17 @@ class Annotator:
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def get_annotation_obj(object_type, data_type):
|
||||
collection = bpy.context.scene.camera.users_collection[0]
|
||||
def get_annotation_obj(drawing, object_type, data_type):
|
||||
camera = tool.Ifc.get_object(drawing)
|
||||
co1, _, _, _ = Annotator.get_placeholder_coords(camera)
|
||||
matrix_world = camera.matrix_world.copy()
|
||||
matrix_world[0][3] = co1.x
|
||||
matrix_world[1][3] = co1.y
|
||||
matrix_world[2][3] = co1.z
|
||||
collection = camera.users_collection[0]
|
||||
if object_type == "TEXT":
|
||||
obj = bpy.data.objects.new(object_type, None)
|
||||
obj.matrix_world = matrix_world
|
||||
collection.objects.link(obj)
|
||||
return obj
|
||||
elif object_type in ("TEXT_LEADER", "SECTION_LEVEL"):
|
||||
@@ -162,12 +169,14 @@ class Annotator:
|
||||
data.dimensions = "3D"
|
||||
data.resolution_u = 2
|
||||
obj = bpy.data.objects.new(object_type, data)
|
||||
obj.matrix_world = matrix_world
|
||||
collection.objects.link(obj)
|
||||
return obj
|
||||
for obj in collection.objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element and element.ObjectType == object_type:
|
||||
return obj
|
||||
if object_type != "ANGLE":
|
||||
for obj in collection.objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element and element.ObjectType == object_type:
|
||||
return obj
|
||||
if data_type == "mesh":
|
||||
data = bpy.data.meshes.new(object_type)
|
||||
elif data_type == "curve":
|
||||
@@ -175,12 +184,14 @@ class Annotator:
|
||||
data.dimensions = "3D"
|
||||
data.resolution_u = 2
|
||||
obj = bpy.data.objects.new(object_type, data)
|
||||
obj.matrix_world = matrix_world
|
||||
collection.objects.link(obj)
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def get_placeholder_coords():
|
||||
camera = bpy.context.scene.camera
|
||||
def get_placeholder_coords(camera=None):
|
||||
if not camera:
|
||||
camera = bpy.context.scene.camera
|
||||
z_offset = camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
|
||||
if bpy.context.scene.render.resolution_x > bpy.context.scene.render.resolution_y:
|
||||
y = (
|
||||
|
||||
@@ -22,19 +22,40 @@ import blenderbim.tool as tool
|
||||
|
||||
|
||||
def refresh():
|
||||
ProductAssignmentsData.is_loaded = False
|
||||
TextData.is_loaded = False
|
||||
SheetsData.is_loaded = False
|
||||
SchedulesData.is_loaded = False
|
||||
DrawingsData.is_loaded = False
|
||||
|
||||
|
||||
class ProductAssignmentsData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {"relating_product": cls.relating_product()}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def relating_product(cls):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
if not element or not element.is_a("IfcAnnotation"):
|
||||
return
|
||||
for rel in element.HasAssignments:
|
||||
if rel.is_a("IfcRelAssignsToProduct"):
|
||||
name = rel.RelatingProduct.Name or "Unnamed"
|
||||
return f"{rel.RelatingProduct.is_a()}/{name}"
|
||||
|
||||
|
||||
class TextData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {"attributes": cls.attributes(), "relating_product": cls.relating_product()}
|
||||
cls.data = {"attributes": cls.attributes()}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
@@ -49,15 +70,6 @@ class TextData:
|
||||
{"name": "BoxAlignment", "value": text_literal.BoxAlignment},
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def relating_product(cls):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
if not element or not element.is_a("IfcAnnotation") or element.ObjectType not in ["TEXT", "TEXT_LEADER"]:
|
||||
return
|
||||
for rel in element.HasAssignments:
|
||||
if rel.is_a("IfcRelAssignsToProduct"):
|
||||
return rel.RelatingProduct.Name or "Unnamed"
|
||||
|
||||
|
||||
class SheetsData:
|
||||
data = {}
|
||||
|
||||
@@ -317,7 +317,7 @@ class OffsetHandle:
|
||||
base = Vector((0, 0, 0))
|
||||
axis = Vector((0, 0, 1))
|
||||
|
||||
# projecttion of the arrow to a plane, perpendicular to view ray
|
||||
# projection of the arrow to a plane, perpendicular to view ray
|
||||
axis_proj = axis - ray_norm * axis.dot(ray_norm)
|
||||
|
||||
# intersection of the axis with the plane through view origin perpendicular to the arrow projection
|
||||
|
||||
@@ -88,17 +88,17 @@ class BoundingBox:
|
||||
def intersect(self, bb):
|
||||
retVal = False
|
||||
if self.vertex != None and bb.vertex != None:
|
||||
# check all the faces of this object for a seperation axis
|
||||
# check all the faces of this object for a separation axis
|
||||
for i, f in enumerate(self.face):
|
||||
d = f.normal
|
||||
if self.whichSide(bb.vertex, d, f.vertex[0]) > 0:
|
||||
return False # all the vertexes are on the +ve side of the face
|
||||
return False # all the vertices are on the +ve side of the face
|
||||
|
||||
# now do it again for the other objects faces
|
||||
for i, f in enumerate(bb.face):
|
||||
d = f.normal
|
||||
if self.whichSide(self.vertex, d, f.vertex[0]) > 0:
|
||||
return False # all the vertexes are on the +ve side of the face
|
||||
return False # all the vertices are on the +ve side of the face
|
||||
|
||||
# do edge checks
|
||||
for e1 in self.edge:
|
||||
@@ -139,7 +139,7 @@ def format_distance(value, isArea=False, hide_units=True):
|
||||
|
||||
value *= scaleFactor
|
||||
|
||||
# Imperial Formating
|
||||
# Imperial Formatting
|
||||
if unit_system == "IMPERIAL":
|
||||
precision = bpy.context.scene.BIMProperties.imperial_precision
|
||||
if precision == "NONE":
|
||||
@@ -152,7 +152,7 @@ def format_distance(value, isArea=False, hide_units=True):
|
||||
base = int(precision)
|
||||
decInches = value * toInches
|
||||
|
||||
# Seperate ft and inches
|
||||
# Separate ft and inches
|
||||
# Unless Inches are the specified Length Unit
|
||||
if unit_length != "INCHES":
|
||||
feet = math.floor(decInches / inPerFoot)
|
||||
@@ -160,7 +160,7 @@ def format_distance(value, isArea=False, hide_units=True):
|
||||
else:
|
||||
feet = 0
|
||||
|
||||
# Seperate Fractional Inches
|
||||
# Separate Fractional Inches
|
||||
inches = math.floor(decInches)
|
||||
if inches != 0:
|
||||
frac = round(base * (decInches - inches))
|
||||
@@ -202,7 +202,7 @@ def format_distance(value, isArea=False, hide_units=True):
|
||||
else:
|
||||
tx_dist = str("%1.3f" % (value * toInches / inPerFoot)) + " sq. ft."
|
||||
|
||||
# METRIC FORMATING
|
||||
# METRIC FORMATTING
|
||||
elif unit_system == "METRIC":
|
||||
precision = bpy.context.scene.BIMProperties.metric_precision
|
||||
if precision != 0:
|
||||
|
||||
@@ -40,8 +40,8 @@ import blenderbim.bim.module.drawing.scheduler as scheduler
|
||||
import blenderbim.bim.module.drawing.helper as helper
|
||||
import blenderbim.bim.export_ifc
|
||||
from lxml import etree
|
||||
from math import pi
|
||||
from mathutils import Vector, Matrix, Euler, geometry
|
||||
from mathutils import Vector
|
||||
from timeit import default_timer as timer
|
||||
from blenderbim.bim.module.drawing.prop import RasterStyleProperty
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from ifcopenshell.api.group.data import Data as GroupData
|
||||
@@ -50,6 +50,21 @@ from ifcopenshell.api.pset.data import Data as PsetData
|
||||
cwd = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
|
||||
class profile:
|
||||
"""
|
||||
A python context manager timing utility
|
||||
"""
|
||||
|
||||
def __init__(self, task):
|
||||
self.task = task
|
||||
|
||||
def __enter__(self):
|
||||
self.start = timer()
|
||||
|
||||
def __exit__(self, *args):
|
||||
print(self.task, timer() - self.start)
|
||||
|
||||
|
||||
def open_with_user_command(user_command, path):
|
||||
if user_command:
|
||||
commands = eval(user_command)
|
||||
@@ -116,53 +131,75 @@ class CreateDrawing(bpy.types.Operator):
|
||||
self.camera = context.scene.camera
|
||||
self.camera_element = tool.Ifc.get_entity(self.camera)
|
||||
self.file = IfcStore.get_file()
|
||||
self.time = None
|
||||
start = time.time()
|
||||
self.profile_code("Start drawing generation process")
|
||||
self.props = context.scene.DocProperties
|
||||
self.cprops = self.camera.data.BIMCameraProperties
|
||||
self.drawing_name = self.file.by_id(self.camera.BIMObjectProperties.ifc_definition_id).Name
|
||||
self.get_scale()
|
||||
if self.cprops.update_representation(self.camera):
|
||||
bpy.ops.bim.update_representation(obj=self.camera.name, ifc_representation_class="")
|
||||
underlay_svg = self.generate_underlay(context)
|
||||
self.profile_code("Generate underlay")
|
||||
linework_svg = self.generate_linework(context)
|
||||
self.profile_code("Generate linework")
|
||||
annotation_svg = self.generate_annotation(context)
|
||||
self.profile_code("Generate annotation")
|
||||
svg_path = self.combine_svgs(context, underlay_svg, linework_svg, annotation_svg)
|
||||
self.profile_code("Combine SVG layers")
|
||||
|
||||
with profile("Drawing generation process"):
|
||||
|
||||
with profile("Initialize drawing generation process"):
|
||||
self.props = context.scene.DocProperties
|
||||
self.cprops = self.camera.data.BIMCameraProperties
|
||||
self.drawing_name = self.file.by_id(self.camera.BIMObjectProperties.ifc_definition_id).Name
|
||||
self.get_scale()
|
||||
if self.cprops.update_representation(self.camera):
|
||||
bpy.ops.bim.update_representation(obj=self.camera.name, ifc_representation_class="")
|
||||
|
||||
self.svg_writer = svgwriter.SvgWriter()
|
||||
self.svg_writer.human_scale = self.human_scale
|
||||
self.svg_writer.scale = self.scale
|
||||
self.svg_writer.data_dir = context.scene.BIMProperties.data_dir
|
||||
self.svg_writer.camera = self.camera
|
||||
self.svg_writer.camera_width, self.svg_writer.camera_height = self.get_camera_dimensions()
|
||||
self.svg_writer.camera_projection = tuple(self.camera.matrix_world.to_quaternion() @ Vector((0, 0, -1)))
|
||||
|
||||
with profile("Generate underlay"):
|
||||
underlay_svg = self.generate_underlay(context)
|
||||
|
||||
with profile("Generate linework"):
|
||||
linework_svg = self.generate_linework(context)
|
||||
|
||||
with profile("Generate annotation"):
|
||||
annotation_svg = self.generate_annotation(context)
|
||||
|
||||
with profile("Combine SVG layers"):
|
||||
svg_path = self.combine_svgs(context, underlay_svg, linework_svg, annotation_svg)
|
||||
|
||||
open_with_user_command(context.preferences.addons["blenderbim"].preferences.svg_command, svg_path)
|
||||
print("Total Time: {:.2f}".format(time.time() - start))
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def profile_code(self, message):
|
||||
if not self.time:
|
||||
self.time = time.time()
|
||||
print("{} :: {:.2f}".format(message, time.time() - self.time))
|
||||
self.time = time.time()
|
||||
def get_camera_dimensions(self):
|
||||
render = bpy.context.scene.render
|
||||
if self.is_landscape(render):
|
||||
width = self.camera.data.ortho_scale
|
||||
height = width / render.resolution_x * render.resolution_y
|
||||
else:
|
||||
height = self.camera.data.ortho_scale
|
||||
width = height / render.resolution_y * render.resolution_x
|
||||
return width, height
|
||||
|
||||
def combine_svgs(self, context, underlay, linework, annotation):
|
||||
# Hacky :)
|
||||
svg_path = os.path.join(context.scene.BIMProperties.data_dir, "diagrams", self.drawing_name + ".svg")
|
||||
with open(svg_path, "w") as outfile:
|
||||
has_boilerplate = False
|
||||
pset = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"]
|
||||
self.svg_writer.create_blank_svg(svg_path).define_boilerplate(
|
||||
pset.get("Stylesheet"), pset.get("Markers"), pset.get("Symbols"), pset.get("Patterns")
|
||||
)
|
||||
boilerplate = self.svg_writer.svg.tostring()
|
||||
outfile.write(boilerplate.replace("</svg>", ""))
|
||||
if underlay:
|
||||
with open(underlay) as infile:
|
||||
for line in infile:
|
||||
if "<svg " in line:
|
||||
line = line.replace('">', '" xmlns:ifc="http://www.ifcopenshell.org/ns">')
|
||||
if "</svg>" in line:
|
||||
for i, line in enumerate(infile):
|
||||
if i < 2:
|
||||
continue
|
||||
elif "</svg>" in line:
|
||||
continue
|
||||
outfile.write(line)
|
||||
shutil.copyfile(underlay[0:-4] + ".png", svg_path[0:-4] + "-underlay.png")
|
||||
has_boilerplate = True
|
||||
if linework:
|
||||
with open(linework) as infile:
|
||||
should_skip = False
|
||||
for i, line in enumerate(infile):
|
||||
if has_boilerplate and i == 0:
|
||||
if i == 0:
|
||||
continue
|
||||
if "</svg>" in line:
|
||||
continue
|
||||
@@ -175,11 +212,10 @@ class CreateDrawing(bpy.types.Operator):
|
||||
elif should_skip:
|
||||
continue
|
||||
outfile.write(line)
|
||||
has_boilerplate = True
|
||||
if annotation:
|
||||
with open(annotation) as infile:
|
||||
for i, line in enumerate(infile):
|
||||
if has_boilerplate and i in [0, 1]:
|
||||
if i < 2:
|
||||
continue
|
||||
if "</svg>" in line:
|
||||
continue
|
||||
@@ -224,25 +260,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
for name, value in previous_visibility.items():
|
||||
bpy.data.objects[name].hide_set(value)
|
||||
|
||||
svg_writer = svgwriter.SvgWriter()
|
||||
render = context.scene.render
|
||||
if self.is_landscape(render):
|
||||
width = self.camera.data.ortho_scale
|
||||
height = width / render.resolution_x * render.resolution_y
|
||||
else:
|
||||
height = self.camera.data.ortho_scale
|
||||
width = height / render.resolution_y * render.resolution_x
|
||||
svg_writer.human_scale = self.human_scale
|
||||
svg_writer.scale = self.scale
|
||||
svg_writer.output = svg_path
|
||||
svg_writer.data_dir = context.scene.BIMProperties.data_dir
|
||||
svg_writer.vector_style = drawing_style.vector_style
|
||||
svg_writer.camera = self.camera
|
||||
svg_writer.camera_width = width
|
||||
svg_writer.camera_height = height
|
||||
svg_writer.camera_projection = tuple(self.camera.matrix_world.to_quaternion() @ Vector((0, 0, -1)))
|
||||
svg_writer.background_image = context.scene.render.filepath
|
||||
svg_writer.write("underlay")
|
||||
self.svg_writer.create_blank_svg(svg_path).draw_underlay(context.scene.render.filepath).save()
|
||||
return svg_path
|
||||
|
||||
def generate_linework(self, context):
|
||||
@@ -252,13 +270,14 @@ class CreateDrawing(bpy.types.Operator):
|
||||
if os.path.isfile(svg_path) and self.props.should_use_linework_cache:
|
||||
return svg_path
|
||||
|
||||
# All very hackish whilst prototyping
|
||||
exporter = blenderbim.bim.export_ifc.IfcExporter(None)
|
||||
exporter.file = tool.Ifc.get()
|
||||
invalidated_guids = exporter.sync_deletions()
|
||||
invalidated_elements = exporter.sync_all_objects()
|
||||
invalidated_elements += exporter.sync_edited_objects()
|
||||
[invalidated_guids.append(e.GlobalId) for e in invalidated_elements if hasattr(e, "GlobalId")]
|
||||
with profile("sync"):
|
||||
# All very hackish whilst prototyping
|
||||
exporter = blenderbim.bim.export_ifc.IfcExporter(None)
|
||||
exporter.file = tool.Ifc.get()
|
||||
invalidated_guids = exporter.sync_deletions()
|
||||
invalidated_elements = exporter.sync_all_objects()
|
||||
invalidated_elements += exporter.sync_edited_objects()
|
||||
[invalidated_guids.append(e.GlobalId) for e in invalidated_elements if hasattr(e, "GlobalId")]
|
||||
|
||||
# If we have already calculated it in the SVG in the past, don't recalculate
|
||||
edited_guids = set()
|
||||
@@ -309,40 +328,46 @@ class CreateDrawing(bpy.types.Operator):
|
||||
cache = IfcStore.get_cache()
|
||||
[cache.remove(guid) for guid in invalidated_guids]
|
||||
|
||||
if target_view_contexts and elements:
|
||||
geom_settings = ifcopenshell.geom.settings(
|
||||
DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True, INCLUDE_CURVES=True
|
||||
)
|
||||
geom_settings.set_context_ids(target_view_contexts)
|
||||
it = ifcopenshell.geom.iterator(geom_settings, ifc, multiprocessing.cpu_count(), include=elements)
|
||||
it.set_cache(cache)
|
||||
processed = set()
|
||||
for elem in self.yield_from_iterator(it):
|
||||
processed.add(ifc.by_id(elem.id))
|
||||
self.serialiser.write(elem)
|
||||
elements -= processed
|
||||
with profile("Processing target view context"):
|
||||
if target_view_contexts and elements:
|
||||
geom_settings = ifcopenshell.geom.settings(
|
||||
DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True, INCLUDE_CURVES=True
|
||||
)
|
||||
geom_settings.set_context_ids(target_view_contexts)
|
||||
it = ifcopenshell.geom.iterator(geom_settings, ifc, multiprocessing.cpu_count(), include=elements)
|
||||
it.set_cache(cache)
|
||||
processed = set()
|
||||
for elem in self.yield_from_iterator(it):
|
||||
processed.add(ifc.by_id(elem.id))
|
||||
self.serialiser.write(elem)
|
||||
elements -= processed
|
||||
|
||||
if body_contexts and elements:
|
||||
with profile("Processing body context"):
|
||||
if body_contexts and elements:
|
||||
geom_settings = ifcopenshell.geom.settings(DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True)
|
||||
geom_settings.set_context_ids(body_contexts)
|
||||
it = ifcopenshell.geom.iterator(geom_settings, ifc, multiprocessing.cpu_count(), include=elements)
|
||||
it.set_cache(cache)
|
||||
for elem in self.yield_from_iterator(it):
|
||||
self.serialiser.write(elem)
|
||||
|
||||
with profile("Camera element"):
|
||||
# @todo tfk: is this needed?
|
||||
geom_settings = ifcopenshell.geom.settings(DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True)
|
||||
geom_settings.set_context_ids(body_contexts)
|
||||
it = ifcopenshell.geom.iterator(geom_settings, ifc, multiprocessing.cpu_count(), include=elements)
|
||||
it.set_cache(cache)
|
||||
it = ifcopenshell.geom.iterator(geom_settings, ifc, include=[self.camera_element])
|
||||
for elem in self.yield_from_iterator(it):
|
||||
self.serialiser.write(elem)
|
||||
|
||||
geom_settings = ifcopenshell.geom.settings(DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True)
|
||||
it = ifcopenshell.geom.iterator(geom_settings, ifc, include=[self.camera_element])
|
||||
for elem in self.yield_from_iterator(it):
|
||||
self.serialiser.write(elem)
|
||||
|
||||
self.serialiser.finalize()
|
||||
with profile("Finalizing"):
|
||||
self.serialiser.finalize()
|
||||
results = self.svg_buffer.get_value()
|
||||
|
||||
root = etree.fromstring(results)
|
||||
self.enrich_linework_with_metadata(root)
|
||||
self.move_projection_to_bottom(root)
|
||||
with open(svg_path, "wb") as svg:
|
||||
svg.write(etree.tostring(root))
|
||||
with profile("Post processing"):
|
||||
root = etree.fromstring(results)
|
||||
self.enrich_linework_with_metadata(root)
|
||||
self.move_projection_to_bottom(root)
|
||||
with open(svg_path, "wb") as svg:
|
||||
svg.write(etree.tostring(root))
|
||||
|
||||
return svg_path
|
||||
|
||||
@@ -416,35 +441,13 @@ class CreateDrawing(bpy.types.Operator):
|
||||
if os.path.isfile(svg_path) and self.props.should_use_annotation_cache:
|
||||
return svg_path
|
||||
|
||||
camera = self.camera
|
||||
svg_writer = svgwriter.SvgWriter()
|
||||
|
||||
drawing_style = context.scene.DocProperties.drawing_styles[self.cprops.active_drawing_style_index]
|
||||
|
||||
render = context.scene.render
|
||||
if self.is_landscape(render):
|
||||
width = camera.data.ortho_scale
|
||||
height = width / render.resolution_x * render.resolution_y
|
||||
else:
|
||||
height = camera.data.ortho_scale
|
||||
width = height / render.resolution_y * render.resolution_x
|
||||
|
||||
svg_writer.human_scale = self.human_scale
|
||||
svg_writer.scale = self.scale
|
||||
svg_writer.output = svg_path
|
||||
svg_writer.data_dir = context.scene.BIMProperties.data_dir
|
||||
svg_writer.vector_style = drawing_style.vector_style
|
||||
svg_writer.camera = camera
|
||||
svg_writer.camera_width = width
|
||||
svg_writer.camera_height = height
|
||||
svg_writer.camera_projection = tuple(camera.matrix_world.to_quaternion() @ Vector((0, 0, -1)))
|
||||
|
||||
elements = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(self.camera_element))
|
||||
svg_writer.annotations = sorted(elements, key=lambda a: tool.Drawing.get_annotation_z_index(a))
|
||||
svg_writer.metadata = tool.Drawing.get_drawing_metadata(self.camera_element)
|
||||
annotations = sorted(elements, key=lambda a: tool.Drawing.get_annotation_z_index(a))
|
||||
|
||||
svg_writer.write("annotation")
|
||||
return svg_writer.output
|
||||
self.svg_writer.metadata = tool.Drawing.get_drawing_metadata(self.camera_element)
|
||||
self.svg_writer.create_blank_svg(svg_path).draw_annotations(annotations).save()
|
||||
|
||||
return svg_path
|
||||
|
||||
def get_scale(self):
|
||||
if self.camera.data.BIMCameraProperties.diagram_scale == "CUSTOM":
|
||||
@@ -506,8 +509,11 @@ class AddAnnotation(bpy.types.Operator, Operator):
|
||||
def _execute(self, context):
|
||||
drawing = tool.Ifc.get_entity(context.scene.camera)
|
||||
if not drawing:
|
||||
self.report({"WARNING"}, "Not a BIM camera")
|
||||
return
|
||||
core.add_annotation(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing, object_type=self.object_type)
|
||||
r = core.add_annotation(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing, object_type=self.object_type)
|
||||
if isinstance(r, str):
|
||||
self.report({"WARNING"}, r)
|
||||
|
||||
|
||||
class AddSheet(bpy.types.Operator, Operator):
|
||||
@@ -1189,7 +1195,7 @@ class AddSectionsAnnotations(bpy.types.Operator):
|
||||
]
|
||||
|
||||
def sideview(cam):
|
||||
# leftmost and righmost points of camera view area, local coords
|
||||
# leftmost and rightmost points of camera view area, local coords
|
||||
xmin, xmax, _, _, _, _ = helper.ortho_view_frame(cam.data, margin=0)
|
||||
proj = camera.matrix_world.inverted() @ cam.matrix_world
|
||||
p_l = proj @ Vector((xmin, 0, 0))
|
||||
@@ -1247,34 +1253,34 @@ class DisableEditingText(bpy.types.Operator, Operator):
|
||||
core.disable_editing_text(tool.Drawing, obj=context.active_object)
|
||||
|
||||
|
||||
class EditTextProduct(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.edit_text_product"
|
||||
class EditAssignedProduct(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.edit_assigned_product"
|
||||
bl_label = "Edit Text Product"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
product = None
|
||||
if context.active_object.BIMTextProperties.relating_product:
|
||||
product = tool.Ifc.get_entity(context.active_object.BIMTextProperties.relating_product)
|
||||
core.edit_text_product(tool.Ifc, tool.Drawing, obj=context.active_object, product=product)
|
||||
if context.active_object.BIMAssignedProductProperties.relating_product:
|
||||
product = tool.Ifc.get_entity(context.active_object.BIMAssignedProductProperties.relating_product)
|
||||
core.edit_assigned_product(tool.Ifc, tool.Drawing, obj=context.active_object, product=product)
|
||||
|
||||
|
||||
class EnableEditingTextProduct(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.enable_editing_text_product"
|
||||
class EnableEditingAssignedProduct(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.enable_editing_assigned_product"
|
||||
bl_label = "Enable Editing Text Product"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
core.enable_editing_text_product(tool.Drawing, obj=context.active_object)
|
||||
core.enable_editing_assigned_product(tool.Drawing, obj=context.active_object)
|
||||
|
||||
|
||||
class DisableEditingTextProduct(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.disable_editing_text_product"
|
||||
class DisableEditingAssignedProduct(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.disable_editing_assigned_product"
|
||||
bl_label = "Disable Editing Text Product"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
core.disable_editing_text_product(tool.Drawing, obj=context.active_object)
|
||||
core.disable_editing_assigned_product(tool.Drawing, obj=context.active_object)
|
||||
|
||||
|
||||
class LoadSheets(bpy.types.Operator, Operator):
|
||||
@@ -1357,44 +1363,3 @@ class ContractSheet(bpy.types.Operator):
|
||||
sheet.is_expanded = False
|
||||
core.load_sheets(tool.Drawing)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ConvertEdgeToArc(bpy.types.Operator):
|
||||
bl_idname = "bim.convert_edge_to_arc"
|
||||
bl_label = "Convert Edge to Arc"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1)
|
||||
should_flip: bpy.props.BoolProperty(name="Flip", description="Flip arc", default=False)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
for prop in self.__class__.__annotations__.keys():
|
||||
layout.prop(self, prop)
|
||||
|
||||
def execute(self, context):
|
||||
if bpy.context.mode != "EDIT_MESH":
|
||||
return {"CANCELLED"}
|
||||
obj = bpy.context.active_object
|
||||
cursor = obj.matrix_world.inverted() @ bpy.context.scene.cursor.location
|
||||
mesh = obj.data
|
||||
bm = bmesh.from_edit_mesh(mesh)
|
||||
selected_verts = [v for v in bm.verts if v.select]
|
||||
if len(selected_verts) != 2:
|
||||
return {"CANCELLED"}
|
||||
l1 = selected_verts[0].co - cursor
|
||||
l2 = selected_verts[1].co - cursor
|
||||
angle = l1.xy.angle_signed(l2.xy)
|
||||
|
||||
if self.should_flip:
|
||||
if angle > 0:
|
||||
angle = -((pi * 2) - angle)
|
||||
else:
|
||||
angle = (pi * 2) + angle
|
||||
|
||||
v = selected_verts[0]
|
||||
bm.verts.remove(selected_verts[1])
|
||||
bmesh.ops.spin(bm, geom=[v], axis=(0, 0, 1), cent=cursor, steps=self.resolution * 4, angle=-angle)
|
||||
bmesh.update_edit_mesh(mesh)
|
||||
mesh.update()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -377,7 +377,6 @@ class BIMCameraProperties(PropertyGroup):
|
||||
|
||||
class BIMTextProperties(PropertyGroup):
|
||||
is_editing: BoolProperty(name="Is Editing", default=False)
|
||||
is_editing_product: BoolProperty(name="Is Editing Product", default=False)
|
||||
attributes: CollectionProperty(name="Attributes", type=Attribute)
|
||||
value: StringProperty(name="Value", default="TEXT")
|
||||
font_size: EnumProperty(
|
||||
@@ -392,4 +391,8 @@ class BIMTextProperties(PropertyGroup):
|
||||
update=refreshFontSize,
|
||||
name="Font Size",
|
||||
)
|
||||
|
||||
|
||||
class BIMAssignedProductProperties(PropertyGroup):
|
||||
is_editing_product: BoolProperty(name="Is Editing Product", default=False)
|
||||
relating_product: PointerProperty(name="Relating Product", type=bpy.types.Object)
|
||||
|
||||
@@ -118,7 +118,7 @@ class SheetBuilder:
|
||||
sheet_tree = ET.parse(sheet_path)
|
||||
sheet_root = sheet_tree.getroot()
|
||||
|
||||
for g in sheet_root.findall('{http://www.w3.org/2000/svg}g'):
|
||||
for g in sheet_root.findall("{http://www.w3.org/2000/svg}g"):
|
||||
if g.attrib.get("data-id") == str(reference.id()):
|
||||
sheet_root.remove(g)
|
||||
break
|
||||
|
||||
@@ -20,7 +20,9 @@ import os
|
||||
import re
|
||||
import bpy
|
||||
import math
|
||||
import bmesh
|
||||
import pystache
|
||||
import mathutils
|
||||
import xml.etree.ElementTree as ET
|
||||
import svgwrite
|
||||
import ifcopenshell
|
||||
@@ -34,14 +36,6 @@ from mathutils import geometry
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
|
||||
|
||||
def load_occ():
|
||||
# Don't import until we really need to, as a temporary step before we can purge OCC
|
||||
try:
|
||||
from OCC.Core import BRep, BRepTools, TopExp, TopAbs
|
||||
except ImportError:
|
||||
from OCC import BRep, BRepTools, TopExp, TopAbs
|
||||
|
||||
|
||||
class External(svgwrite.container.Group):
|
||||
def __init__(self, xml, **extra):
|
||||
self.xml = xml
|
||||
@@ -61,87 +55,78 @@ class External(svgwrite.container.Group):
|
||||
|
||||
class SvgWriter:
|
||||
def __init__(self):
|
||||
self.output = "out.svg"
|
||||
self.data_dir = None
|
||||
self.vector_style = None
|
||||
self.human_scale = "NTS"
|
||||
self.annotations = []
|
||||
self.metadata = []
|
||||
self.background_image = None
|
||||
self.scale = 1 / 100 # 1:100
|
||||
self.camera_width = None
|
||||
self.camera_height = None
|
||||
|
||||
def write(self, layer):
|
||||
def create_blank_svg(self, output_path):
|
||||
self.calculate_scale()
|
||||
self.svg = svgwrite.Drawing(
|
||||
self.output,
|
||||
debug=False,
|
||||
output_path,
|
||||
size=("{}mm".format(self.width), "{}mm".format(self.height)),
|
||||
viewBox=("0 0 {} {}".format(self.width, self.height)),
|
||||
id="root",
|
||||
data_scale=self.human_scale,
|
||||
debug=False, # Disable validation so that we can insert the IFC namespace
|
||||
)
|
||||
self.svg.attribs["xmlns:ifc"] = "http://www.ifcopenshell.org/ns"
|
||||
return self
|
||||
|
||||
if layer == "underlay":
|
||||
self.draw_background_image()
|
||||
elif layer == "annotation":
|
||||
self.add_stylesheet()
|
||||
self.add_markers()
|
||||
self.add_symbols()
|
||||
self.add_patterns()
|
||||
# self.draw_background_elements()
|
||||
# self.draw_cut_polygons()
|
||||
self.draw_annotations()
|
||||
def save(self):
|
||||
self.svg.save(pretty=True)
|
||||
|
||||
def calculate_scale(self):
|
||||
self.scale *= 1000 # IFC is in meters, SVG is in mm
|
||||
self.raw_width = self.camera_width
|
||||
self.raw_height = self.camera_height
|
||||
self.width = self.raw_width * self.scale
|
||||
self.height = self.raw_height * self.scale
|
||||
|
||||
def add_stylesheet(self):
|
||||
with open(os.path.join(self.data_dir, "styles", f"{self.vector_style}.css"), "r") as stylesheet:
|
||||
self.svg.defs.add(self.svg.style(stylesheet.read()))
|
||||
|
||||
def add_markers(self):
|
||||
tree = ET.parse(os.path.join(self.data_dir, "templates", "markers.svg"))
|
||||
root = tree.getroot()
|
||||
for child in root:
|
||||
self.svg.defs.add(External(child))
|
||||
|
||||
def add_symbols(self):
|
||||
tree = ET.parse(os.path.join(self.data_dir, "templates", "symbols.svg"))
|
||||
root = tree.getroot()
|
||||
for child in root:
|
||||
self.svg.defs.add(External(child))
|
||||
|
||||
def add_patterns(self):
|
||||
tree = ET.parse(os.path.join(self.data_dir, "templates", "patterns.svg"))
|
||||
root = tree.getroot()
|
||||
for child in root:
|
||||
self.svg.defs.add(External(child))
|
||||
|
||||
def draw_background_image(self):
|
||||
def draw_underlay(self, image):
|
||||
self.svg.add(
|
||||
self.svg.image(
|
||||
os.path.join("..", "diagrams", os.path.basename(self.background_image)),
|
||||
**{"width": self.width, "height": self.height},
|
||||
os.path.join("..", "diagrams", os.path.basename(image)),
|
||||
width=self.width,
|
||||
height=self.height,
|
||||
)
|
||||
)
|
||||
return self
|
||||
|
||||
def draw_background_elements(self):
|
||||
return # TODO purge?
|
||||
for element in self.ifc_cutter.background_elements:
|
||||
if element["type"] == "polygon":
|
||||
self.draw_polygon(element, "background")
|
||||
elif element["type"] == "polyline":
|
||||
self.draw_polyline(element, "background")
|
||||
elif element["type"] == "line":
|
||||
self.draw_line(element, "background")
|
||||
def define_boilerplate(self, stylesheet=None, markers=None, symbols=None, patterns=None):
|
||||
self.add_stylesheet(stylesheet)
|
||||
self.add_markers(markers)
|
||||
self.add_symbols(symbols)
|
||||
self.add_patterns(patterns)
|
||||
return self
|
||||
|
||||
def draw_annotations(self):
|
||||
for element in self.annotations:
|
||||
def calculate_scale(self):
|
||||
self.svg_scale = self.scale * 1000 # IFC is in meters, SVG is in mm
|
||||
self.raw_width = self.camera_width
|
||||
self.raw_height = self.camera_height
|
||||
self.width = self.raw_width * self.svg_scale
|
||||
self.height = self.raw_height * self.svg_scale
|
||||
|
||||
def add_stylesheet(self, uri):
|
||||
default_stylesheet = os.path.join(self.data_dir, "styles", f"default.css")
|
||||
with open(tool.Ifc.resolve_uri(uri) or default_stylesheet, "r") as stylesheet:
|
||||
self.svg.defs.add(self.svg.style(stylesheet.read()))
|
||||
|
||||
def add_markers(self, uri):
|
||||
tree = ET.parse(tool.Ifc.resolve_uri(uri) or os.path.join(self.data_dir, "templates", "markers.svg"))
|
||||
root = tree.getroot()
|
||||
for child in root:
|
||||
self.svg.defs.add(External(child))
|
||||
|
||||
def add_symbols(self, uri):
|
||||
tree = ET.parse(tool.Ifc.resolve_uri(uri) or os.path.join(self.data_dir, "templates", "symbols.svg"))
|
||||
root = tree.getroot()
|
||||
for child in root:
|
||||
self.svg.defs.add(External(child))
|
||||
|
||||
def add_patterns(self, uri):
|
||||
tree = ET.parse(tool.Ifc.resolve_uri(uri) or os.path.join(self.data_dir, "templates", "patterns.svg"))
|
||||
root = tree.getroot()
|
||||
for child in root:
|
||||
self.svg.defs.add(External(child))
|
||||
|
||||
def draw_annotations(self, annotations):
|
||||
for element in annotations:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if not obj or element.ObjectType == "DRAWING":
|
||||
continue
|
||||
@@ -153,6 +138,8 @@ class SvgWriter:
|
||||
self.draw_stair_annotation(obj)
|
||||
elif element.ObjectType == "DIMENSION":
|
||||
self.draw_dimension_annotations(obj)
|
||||
elif element.ObjectType == "ANGLE":
|
||||
self.draw_angle_annotations(obj)
|
||||
elif element.ObjectType == "RADIUS":
|
||||
self.draw_radius_annotations(obj)
|
||||
elif element.ObjectType == "DIAMETER":
|
||||
@@ -176,6 +163,7 @@ class SvgWriter:
|
||||
|
||||
# Experimental integration with the MeasureIt-ARCH Add-on
|
||||
self.draw_measureit_arch_dimension_annotations()
|
||||
return self
|
||||
|
||||
def draw_section_level_annotation(self, obj):
|
||||
x_offset = self.raw_width / 2
|
||||
@@ -190,7 +178,7 @@ class SvgWriter:
|
||||
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
|
||||
d = " ".join(
|
||||
[
|
||||
"L {} {}".format((x_offset + p.x) * self.scale, (y_offset - p.y) * self.scale)
|
||||
"L {} {}".format((x_offset + p.x) * self.svg_scale, (y_offset - p.y) * self.svg_scale)
|
||||
for p in projected_points
|
||||
]
|
||||
)
|
||||
@@ -198,8 +186,8 @@ class SvgWriter:
|
||||
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
|
||||
text_position = Vector(
|
||||
(
|
||||
(x_offset + projected_points[0].x) * self.scale,
|
||||
((y_offset - projected_points[0].y) * self.scale) - 3.5,
|
||||
(x_offset + projected_points[0].x) * self.svg_scale,
|
||||
((y_offset - projected_points[0].y) * self.svg_scale) - 3.5,
|
||||
)
|
||||
)
|
||||
# TODO: allow metric to be configurable
|
||||
@@ -229,14 +217,14 @@ class SvgWriter:
|
||||
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
|
||||
d = " ".join(
|
||||
[
|
||||
"L {} {}".format((x_offset + p.x) * self.scale, (y_offset - p.y) * self.scale)
|
||||
"L {} {}".format((x_offset + p.x) * self.svg_scale, (y_offset - p.y) * self.svg_scale)
|
||||
for p in projected_points
|
||||
]
|
||||
)
|
||||
d = "M{}".format(d[1:])
|
||||
start = Vector(((x_offset + projected_points[0].x), (y_offset - projected_points[0].y)))
|
||||
next_point = Vector(((x_offset + projected_points[1].x), (y_offset - projected_points[1].y)))
|
||||
text_position = (start * self.scale) - ((next_point - start).normalized() * 5)
|
||||
text_position = (start * self.svg_scale) - ((next_point - start).normalized() * 5)
|
||||
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
|
||||
self.svg.add(
|
||||
self.svg.text(
|
||||
@@ -266,14 +254,16 @@ class SvgWriter:
|
||||
end = Vector(((x_offset + v1.x), (y_offset - v1.y)))
|
||||
vector = end - start
|
||||
line = self.svg.add(
|
||||
self.svg.line(start=tuple(start * self.scale), end=tuple(end * self.scale), class_=" ".join(classes))
|
||||
self.svg.line(
|
||||
start=tuple(start * self.svg_scale), end=tuple(end * self.svg_scale), class_=" ".join(classes)
|
||||
)
|
||||
)
|
||||
line["stroke-dasharray"] = "12.5, 3, 3, 3"
|
||||
axis_tag = tool.Ifc.get_entity(obj).Name
|
||||
self.svg.add(
|
||||
self.svg.text(
|
||||
axis_tag,
|
||||
insert=tuple(start * self.scale),
|
||||
insert=tuple(start * self.svg_scale),
|
||||
**{
|
||||
"font-size": annotation.Annotator.get_svg_text_size(5.0),
|
||||
"font-family": "OpenGost Type B TT",
|
||||
@@ -286,7 +276,7 @@ class SvgWriter:
|
||||
self.svg.add(
|
||||
self.svg.text(
|
||||
axis_tag,
|
||||
insert=tuple(end * self.scale),
|
||||
insert=tuple(end * self.svg_scale),
|
||||
**{
|
||||
"font-size": annotation.Annotator.get_svg_text_size(5.0),
|
||||
"font-family": "OpenGost Type B TT",
|
||||
@@ -316,7 +306,7 @@ class SvgWriter:
|
||||
projected_points.append(projected_points[0])
|
||||
d = " ".join(
|
||||
[
|
||||
"L {} {}".format((x_offset + p.x) * self.scale, (y_offset - p.y) * self.scale)
|
||||
"L {} {}".format((x_offset + p.x) * self.svg_scale, (y_offset - p.y) * self.svg_scale)
|
||||
for p in projected_points
|
||||
]
|
||||
)
|
||||
@@ -353,7 +343,7 @@ class SvgWriter:
|
||||
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
|
||||
d = " ".join(
|
||||
[
|
||||
"L {} {}".format((x_offset + p.x) * self.scale, (y_offset - p.y) * self.scale)
|
||||
"L {} {}".format((x_offset + p.x) * self.svg_scale, (y_offset - p.y) * self.svg_scale)
|
||||
for p in projected_points
|
||||
]
|
||||
)
|
||||
@@ -375,7 +365,9 @@ class SvgWriter:
|
||||
end = Vector(((x_offset + v1.x), (y_offset - v1.y)))
|
||||
vector = end - start
|
||||
line = self.svg.add(
|
||||
self.svg.line(start=tuple(start * self.scale), end=tuple(end * self.scale), class_=" ".join(classes))
|
||||
self.svg.line(
|
||||
start=tuple(start * self.svg_scale), end=tuple(end * self.svg_scale), class_=" ".join(classes)
|
||||
)
|
||||
)
|
||||
|
||||
def draw_leader_annotation(self, obj):
|
||||
@@ -404,17 +396,17 @@ class SvgWriter:
|
||||
symbol_position = Vector(((x_offset + symbol_position.x), (y_offset - symbol_position.y)))
|
||||
transform = "rotate({}, {}, {})".format(
|
||||
angle,
|
||||
(symbol_position * self.scale)[0],
|
||||
(symbol_position * self.scale)[1],
|
||||
(symbol_position * self.svg_scale)[0],
|
||||
(symbol_position * self.svg_scale)[1],
|
||||
)
|
||||
|
||||
self.svg.add(
|
||||
self.svg.use("#section-arrow", insert=tuple(symbol_position * self.scale), transform=transform)
|
||||
self.svg.use("#section-arrow", insert=tuple(symbol_position * self.svg_scale), transform=transform)
|
||||
)
|
||||
self.svg.add(self.svg.use("#section-tag", insert=tuple(symbol_position * self.scale)))
|
||||
self.svg.add(self.svg.use("#section-tag", insert=tuple(symbol_position * self.svg_scale)))
|
||||
|
||||
reference_id, sheet_id = self.get_reference_and_sheet_id_from_annotation(tool.Ifc.get_entity(obj))
|
||||
text_position = list(symbol_position * self.scale)
|
||||
text_position = list(symbol_position * self.svg_scale)
|
||||
text_style = {
|
||||
"font-size": annotation.Annotator.get_svg_text_size(2.5),
|
||||
"font-family": "OpenGost Type B TT",
|
||||
@@ -437,15 +429,17 @@ class SvgWriter:
|
||||
|
||||
transform = "rotate({}, {}, {})".format(
|
||||
angle,
|
||||
(symbol_position * self.scale)[0],
|
||||
(symbol_position * self.scale)[1],
|
||||
(symbol_position * self.svg_scale)[0],
|
||||
(symbol_position * self.svg_scale)[1],
|
||||
)
|
||||
|
||||
self.svg.add(self.svg.use("#elevation-arrow", insert=tuple(symbol_position * self.scale), transform=transform))
|
||||
self.svg.add(self.svg.use("#elevation-tag", insert=tuple(symbol_position * self.scale)))
|
||||
self.svg.add(
|
||||
self.svg.use("#elevation-arrow", insert=tuple(symbol_position * self.svg_scale), transform=transform)
|
||||
)
|
||||
self.svg.add(self.svg.use("#elevation-tag", insert=tuple(symbol_position * self.svg_scale)))
|
||||
|
||||
reference_id, sheet_id = self.get_reference_and_sheet_id_from_annotation(tool.Ifc.get_entity(obj))
|
||||
text_position = list(symbol_position * self.scale)
|
||||
text_position = list(symbol_position * self.svg_scale)
|
||||
text_style = {
|
||||
"font-size": annotation.Annotator.get_svg_text_size(2.5),
|
||||
"font-family": "OpenGost Type B TT",
|
||||
@@ -493,13 +487,13 @@ class SvgWriter:
|
||||
|
||||
transform = "rotate({}, {}, {})".format(
|
||||
angle,
|
||||
(text_position * self.scale)[0],
|
||||
(text_position * self.scale)[1],
|
||||
(text_position * self.svg_scale)[0],
|
||||
(text_position * self.svg_scale)[1],
|
||||
)
|
||||
|
||||
symbol = tool.Drawing.get_annotation_symbol(element)
|
||||
if symbol:
|
||||
self.svg.add(self.svg.use(f"#{symbol}", insert=tuple(text_position * self.scale)))
|
||||
self.svg.add(self.svg.use(f"#{symbol}", insert=tuple(text_position * self.svg_scale)))
|
||||
|
||||
if text_literal.BoxAlignment == "top-left":
|
||||
alignment_baseline = "hanging"
|
||||
@@ -531,17 +525,18 @@ class SvgWriter:
|
||||
|
||||
literal = text_literal.Literal
|
||||
|
||||
product = tool.Drawing.get_text_product(element)
|
||||
product = tool.Drawing.get_assigned_product(element)
|
||||
selector = ifcopenshell.util.selector.Selector
|
||||
variables = {}
|
||||
for variable in re.findall("{{.*?}}", literal):
|
||||
literal = literal.replace(variable, selector.get_element_value(product, variable[2:-2]) or "")
|
||||
if product != None:
|
||||
for variable in re.findall("{{.*?}}", literal):
|
||||
literal = literal.replace(variable, str(selector.get_element_value(product, variable[2:-2]) or ""))
|
||||
|
||||
for line_number, text_line in enumerate(literal.replace("\\n", "\n").split("\n")):
|
||||
self.svg.add(
|
||||
self.svg.text(
|
||||
text_line,
|
||||
insert=tuple((text_position * self.scale) + Vector((0, 3.5 * line_number))),
|
||||
insert=tuple((text_position * self.svg_scale) + Vector((0, 3.5 * line_number))),
|
||||
class_=" ".join(self.get_attribute_classes(text_obj)),
|
||||
**{
|
||||
"font-size": annotation.Annotator.get_svg_text_size(text_obj.BIMTextProperties.font_size),
|
||||
@@ -570,7 +565,7 @@ class SvgWriter:
|
||||
]
|
||||
d = " ".join(
|
||||
[
|
||||
"L {} {}".format((x_offset + p.x) * self.scale, (y_offset - p.y) * self.scale)
|
||||
"L {} {}".format((x_offset + p.x) * self.svg_scale, (y_offset - p.y) * self.svg_scale)
|
||||
for p in projected_points
|
||||
]
|
||||
)
|
||||
@@ -587,7 +582,7 @@ class SvgWriter:
|
||||
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
|
||||
d = " ".join(
|
||||
[
|
||||
"L {} {}".format((x_offset + p.x) * self.scale, (y_offset - p.y) * self.scale)
|
||||
"L {} {}".format((x_offset + p.x) * self.svg_scale, (y_offset - p.y) * self.svg_scale)
|
||||
for p in projected_points
|
||||
]
|
||||
)
|
||||
@@ -595,8 +590,8 @@ class SvgWriter:
|
||||
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
|
||||
text_position = Vector(
|
||||
(
|
||||
(x_offset + projected_points[0].x) * self.scale,
|
||||
((y_offset - projected_points[0].y) * self.scale) - 2.5,
|
||||
(x_offset + projected_points[0].x) * self.svg_scale,
|
||||
((y_offset - projected_points[0].y) * self.svg_scale) - 2.5,
|
||||
)
|
||||
)
|
||||
# TODO: allow metric to be configurable
|
||||
@@ -623,6 +618,119 @@ class SvgWriter:
|
||||
)
|
||||
)
|
||||
|
||||
def draw_angle_annotations(self, obj):
|
||||
# This implementation uses an SVG arc, which means that it can only draw
|
||||
# arcs that are orthogonal to the view (e.g. not arcs in 3D).
|
||||
# Gosh this is bad code :(
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
classes = self.get_attribute_classes(obj)
|
||||
matrix_world = obj.matrix_world
|
||||
|
||||
points = [v.co for v in obj.data.vertices][:3]
|
||||
center = tool.Cad.get_center_of_arc(points, obj)
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
bm.verts.ensure_lookup_table()
|
||||
arc_end_verts = [v for v in bm.verts if len(v.link_edges) == 1]
|
||||
arc_end_pts = [matrix_world @ v.co for v in arc_end_verts]
|
||||
|
||||
# Probably need this when rewriting to use an SVG polyline instead of an arc
|
||||
# arc_path = [arc_end_verts[0]]
|
||||
# while True:
|
||||
# last_point = arc_end_verts[0]
|
||||
# found_another_point = False
|
||||
# for edge in last_point.link_edges:
|
||||
# v = edge.other_vert(last_point)
|
||||
# if v not in arc_path:
|
||||
# found_another_point = True
|
||||
# arc_path.append(v)
|
||||
# if not found_another_point:
|
||||
# break
|
||||
|
||||
distance_between_end_verts = (arc_end_verts[0].co - arc_end_verts[1].co).length
|
||||
arc_mid_vert = arc_end_verts[0].link_edges[0].other_vert(arc_end_verts[0])
|
||||
is_reflex = 0 if (arc_mid_vert.co - arc_end_verts[1].co).length < distance_between_end_verts else 1
|
||||
|
||||
bm.free()
|
||||
|
||||
# Calculate the angle
|
||||
# This is the true normal in 3D, whereas the camera projection we use is the drawing direction.
|
||||
# This assumes (because we use SVG arcs) that the radius is always orthogonal to our view.
|
||||
# When rewriting to use polylines, we should use this normal instead.
|
||||
# normal = mathutils.geometry.normal([arc_end_pts[0], arc_end_pts[1], center])
|
||||
normal = Vector(self.camera_projection)
|
||||
dir1 = (arc_end_pts[0] - center).normalized()
|
||||
dir2 = (arc_end_pts[1] - center).normalized()
|
||||
|
||||
# Let's get the matrix that represents the coordinate system of the arc.
|
||||
# This matrix allows us to get 2D vectors for calculating the signed arc angle.
|
||||
z = normal
|
||||
x = (arc_end_pts[0] - center).normalized()
|
||||
y = z.cross(x)
|
||||
arc_matrix = mathutils.Matrix([x, y, z]).transposed().to_4x4()
|
||||
|
||||
dir1 = ((arc_matrix.inverted() @ arc_end_pts[0]) - (arc_matrix.inverted() @ center)).normalized()
|
||||
dir2 = ((arc_matrix.inverted() @ arc_end_pts[1]) - (arc_matrix.inverted() @ center)).normalized()
|
||||
angle = -dir1.xy.angle_signed(dir2.xy)
|
||||
|
||||
# if is_reflex:
|
||||
# angle = angle % (math.pi * 2)
|
||||
|
||||
# Center of gravity of all vertices, used to help position the text
|
||||
cog = Vector((0, 0, 0))
|
||||
for vert in obj.data.vertices:
|
||||
cog += vert.co
|
||||
cog = matrix_world @ (cog / len(obj.data.vertices))
|
||||
|
||||
radius = ((matrix_world @ obj.data.vertices[0].co) - center).length
|
||||
|
||||
arc_midpoint = center + ((cog - center).normalized() * radius)
|
||||
|
||||
text_position = self.project_point_onto_camera(arc_midpoint)
|
||||
text_position = Vector(
|
||||
((x_offset + text_position.x) * self.svg_scale, (y_offset - text_position.y) * self.svg_scale)
|
||||
)
|
||||
|
||||
center_projected = self.project_point_onto_camera(center)
|
||||
center_position = Vector(
|
||||
((x_offset + center_projected.x) * self.svg_scale, (y_offset - center_projected.y) * self.svg_scale)
|
||||
)
|
||||
text_offset = (text_position - center_position).xy.normalized() * 5
|
||||
text_position += text_offset
|
||||
|
||||
text_style = {
|
||||
"font-size": annotation.Annotator.get_svg_text_size(2.5),
|
||||
"font-family": "OpenGost Type B TT",
|
||||
"text-anchor": "middle",
|
||||
"alignment-baseline": "middle",
|
||||
"dominant-baseline": "middle",
|
||||
}
|
||||
|
||||
angle_text = abs(round(math.degrees(angle), 3))
|
||||
if is_reflex:
|
||||
angle_text = 360 - angle_text
|
||||
self.svg.add(self.svg.text(f"{angle_text}deg", insert=tuple(text_position), **text_style))
|
||||
|
||||
# Draw SVG arc, see for details: http://xahlee.info/js/svg_circle_arc.html
|
||||
arc_proj_end_pts = [self.project_point_onto_camera(v) for v in arc_end_pts]
|
||||
p1 = Vector(
|
||||
((x_offset + arc_proj_end_pts[0].x) * self.svg_scale, (y_offset - arc_proj_end_pts[0].y) * self.svg_scale)
|
||||
)
|
||||
p2 = Vector(
|
||||
((x_offset + arc_proj_end_pts[1].x) * self.svg_scale, (y_offset - arc_proj_end_pts[1].y) * self.svg_scale)
|
||||
)
|
||||
r = radius * self.svg_scale
|
||||
# reflex = 1 if angle > math.pi else 0
|
||||
reflex = is_reflex
|
||||
if reflex:
|
||||
sense = 0 if angle > 0 else 1
|
||||
else:
|
||||
sense = 1 if angle > 0 else 0
|
||||
d = f"M {p1.x} {p1.y} A {r} {r} 0 {reflex} {sense} {p2.x} {p2.y}"
|
||||
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
|
||||
|
||||
def draw_radius_annotations(self, obj):
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
@@ -634,7 +742,7 @@ class SvgWriter:
|
||||
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
|
||||
d = " ".join(
|
||||
[
|
||||
"L {} {}".format((x_offset + p.x) * self.scale, (y_offset - p.y) * self.scale)
|
||||
"L {} {}".format((x_offset + p.x) * self.svg_scale, (y_offset - p.y) * self.svg_scale)
|
||||
for p in projected_points
|
||||
]
|
||||
)
|
||||
@@ -642,15 +750,21 @@ class SvgWriter:
|
||||
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
|
||||
|
||||
p0 = Vector(
|
||||
((x_offset + projected_points[0].x) * self.scale, (y_offset - projected_points[0].y) * self.scale)
|
||||
(
|
||||
(x_offset + projected_points[0].x) * self.svg_scale,
|
||||
(y_offset - projected_points[0].y) * self.svg_scale,
|
||||
)
|
||||
)
|
||||
p1 = Vector(
|
||||
((x_offset + projected_points[1].x) * self.scale, (y_offset - projected_points[1].y) * self.scale)
|
||||
(
|
||||
(x_offset + projected_points[1].x) * self.svg_scale,
|
||||
(y_offset - projected_points[1].y) * self.svg_scale,
|
||||
)
|
||||
)
|
||||
text_offset = (p0 - p1).xy.normalized() * 5
|
||||
text_position = projected_points[0]
|
||||
text_position = Vector(
|
||||
((x_offset + text_position.x) * self.scale, (y_offset - text_position.y) * self.scale)
|
||||
((x_offset + text_position.x) * self.svg_scale, (y_offset - text_position.y) * self.svg_scale)
|
||||
)
|
||||
text_position += text_offset
|
||||
|
||||
@@ -722,15 +836,17 @@ class SvgWriter:
|
||||
perpendicular = Vector((vector.y, -vector.x)).normalized()
|
||||
dimension = (v1_global - v0_global).length
|
||||
dimension = helper.format_distance(dimension)
|
||||
sheet_dimension = ((end * self.scale) - (start * self.scale)).length
|
||||
sheet_dimension = ((end * self.svg_scale) - (start * self.svg_scale)).length
|
||||
if sheet_dimension < 5: # annotation can't fit
|
||||
# offset text to right of marker
|
||||
text_position = (end * self.scale) + perpendicular + (3 * vector.normalized())
|
||||
text_position = (end * self.svg_scale) + perpendicular + (3 * vector.normalized())
|
||||
else:
|
||||
text_position = (mid * self.scale) + perpendicular
|
||||
text_position = (mid * self.svg_scale) + perpendicular
|
||||
rotation = math.degrees(vector.angle_signed(Vector((1, 0))))
|
||||
line = self.svg.add(
|
||||
self.svg.line(start=tuple(start * self.scale), end=tuple(end * self.scale), class_=" ".join(classes))
|
||||
self.svg.line(
|
||||
start=tuple(start * self.svg_scale), end=tuple(end * self.svg_scale), class_=" ".join(classes)
|
||||
)
|
||||
)
|
||||
if text_override is not None:
|
||||
text = text_override
|
||||
@@ -762,35 +878,8 @@ class SvgWriter:
|
||||
def get_spline_points(self, spline):
|
||||
return spline.bezier_points if spline.bezier_points else spline.points
|
||||
|
||||
def draw_cut_polygons(self):
|
||||
return # deprecate?
|
||||
for polygon in self.ifc_cutter.cut_polygons:
|
||||
self.draw_polygon(polygon, "cut")
|
||||
|
||||
def draw_polyline(self, element, position):
|
||||
load_occ()
|
||||
classes = self.get_classes(element["raw"], position)
|
||||
exp = BRepTools.BRepTools_WireExplorer(element["geometry"])
|
||||
points = []
|
||||
while exp.More():
|
||||
point = BRep.BRep_Tool.Pnt(exp.CurrentVertex())
|
||||
points.append((point.X() * self.scale, -point.Y() * self.scale))
|
||||
exp.Next()
|
||||
self.svg.add(self.svg.polyline(points=points, class_=" ".join(classes)))
|
||||
|
||||
def draw_line(self, element, position):
|
||||
load_occ()
|
||||
classes = self.get_classes(element["raw"], position)
|
||||
exp = TopExp.TopExp_Explorer(element["geometry"], TopAbs.TopAbs_VERTEX)
|
||||
points = []
|
||||
while exp.More():
|
||||
point = BRep.BRep_Tool.Pnt(topods.Vertex(exp.Current()))
|
||||
points.append((point.X() * self.scale, -point.Y() * self.scale))
|
||||
exp.Next()
|
||||
self.svg.add(self.svg.line(start=points[0], end=points[1], class_=" ".join(classes)))
|
||||
|
||||
def draw_polygon(self, polygon, position):
|
||||
points = [(p[0] * self.scale, p[1] * self.scale) for p in polygon["points"]]
|
||||
points = [(p[0] * self.svg_scale, p[1] * self.svg_scale) for p in polygon["points"]]
|
||||
if "classes" in polygon["metadata"]:
|
||||
classes = " ".join(polygon["metadata"]["classes"])
|
||||
else:
|
||||
|
||||
@@ -20,7 +20,7 @@ import bpy
|
||||
import blenderbim.bim.helper
|
||||
import blenderbim.tool as tool
|
||||
from bpy.types import Panel
|
||||
from blenderbim.bim.module.drawing.data import TextData, SheetsData, SchedulesData, DrawingsData
|
||||
from blenderbim.bim.module.drawing.data import ProductAssignmentsData, TextData, SheetsData, SchedulesData, DrawingsData
|
||||
|
||||
|
||||
class BIM_PT_camera(Panel):
|
||||
@@ -170,13 +170,16 @@ class BIM_PT_drawings(Panel):
|
||||
if self.props.active_drawing_index < len(self.props.drawings):
|
||||
active_drawing = self.props.drawings[self.props.active_drawing_index]
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
col = row.column()
|
||||
col.alignment = 'LEFT'
|
||||
col.operator("bim.remove_drawing", icon="X", text="").drawing = active_drawing.ifc_definition_id
|
||||
col = row.column()
|
||||
col.alignment = 'RIGHT'
|
||||
op = row.operator("bim.open_view", icon="URL", text="")
|
||||
op.view = active_drawing.name
|
||||
op = row.operator("bim.activate_view", icon="OUTLINER_OB_CAMERA", text="")
|
||||
op.drawing = active_drawing.ifc_definition_id
|
||||
row.operator("bim.create_drawing", text="", icon="OUTPUT")
|
||||
row.operator("bim.remove_drawing", icon="X", text="").drawing = active_drawing.ifc_definition_id
|
||||
self.layout.template_list(
|
||||
"BIM_UL_drawinglist", "", self.props, "drawings", self.props, "active_drawing_index"
|
||||
)
|
||||
@@ -225,10 +228,14 @@ class BIM_PT_schedules(Panel):
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
row.operator("bim.open_schedule", icon="URL", text="").schedule = active_schedule.ifc_definition_id
|
||||
row.operator("bim.build_schedule", icon="LINENUMBERS_ON", text="").schedule = active_schedule.ifc_definition_id
|
||||
row.operator(
|
||||
"bim.build_schedule", icon="LINENUMBERS_ON", text=""
|
||||
).schedule = active_schedule.ifc_definition_id
|
||||
row.operator("bim.remove_schedule", icon="X", text="").schedule = active_schedule.ifc_definition_id
|
||||
|
||||
self.layout.template_list("BIM_UL_generic", "", self.props, "schedules", self.props, "active_schedule_index")
|
||||
self.layout.template_list(
|
||||
"BIM_UL_generic", "", self.props, "schedules", self.props, "active_schedule_index"
|
||||
)
|
||||
|
||||
|
||||
class BIM_PT_sheets(Panel):
|
||||
@@ -276,6 +283,39 @@ class BIM_PT_sheets(Panel):
|
||||
self.layout.template_list("BIM_UL_sheets", "", self.props, "sheets", self.props, "active_sheet_index")
|
||||
|
||||
|
||||
class BIM_PT_product_assignments(Panel):
|
||||
bl_label = "IFC Product Assignments"
|
||||
bl_idname = "BIM_PT_product_assignments"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "object"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not tool.Ifc.get() or not context.active_object:
|
||||
return
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
if not element:
|
||||
return
|
||||
return element.is_a("IfcAnnotation")
|
||||
|
||||
def draw(self, context):
|
||||
if not ProductAssignmentsData.is_loaded:
|
||||
ProductAssignmentsData.load()
|
||||
|
||||
props = context.active_object.BIMAssignedProductProperties
|
||||
|
||||
if props.is_editing_product:
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "relating_product", text="")
|
||||
row.operator("bim.edit_assigned_product", icon="CHECKMARK", text="")
|
||||
row.operator("bim.disable_editing_assigned_product", icon="CANCEL", text="")
|
||||
else:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=ProductAssignmentsData.data["relating_product"] or "No Relating Product", icon="OBJECT_DATA")
|
||||
row.operator("bim.enable_editing_assigned_product", icon="GREASEPENCIL", text="")
|
||||
|
||||
|
||||
class BIM_PT_text(Panel):
|
||||
bl_label = "IFC Text"
|
||||
bl_idname = "BIM_PT_text"
|
||||
@@ -312,16 +352,6 @@ class BIM_PT_text(Panel):
|
||||
row.label(text=attribute["name"])
|
||||
row.label(text=attribute["value"])
|
||||
|
||||
if props.is_editing_product:
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "relating_product", text="")
|
||||
row.operator("bim.edit_text_product", icon="CHECKMARK", text="")
|
||||
row.operator("bim.disable_editing_text_product", icon="CANCEL", text="")
|
||||
else:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=TextData.data["relating_product"] or "No Relating Product", icon="OBJECT_DATA")
|
||||
row.operator("bim.enable_editing_text_product", icon="GREASEPENCIL", text="")
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(props, "font_size")
|
||||
row = self.layout.row()
|
||||
|
||||
@@ -25,17 +25,20 @@ classes = (
|
||||
operator.EditObjectPlacement,
|
||||
operator.GetRepresentationIfcParameters,
|
||||
operator.OverrideDelete,
|
||||
operator.OverrideOutlinerDelete,
|
||||
operator.OverrideDuplicateMove,
|
||||
operator.OverrideDuplicateMoveLinked,
|
||||
operator.OverrideOutlinerDelete,
|
||||
operator.OverridePasteBuffer,
|
||||
operator.RemoveConnection,
|
||||
operator.RemoveRepresentation,
|
||||
operator.SelectConnection,
|
||||
operator.SwitchRepresentation,
|
||||
operator.UpdateParametricRepresentation,
|
||||
operator.UpdateRepresentation,
|
||||
prop.BIMGeometryProperties,
|
||||
ui.BIM_PT_derived_placements,
|
||||
ui.BIM_PT_representations,
|
||||
ui.BIM_PT_connections,
|
||||
ui.BIM_PT_mesh,
|
||||
ui.BIM_PT_workarounds,
|
||||
)
|
||||
|
||||
@@ -25,6 +25,7 @@ from mathutils import Vector
|
||||
def refresh():
|
||||
DerivedPlacementsData.is_loaded = False
|
||||
RepresentationsData.is_loaded = False
|
||||
ConnectionsData.is_loaded = False
|
||||
|
||||
|
||||
class RepresentationsData:
|
||||
@@ -64,6 +65,40 @@ class RepresentationsData:
|
||||
return results
|
||||
|
||||
|
||||
class ConnectionsData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {"connections": cls.connections()}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def connections(cls):
|
||||
results = []
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
for rel in getattr(element, "ConnectedTo", []):
|
||||
results.append(
|
||||
{
|
||||
"id": rel.id(),
|
||||
"is_relating": True,
|
||||
"Name": rel.RelatedElement.Name or "Unnamed",
|
||||
"ConnectionType": rel.RelatingConnectionType,
|
||||
}
|
||||
)
|
||||
for rel in getattr(element, "ConnectedFrom", []):
|
||||
results.append(
|
||||
{
|
||||
"id": rel.id(),
|
||||
"is_relating": False,
|
||||
"Name": rel.RelatingElement.Name or "Unnamed",
|
||||
"ConnectionType": rel.RelatedConnectionType,
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
class DerivedPlacementsData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@@ -21,7 +21,7 @@ import bmesh
|
||||
import mathutils
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.unit
|
||||
from math import pi
|
||||
from math import pi, pow
|
||||
from mathutils import Vector, Matrix, geometry
|
||||
|
||||
|
||||
@@ -112,6 +112,167 @@ class Helper:
|
||||
|
||||
return {"profile": profile, "extrusion": extrusion}
|
||||
|
||||
def auto_detect_arbitrary_profile_with_voids(self, obj, mesh):
|
||||
groups = {"IFCARCINDEX": [], "IFCCIRCLE": []}
|
||||
for i, group in enumerate(obj.vertex_groups):
|
||||
if "IFCARCINDEX" in group.name:
|
||||
groups["IFCARCINDEX"].append(i)
|
||||
elif "IFCCIRCLE" in group.name:
|
||||
groups["IFCCIRCLE"].append(i)
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
|
||||
bmesh.ops.delete(bm, geom=bm.faces, context="FACES_ONLY")
|
||||
|
||||
# https://docs.blender.org/api/blender_python_api_2_63_8/bmesh.html#CustomDataAccess
|
||||
# This is how we access vertex groups via bmesh, apparently, it's not very intuitive
|
||||
deform_layer = bm.verts.layers.deform.active
|
||||
|
||||
# Sanity check
|
||||
group_verts = {"IFCARCINDEX": {}, "IFCCIRCLE": {}}
|
||||
for vert in bm.verts:
|
||||
total_groups = 0
|
||||
is_circle = False
|
||||
for group_type, group_indices in groups.items():
|
||||
for group_index in group_indices:
|
||||
if group_index in vert[deform_layer]:
|
||||
if group_type == "IFCCIRCLE":
|
||||
is_circle = True
|
||||
group_verts[group_type].setdefault(group_index, 0)
|
||||
group_verts[group_type][group_index] += 1
|
||||
total_groups += 0
|
||||
if total_groups > 1: # A vert can only belong to one group
|
||||
return (False, "AMBIGUOUS_SPECIAL_VERTEX")
|
||||
elif is_circle:
|
||||
pass # Circles are allowed to be unclosed
|
||||
elif total_groups == 0 and len(vert.link_edges) != 2: # Unclosed loop or forked loop
|
||||
return (False, "UNCLOSED_LOOP")
|
||||
|
||||
for group_type, group_counts in group_verts.items():
|
||||
if group_type == "IFCARCINDEX":
|
||||
for group_count in group_counts.values():
|
||||
if group_count != 3: # Each arc needs 3 verts
|
||||
return (False, "3POINT_ARC")
|
||||
elif group_type == "IFCCIRCLE":
|
||||
for group_count in group_counts.values():
|
||||
if group_count != 2: # Each circle needs 2 verts
|
||||
return (False, "CIRCLE")
|
||||
|
||||
loop_edges = set(bm.edges)
|
||||
|
||||
# Create loops from edges
|
||||
loops = []
|
||||
while loop_edges:
|
||||
edge = loop_edges.pop()
|
||||
loop = [edge]
|
||||
has_found_connected_edge = True
|
||||
while has_found_connected_edge:
|
||||
has_found_connected_edge = False
|
||||
for edge in loop_edges.copy():
|
||||
edge_verts = set(edge.verts)
|
||||
if edge_verts & set(loop[0].verts):
|
||||
loop.insert(0, edge)
|
||||
loop_edges.remove(edge)
|
||||
has_found_connected_edge = True
|
||||
elif edge_verts & set(loop[-1].verts):
|
||||
loop.append(edge)
|
||||
loop_edges.remove(edge)
|
||||
has_found_connected_edge = True
|
||||
loops.append(loop)
|
||||
|
||||
# Determine outer loop
|
||||
max_area = 0
|
||||
outer_loop = None
|
||||
inner_loops = []
|
||||
|
||||
for loop in loops:
|
||||
loop_vertices = []
|
||||
total_edges = len(loop)
|
||||
|
||||
if total_edges == 1:
|
||||
loop_vertices = [loop[0].verts[0], loop[0].verts[1]]
|
||||
else:
|
||||
for i, edge in enumerate(loop):
|
||||
if i + 1 == total_edges and edge.verts[0] in loop[i - 1].verts:
|
||||
loop_vertices.append(edge.verts[0])
|
||||
elif i + 1 == total_edges and edge.verts[1] in loop[i - 1].verts:
|
||||
loop_vertices.append(edge.verts[1])
|
||||
elif edge.verts[0] in loop[i + 1].verts:
|
||||
loop_vertices.append(edge.verts[1])
|
||||
elif edge.verts[1] in loop[i + 1].verts:
|
||||
loop_vertices.append(edge.verts[0])
|
||||
|
||||
if len(loop_vertices) == 2:
|
||||
# Unofficial convention right now that a loop of 2 verts always is a circle
|
||||
radius = (loop_vertices[0].co - loop_vertices[1].co).length / 2
|
||||
face_area = pi * pow(radius, 2)
|
||||
else:
|
||||
loop_bm = bmesh.new()
|
||||
for vert in loop_vertices:
|
||||
loop_bm.verts.new(vert.co)
|
||||
face = loop_bm.faces.new(loop_bm.verts)
|
||||
face_area = face.calc_area()
|
||||
loop_bm.free()
|
||||
|
||||
loop_vertex_indices = []
|
||||
active_arc_id = None
|
||||
arc_stack = []
|
||||
last_index = len(loop_vertices) - 1
|
||||
|
||||
# It is possible to loop through loop_vertices halfway through an arc.
|
||||
# If that is the case, we store it in incomplete_arc
|
||||
incomplete_arc = []
|
||||
for i, v in enumerate(loop_vertices):
|
||||
if len(arc_stack) == 3:
|
||||
loop_vertex_indices.append(arc_stack)
|
||||
loop_vertex_indices.append((arc_stack[-1], v.index))
|
||||
arc_stack = []
|
||||
active_arc_id = None
|
||||
|
||||
is_arc = False
|
||||
for group_index in groups["IFCARCINDEX"]:
|
||||
if group_index in v[deform_layer]:
|
||||
if active_arc_id is not None and active_arc_id != group_index:
|
||||
incomplete_arc = arc_stack
|
||||
arc_stack = []
|
||||
active_arc_id = group_index
|
||||
is_arc = True
|
||||
break
|
||||
|
||||
if i == last_index and not is_arc:
|
||||
continue
|
||||
|
||||
if is_arc:
|
||||
arc_stack.append(v.index)
|
||||
else:
|
||||
if active_arc_id is not None:
|
||||
incomplete_arc = arc_stack
|
||||
active_arc_id = None
|
||||
arc_stack = []
|
||||
loop_vertex_indices.append((v.index, loop_vertices[i + 1].index))
|
||||
|
||||
if active_arc_id is not None:
|
||||
arc_stack.extend(incomplete_arc)
|
||||
loop_vertex_indices.append(arc_stack)
|
||||
|
||||
loop_vertex_indices.append((loop_vertex_indices[-1][-1], loop_vertex_indices[0][0]))
|
||||
|
||||
if face_area > max_area:
|
||||
max_area = face_area
|
||||
outer_loop = loop_vertex_indices
|
||||
inner_loops.append(loop_vertex_indices)
|
||||
|
||||
inner_loops.remove(outer_loop)
|
||||
|
||||
points = [v.co for v in bm.verts]
|
||||
|
||||
bm.to_mesh(mesh)
|
||||
mesh.update()
|
||||
bm.free()
|
||||
|
||||
return {"points": points, "profile": outer_loop, "inner_curves": inner_loops}
|
||||
|
||||
# An arbitrary closed profile with voids is similar to one without voids.
|
||||
# We start the same way with any ngon (no tri), but instead of being the entire
|
||||
# profile, it is only one of the possible faces that make up the end of our
|
||||
|
||||
@@ -86,6 +86,26 @@ class AddRepresentation(bpy.types.Operator, Operator):
|
||||
)
|
||||
|
||||
|
||||
class SelectConnection(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.select_connection"
|
||||
bl_label = "Select Connection"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
connection: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
core.select_connection(tool.Geometry, connection=tool.Ifc.get().by_id(self.connection))
|
||||
|
||||
|
||||
class RemoveConnection(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.remove_connection"
|
||||
bl_label = "Remove Connection"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
connection: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
core.remove_connection(tool.Geometry, connection=tool.Ifc.get().by_id(self.connection))
|
||||
|
||||
|
||||
class SwitchRepresentation(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.switch_representation"
|
||||
bl_label = "Switch Representation"
|
||||
@@ -97,15 +117,29 @@ class SwitchRepresentation(bpy.types.Operator, Operator):
|
||||
should_switch_all_meshes: bpy.props.BoolProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
core.switch_representation(
|
||||
tool.Geometry,
|
||||
obj=bpy.data.objects.get(self.obj) if self.obj else context.active_object,
|
||||
representation=tool.Ifc.get().by_id(self.ifc_definition_id),
|
||||
should_reload=self.should_reload,
|
||||
enable_dynamic_voids=self.disable_opening_subtractions,
|
||||
is_global=self.should_switch_all_meshes,
|
||||
should_sync_changes_first=True,
|
||||
)
|
||||
target = tool.Ifc.get().by_id(self.ifc_definition_id).ContextOfItems
|
||||
is_subcontext = target.is_a("IfcGeometricRepresentationSubContext")
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
if is_subcontext:
|
||||
representation = ifcopenshell.util.representation.get_representation(
|
||||
element, target.ContextType, target.ContextIdentifier, target.TargetView
|
||||
)
|
||||
else:
|
||||
representation = ifcopenshell.util.representation.get_representation(element, target.ContextType)
|
||||
if not representation:
|
||||
continue
|
||||
core.switch_representation(
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=self.should_reload,
|
||||
enable_dynamic_voids=self.disable_opening_subtractions,
|
||||
is_global=self.should_switch_all_meshes,
|
||||
should_sync_changes_first=True,
|
||||
)
|
||||
|
||||
|
||||
class RemoveRepresentation(bpy.types.Operator, Operator):
|
||||
@@ -130,10 +164,6 @@ class UpdateRepresentation(bpy.types.Operator):
|
||||
obj: bpy.props.StringProperty()
|
||||
ifc_representation_class: bpy.props.StringProperty()
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.active_object.mode == "OBJECT"
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
@@ -141,6 +171,10 @@ class UpdateRepresentation(bpy.types.Operator):
|
||||
if not ContextData.is_loaded:
|
||||
ContextData.load(IfcStore.get_file())
|
||||
|
||||
if context.active_object and context.active_object.mode != "OBJECT":
|
||||
# Ensure mode is object to prevent invalid mesh data causing CTD
|
||||
bpy.ops.object.mode_set(mode="OBJECT", toggle=False)
|
||||
|
||||
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
|
||||
self.file = IfcStore.get_file()
|
||||
|
||||
@@ -156,18 +190,23 @@ class UpdateRepresentation(bpy.types.Operator):
|
||||
|
||||
def update_obj_mesh_representation(self, context, obj):
|
||||
product = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
material = ifcopenshell.util.element.get_material(product, should_skip_usage=True)
|
||||
|
||||
if not product.is_a("IfcGridAxis"):
|
||||
tool.Geometry.clear_cache(product)
|
||||
|
||||
if product.is_a("IfcGridAxis"):
|
||||
# Grid geometry does not follow the "representation" paradigm and needs to be treated specially
|
||||
ifcopenshell.api.run("grid.create_axis_curve", self.file, **{"axis_curve": obj, "grid_axis": product})
|
||||
return
|
||||
if product.is_a("IfcRelSpaceBoundary"):
|
||||
elif product.is_a("IfcRelSpaceBoundary"):
|
||||
# TODO refactor
|
||||
settings = tool.Boundary.get_assign_connection_geometry_settings(obj)
|
||||
ifcopenshell.api.run("boundary.assign_connection_geometry", tool.Ifc.get(), **settings)
|
||||
return
|
||||
elif material and material.is_a() in ["IfcMaterialProfileSet", "IfcMaterialLayerSet"]:
|
||||
# These objects are parametrically based on an axis and should not be modified as a mesh
|
||||
return
|
||||
|
||||
core.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
|
||||
@@ -207,7 +246,10 @@ class UpdateRepresentation(bpy.types.Operator):
|
||||
new_representation = ifcopenshell.api.run("geometry.add_representation", self.file, **representation_data)
|
||||
|
||||
if tool.Geometry.is_body_representation(new_representation):
|
||||
[tool.Geometry.run_style_add_style(obj=mat) for mat in tool.Geometry.get_object_materials_without_styles(obj)]
|
||||
[
|
||||
tool.Geometry.run_style_add_style(obj=mat)
|
||||
for mat in tool.Geometry.get_object_materials_without_styles(obj)
|
||||
]
|
||||
ifcopenshell.api.run(
|
||||
"style.assign_representation_styles",
|
||||
self.file,
|
||||
@@ -237,7 +279,7 @@ class UpdateParametricRepresentation(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.active_object.mode == "OBJECT"
|
||||
return context.active_object and context.active_object.mode == "OBJECT"
|
||||
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
@@ -331,9 +373,12 @@ class OverrideDeleteTrait:
|
||||
self.remove_filling(rel.RelatedBuildingElement)
|
||||
if element.VoidsElements:
|
||||
self.delete_opening_element(element)
|
||||
elif getattr(element, "HasOpenings", None):
|
||||
for rel in element.HasOpenings:
|
||||
self.delete_opening_element(rel.RelatedOpeningElement)
|
||||
else:
|
||||
if getattr(element, "HasOpenings", None):
|
||||
for rel in element.HasOpenings:
|
||||
self.delete_opening_element(rel.RelatedOpeningElement)
|
||||
for port in ifcopenshell.util.system.get_ports(element):
|
||||
self.remove_port(port)
|
||||
|
||||
def delete_opening_element(self, element):
|
||||
obj = IfcStore.get_element(element.VoidsElements[0].RelatingBuildingElement.id())
|
||||
@@ -343,6 +388,9 @@ class OverrideDeleteTrait:
|
||||
obj = IfcStore.get_element(element.id())
|
||||
bpy.ops.bim.remove_filling(obj=obj.name)
|
||||
|
||||
def remove_port(self, port):
|
||||
blenderbim.core.system.remove_port(tool.Ifc, tool.System, port=port)
|
||||
|
||||
|
||||
class OverrideDelete(bpy.types.Operator, OverrideDeleteTrait):
|
||||
bl_idname = "object.delete"
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
import bpy
|
||||
from bpy.types import Panel
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.geometry.data import RepresentationsData, DerivedPlacementsData
|
||||
from blenderbim.bim.helper import prop_with_search
|
||||
from blenderbim.bim.module.geometry.data import RepresentationsData, ConnectionsData, DerivedPlacementsData
|
||||
|
||||
|
||||
class BIM_PT_representations(Panel):
|
||||
@@ -50,7 +51,7 @@ class BIM_PT_representations(Panel):
|
||||
layout.label(text="No representations found")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(context.scene.BIMRootProperties, "contexts", text="")
|
||||
prop_with_search(row, context.scene.BIMRootProperties, "contexts", text="")
|
||||
row.operator("bim.add_representation", icon="ADD", text="")
|
||||
|
||||
for representation in RepresentationsData.data["representations"]:
|
||||
@@ -67,6 +68,43 @@ class BIM_PT_representations(Panel):
|
||||
row.operator("bim.remove_representation", icon="X", text="").representation_id = representation["id"]
|
||||
|
||||
|
||||
class BIM_PT_connections(Panel):
|
||||
bl_label = "IFC Connections"
|
||||
bl_idname = "BIM_PT_connections"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "object"
|
||||
bl_parent_id = "BIM_PT_geometry_object"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
return False
|
||||
if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id):
|
||||
return False
|
||||
return IfcStore.get_file()
|
||||
|
||||
def draw(self, context):
|
||||
if not ConnectionsData.is_loaded:
|
||||
ConnectionsData.load()
|
||||
|
||||
layout = self.layout
|
||||
props = context.active_object.BIMObjectProperties
|
||||
|
||||
if not ConnectionsData.data["connections"]:
|
||||
layout.label(text="No connections found")
|
||||
|
||||
for connection in ConnectionsData.data["connections"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=connection["Name"], icon="SNAP_ON" if connection["is_relating"] else "SNAP_OFF")
|
||||
row.label(text=connection["ConnectionType"])
|
||||
op = row.operator("bim.select_connection", icon="RESTRICT_SELECT_OFF", text="")
|
||||
op.connection = connection["id"]
|
||||
op = row.operator("bim.remove_connection", icon="X", text="")
|
||||
op.connection = connection["id"]
|
||||
|
||||
|
||||
class BIM_PT_mesh(Panel):
|
||||
bl_label = "IFC Representation"
|
||||
bl_idname = "BIM_PT_mesh"
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import bpy
|
||||
import ifcopenshell.util.geolocation
|
||||
import blenderbim.tool as tool
|
||||
|
||||
|
||||
def refresh():
|
||||
GeoreferenceData.is_loaded = False
|
||||
|
||||
|
||||
class GeoreferenceData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data["blender_derived_angle"] = cls.blender_derived_angle()
|
||||
cls.data["map_conversion"] = cls.map_conversion()
|
||||
cls.data["map_derived_angle"] = cls.map_derived_angle()
|
||||
cls.data["projected_crs"] = cls.projected_crs()
|
||||
cls.data["true_north"] = cls.true_north()
|
||||
cls.data["true_derived_angle"] = cls.true_derived_angle()
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def blender_derived_angle(cls):
|
||||
props = bpy.context.scene.BIMGeoreferenceProperties
|
||||
if props.has_blender_offset:
|
||||
return str(
|
||||
round(
|
||||
ifcopenshell.util.geolocation.xaxis2angle(
|
||||
float(props.blender_x_axis_abscissa), float(props.blender_x_axis_ordinate)
|
||||
),
|
||||
3,
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def map_conversion(cls):
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
return {}
|
||||
|
||||
for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
|
||||
if context.HasCoordinateOperation:
|
||||
map_conversion = context.HasCoordinateOperation[0].get_info()
|
||||
del map_conversion["id"]
|
||||
del map_conversion["type"]
|
||||
del map_conversion["SourceCRS"]
|
||||
del map_conversion["TargetCRS"]
|
||||
return map_conversion
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def map_derived_angle(cls):
|
||||
if (
|
||||
cls.data["map_conversion"]
|
||||
and cls.data["map_conversion"]["XAxisAbscissa"] is not None
|
||||
and cls.data["map_conversion"]["XAxisOrdinate"] is not None
|
||||
):
|
||||
return str(
|
||||
round(
|
||||
ifcopenshell.util.geolocation.xaxis2angle(
|
||||
cls.data["map_conversion"]["XAxisAbscissa"], cls.data["map_conversion"]["XAxisOrdinate"]
|
||||
),
|
||||
3,
|
||||
)
|
||||
)
|
||||
return ""
|
||||
|
||||
@classmethod
|
||||
def projected_crs(cls):
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
return {}
|
||||
|
||||
for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
|
||||
if context.HasCoordinateOperation:
|
||||
projected_crs = context.HasCoordinateOperation[0].TargetCRS.get_info()
|
||||
del projected_crs["id"]
|
||||
del projected_crs["type"]
|
||||
if projected_crs["MapUnit"]:
|
||||
unit = projected_crs["MapUnit"]
|
||||
projected_crs["MapUnit"] = (getattr(unit, "Prefix", "") or "") + unit.Name
|
||||
return projected_crs
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def true_north(cls):
|
||||
ifc = tool.Ifc.get()
|
||||
true_north = {}
|
||||
|
||||
if ifc.schema == "IFC2X3":
|
||||
return
|
||||
|
||||
for context in ifc.by_type("IfcGeometricRepresentationContext", include_subtypes=False):
|
||||
if not context.TrueNorth:
|
||||
continue
|
||||
true_north = context.TrueNorth.DirectionRatios
|
||||
break
|
||||
|
||||
return true_north
|
||||
|
||||
@classmethod
|
||||
def true_derived_angle(cls):
|
||||
if cls.data["true_north"]:
|
||||
return str(round(ifcopenshell.util.geolocation.yaxis2angle(*cls.data["true_north"][0:2]), 3))
|
||||
@@ -17,345 +17,157 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import json
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.attribute
|
||||
import ifcopenshell.api
|
||||
import blenderbim.bim.helper
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from ifcopenshell.api.georeference.data import Data
|
||||
from ifcopenshell.api.unit.data import Data as UnitData
|
||||
from math import radians, degrees, atan, tan, cos, sin
|
||||
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.georeference as core
|
||||
|
||||
|
||||
class EnableEditingGeoreferencing(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_georeferencing"
|
||||
bl_label = "Enable Editing Georeferencing"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
props = context.scene.BIMGeoreferenceProperties
|
||||
self.props = props
|
||||
|
||||
props.projected_crs.clear()
|
||||
|
||||
blenderbim.bim.helper.import_attributes(
|
||||
"IfcProjectedCRS", props.projected_crs, Data.projected_crs, self.import_projected_crs_attributes
|
||||
)
|
||||
|
||||
props.map_conversion.clear()
|
||||
blenderbim.bim.helper.import_attributes(
|
||||
"IfcMapConversion", props.map_conversion, Data.map_conversion, self.import_map_conversion_attributes
|
||||
)
|
||||
|
||||
props.has_true_north = bool(Data.true_north)
|
||||
if Data.true_north:
|
||||
props.true_north_abscissa = str(Data.true_north[0])
|
||||
props.true_north_ordinate = str(Data.true_north[1])
|
||||
|
||||
props.is_editing = True
|
||||
return {"FINISHED"}
|
||||
|
||||
def import_projected_crs_attributes(self, name, prop, data):
|
||||
if name == "MapUnit":
|
||||
new = self.props.projected_crs.add()
|
||||
new.name = name
|
||||
new.data_type = "enum"
|
||||
new.is_null = data[name] is None
|
||||
new.is_optional = True
|
||||
new.enum_items = json.dumps(
|
||||
{u["id"]: u["Name"] for u in UnitData.units.values() if u["UnitType"] == "LENGTHUNIT"}
|
||||
)
|
||||
if data["MapUnit"]:
|
||||
new.enum_value = str(data["MapUnit"]["id"])
|
||||
return True
|
||||
|
||||
def import_map_conversion_attributes(self, name, prop, data):
|
||||
if name not in ["SourceCRS", "TargetCRS"]:
|
||||
# Enforce a string data type to prevent data loss in single-precision Blender props
|
||||
prop.data_type = "string"
|
||||
prop.string_value = "" if prop.is_null else str(data[name])
|
||||
return True
|
||||
|
||||
|
||||
class DisableEditingGeoreferencing(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_editing_georeferencing"
|
||||
bl_label = "Disable Editing Georeferencing"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMGeoreferenceProperties
|
||||
props.is_editing = False
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EditGeoreferencing(bpy.types.Operator):
|
||||
bl_idname = "bim.edit_georeferencing"
|
||||
bl_label = "Edit Georeferencing"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
props = context.scene.BIMGeoreferenceProperties
|
||||
|
||||
projected_crs = blenderbim.bim.helper.export_attributes(props.projected_crs, self.export_crs_attributes)
|
||||
map_conversion = blenderbim.bim.helper.export_attributes(props.map_conversion, self.export_map_attributes)
|
||||
|
||||
true_north = None
|
||||
if props.has_true_north:
|
||||
try:
|
||||
true_north = [float(props.true_north_abscissa), float(props.true_north_ordinate)]
|
||||
except ValueError:
|
||||
self.report({"ERROR"}, "True North Abscissa and Ordinate expect a number")
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"georeference.edit_georeferencing",
|
||||
self.file,
|
||||
**{
|
||||
"map_conversion": map_conversion,
|
||||
"projected_crs": projected_crs,
|
||||
"true_north": true_north,
|
||||
}
|
||||
)
|
||||
Data.load(self.file)
|
||||
bpy.ops.bim.disable_editing_georeferencing()
|
||||
return {"FINISHED"}
|
||||
|
||||
def export_map_attributes(self, attributes, prop):
|
||||
if not prop.is_null and prop.data_type == "string":
|
||||
# We store our floats as string to prevent single precision data loss
|
||||
attributes[prop.name] = float(prop.string_value)
|
||||
return True
|
||||
|
||||
def export_crs_attributes(self, attributes, prop):
|
||||
if not prop.is_null and prop.name == "MapUnit":
|
||||
attributes[prop.name] = self.file.by_id(int(prop.enum_value))
|
||||
return True
|
||||
|
||||
|
||||
class SetBlenderGridNorth(bpy.types.Operator):
|
||||
bl_idname = "bim.set_blender_grid_north"
|
||||
bl_label = "Set Blender Grid North"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.sun_pos_properties.north_offset = -radians(
|
||||
ifcopenshell.util.geolocation.xaxis2angle(
|
||||
float(context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisAbscissa").string_value),
|
||||
float(context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisOrdinate").string_value),
|
||||
)
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SetIfcGridNorth(bpy.types.Operator):
|
||||
bl_idname = "bim.set_ifc_grid_north"
|
||||
bl_label = "Set IFC Grid North"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
x_angle = -context.scene.sun_pos_properties.north_offset
|
||||
context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisAbscissa").string_value = str(cos(x_angle))
|
||||
context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisOrdinate").string_value = str(sin(x_angle))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SetBlenderTrueNorth(bpy.types.Operator):
|
||||
bl_idname = "bim.set_blender_true_north"
|
||||
bl_label = "Set Blender True North"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.sun_pos_properties.north_offset = -radians(
|
||||
ifcopenshell.util.geolocation.yaxis2angle(
|
||||
float(context.scene.BIMGeoreferenceProperties.true_north_abscissa),
|
||||
float(context.scene.BIMGeoreferenceProperties.true_north_ordinate),
|
||||
)
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SetIfcTrueNorth(bpy.types.Operator):
|
||||
bl_idname = "bim.set_ifc_true_north"
|
||||
bl_label = "Set IFC True North"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
y_angle = -context.scene.sun_pos_properties.north_offset + radians(90)
|
||||
context.scene.BIMGeoreferenceProperties.true_north_abscissa = str(cos(y_angle))
|
||||
context.scene.BIMGeoreferenceProperties.true_north_ordinate = str(sin(y_angle))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemoveGeoreferencing(bpy.types.Operator):
|
||||
bl_idname = "bim.remove_georeferencing"
|
||||
bl_label = "Remove Georeferencing"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
ifcopenshell.api.run("georeference.remove_georeferencing", IfcStore.get_file())
|
||||
Data.load(IfcStore.get_file())
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddGeoreferencing(bpy.types.Operator):
|
||||
class AddGeoreferencing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_georeferencing"
|
||||
bl_label = "Add Georeferencing"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
bl_description = "Add a new georeference"
|
||||
|
||||
def _execute(self, context):
|
||||
ifcopenshell.api.run("georeference.add_georeferencing", IfcStore.get_file())
|
||||
Data.load(IfcStore.get_file())
|
||||
return {"FINISHED"}
|
||||
core.add_georeferencing(tool.Ifc)
|
||||
|
||||
|
||||
class ConvertLocalToGlobal(bpy.types.Operator):
|
||||
bl_idname = "bim.convert_local_to_global"
|
||||
bl_label = "Convert Local To Global"
|
||||
class EnableEditingGeoreferencing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.enable_editing_georeferencing"
|
||||
bl_label = "Enable Editing Georeferencing"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Enable editing georeferencing"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
file = IfcStore.get_file()
|
||||
props = context.scene.BIMGeoreferenceProperties
|
||||
return file and props.coordinate_input.count(",") == 2
|
||||
|
||||
def execute(self, context):
|
||||
if not Data.is_loaded:
|
||||
Data.load(IfcStore.get_file())
|
||||
props = context.scene.BIMGeoreferenceProperties
|
||||
x, y, z = [float(co) for co in props.coordinate_input.split(",")]
|
||||
|
||||
if props.has_blender_offset:
|
||||
results = ifcopenshell.util.geolocation.xyz2enh(
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
float(props.blender_eastings),
|
||||
float(props.blender_northings),
|
||||
float(props.blender_orthogonal_height),
|
||||
float(props.blender_x_axis_abscissa),
|
||||
float(props.blender_x_axis_ordinate),
|
||||
1.0,
|
||||
)
|
||||
x, y, z = results
|
||||
|
||||
# TODO: what if the project CRS units and the project units are different?
|
||||
|
||||
if Data.map_conversion:
|
||||
results = ifcopenshell.util.geolocation.xyz2enh(
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
Data.map_conversion["Eastings"],
|
||||
Data.map_conversion["Northings"],
|
||||
Data.map_conversion["OrthogonalHeight"],
|
||||
Data.map_conversion.get("XAxisAbscissa", 1.0),
|
||||
Data.map_conversion.get("XAxisOrdinate", 0.0),
|
||||
Data.map_conversion.get("Scale", 1.0),
|
||||
)
|
||||
else:
|
||||
results = (x, y, z)
|
||||
|
||||
props.coordinate_output = ",".join([str(r) for r in results])
|
||||
context.scene.cursor.location = results
|
||||
return {"FINISHED"}
|
||||
def _execute(self, context):
|
||||
core.enable_editing_georeferencing(tool.Georeference)
|
||||
|
||||
|
||||
class ConvertGlobalToLocal(bpy.types.Operator):
|
||||
bl_idname = "bim.convert_global_to_local"
|
||||
bl_label = "Convert Global To Local"
|
||||
class RemoveGeoreferencing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_georeferencing"
|
||||
bl_label = "Remove Georeferencing"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Remove the georeferencing"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
file = IfcStore.get_file()
|
||||
props = context.scene.BIMGeoreferenceProperties
|
||||
return file and file.by_type("IfcUnitAssignment") and props.coordinate_input.count(",") == 2
|
||||
|
||||
def execute(self, context):
|
||||
if not Data.is_loaded:
|
||||
Data.load(IfcStore.get_file())
|
||||
props = context.scene.BIMGeoreferenceProperties
|
||||
x, y, z = [float(co) for co in props.coordinate_input.split(",")]
|
||||
|
||||
if Data.map_conversion:
|
||||
results = ifcopenshell.util.geolocation.enh2xyz(
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
Data.map_conversion["Eastings"],
|
||||
Data.map_conversion["Northings"],
|
||||
Data.map_conversion["OrthogonalHeight"],
|
||||
Data.map_conversion.get("XAxisAbscissa", 1.0),
|
||||
Data.map_conversion.get("XAxisOrdinate", 0.0),
|
||||
Data.map_conversion.get("Scale", 1.0),
|
||||
)
|
||||
else:
|
||||
results = (x, y, z)
|
||||
|
||||
if props.has_blender_offset:
|
||||
results = ifcopenshell.util.geolocation.enh2xyz(
|
||||
results[0],
|
||||
results[1],
|
||||
results[2],
|
||||
float(props.blender_eastings),
|
||||
float(props.blender_northings),
|
||||
float(props.blender_orthogonal_height),
|
||||
float(props.blender_x_axis_abscissa),
|
||||
float(props.blender_x_axis_ordinate),
|
||||
1.0,
|
||||
)
|
||||
|
||||
props.coordinate_output = ",".join([str(r) for r in results])
|
||||
|
||||
scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file())
|
||||
context.scene.cursor.location = [o * scale for o in results]
|
||||
return {"FINISHED"}
|
||||
def _execute(self, context):
|
||||
core.remove_georeferencing(tool.Ifc)
|
||||
|
||||
|
||||
class GetCursorLocation(bpy.types.Operator):
|
||||
class EditGeoreferencing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.edit_georeferencing"
|
||||
bl_label = "Edit Georeferencing"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Edit the georeferencing"
|
||||
|
||||
def _execute(self, context):
|
||||
core.edit_georeferencing(tool.Ifc, tool.Georeference)
|
||||
|
||||
|
||||
class DisableEditingGeoreferencing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.disable_editing_georeferencing"
|
||||
bl_label = "Disable Editing Georeferencing"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Close editing panel"
|
||||
|
||||
def _execute(self, context):
|
||||
core.disable_editing_georeferencing(tool.Georeference)
|
||||
|
||||
|
||||
class SetIfcGridNorth(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.set_ifc_grid_north"
|
||||
bl_label = "Set IFC Grid North"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Set IFC grid north"
|
||||
|
||||
def _execute(self, context):
|
||||
core.set_ifc_grid_north(tool.Georeference)
|
||||
|
||||
|
||||
class SetBlenderGridNorth(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.set_blender_grid_north"
|
||||
bl_label = "Set Blender Grid North"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Set Blender grid north"
|
||||
|
||||
def _execute(self, context):
|
||||
core.set_blender_grid_north(tool.Georeference)
|
||||
|
||||
|
||||
class GetCursorLocation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.get_cursor_location"
|
||||
bl_label = "Get Cursor Location"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Insert the current cursor coordinates"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
file = IfcStore.get_file()
|
||||
return file and file.by_type("IfcUnitAssignment")
|
||||
return tool.Ifc.get()
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMGeoreferenceProperties
|
||||
scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file())
|
||||
project_coordinates = [o / scale for o in context.scene.cursor.location]
|
||||
props.coordinate_input = ",".join([str(o) for o in project_coordinates])
|
||||
return {"FINISHED"}
|
||||
def _execute(self, context):
|
||||
core.get_cursor_location(tool.Georeference)
|
||||
|
||||
|
||||
class SetCursorLocation(bpy.types.Operator):
|
||||
class SetCursorLocation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.set_cursor_location"
|
||||
bl_label = "Set Cursor Location"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Move curson location to the specified coordinates"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
file = IfcStore.get_file()
|
||||
props = context.scene.BIMGeoreferenceProperties
|
||||
return file and file.by_type("IfcUnitAssignment") and props.coordinate_output.count(",") == 2
|
||||
return tool.Ifc.get() and props.coordinate_output.count(",") == 2
|
||||
|
||||
def execute(self, context):
|
||||
def _execute(self, context):
|
||||
core.set_cursor_location(tool.Georeference)
|
||||
|
||||
|
||||
class SetIfcTrueNorth(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.set_ifc_true_north"
|
||||
bl_label = "Set IFC True North"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Set IFC True north"
|
||||
|
||||
def _execute(self, context):
|
||||
core.set_ifc_true_north(tool.Georeference)
|
||||
|
||||
|
||||
class SetBlenderTrueNorth(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.set_blender_true_north"
|
||||
bl_label = "Set Blender True North"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Set Blender true north"
|
||||
|
||||
def _execute(self, context):
|
||||
core.set_blender_true_north(tool.Georeference)
|
||||
|
||||
|
||||
class ConvertLocalToGlobal(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.convert_local_to_global"
|
||||
bl_label = "Convert Local To Global"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Convert local coordinate to global coordinate"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
file = tool.Ifc.get()
|
||||
props = context.scene.BIMGeoreferenceProperties
|
||||
scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file())
|
||||
context.scene.cursor.location = [float(co) * scale for co in props.coordinate_output.split(",")]
|
||||
return {"FINISHED"}
|
||||
return file and props.coordinate_input.count(",") == 2
|
||||
|
||||
def _execute(self, context):
|
||||
core.convert_local_to_global(tool.Georeference)
|
||||
|
||||
|
||||
class ConvertGlobalToLocal(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.convert_global_to_local"
|
||||
bl_label = "Convert Global To Local"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Convert global coordinate to local coordinate"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
file = tool.Ifc.get()
|
||||
props = context.scene.BIMGeoreferenceProperties
|
||||
return file and file.by_type("IfcUnitAssignment") and props.coordinate_input.count(",") == 2
|
||||
|
||||
def _execute(self, context):
|
||||
core.convert_global_to_local(tool.Georeference)
|
||||
|
||||
@@ -16,11 +16,10 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import ifcopenshell.util.geolocation
|
||||
import blenderbim.tool as tool
|
||||
from bpy.types import Panel
|
||||
from ifcopenshell.api.georeference.data import Data
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.helper import draw_attributes, draw_attribute
|
||||
from blenderbim.bim.module.georeference.data import GeoreferenceData
|
||||
|
||||
|
||||
class BIM_PT_gis(Panel):
|
||||
@@ -32,16 +31,13 @@ class BIM_PT_gis(Panel):
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_geometry"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return IfcStore.get_file()
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.use_property_split = True
|
||||
self.layout.use_property_decorate = False
|
||||
props = context.scene.BIMGeoreferenceProperties
|
||||
if not Data.is_loaded:
|
||||
Data.load(IfcStore.get_file())
|
||||
|
||||
if not GeoreferenceData.is_loaded:
|
||||
GeoreferenceData.load()
|
||||
|
||||
if props.is_editing:
|
||||
return self.draw_editable_ui(context)
|
||||
@@ -82,10 +78,10 @@ class BIM_PT_gis(Panel):
|
||||
def draw_ui(self, context):
|
||||
props = context.scene.BIMGeoreferenceProperties
|
||||
|
||||
if not Data.projected_crs:
|
||||
if not GeoreferenceData.data["projected_crs"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Not Georeferenced")
|
||||
if IfcStore.get_file().schema != "IFC2X3":
|
||||
if tool.Ifc.get_schema != "IFC2X3":
|
||||
row.operator("bim.add_georeferencing", icon="ADD", text="")
|
||||
|
||||
if props.has_blender_offset:
|
||||
@@ -109,40 +105,27 @@ class BIM_PT_gis(Panel):
|
||||
row.label(text=props.blender_x_axis_ordinate)
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Derived Grid North")
|
||||
row.label(
|
||||
text=str(
|
||||
round(
|
||||
ifcopenshell.util.geolocation.xaxis2angle(
|
||||
float(props.blender_x_axis_abscissa), float(props.blender_x_axis_ordinate)
|
||||
),
|
||||
3,
|
||||
)
|
||||
)
|
||||
)
|
||||
row.label(text=GeoreferenceData.data["blender_derived_angle"])
|
||||
|
||||
if Data.projected_crs:
|
||||
if GeoreferenceData.data["projected_crs"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Projected CRS", icon="WORLD")
|
||||
row.operator("bim.enable_editing_georeferencing", icon="GREASEPENCIL", text="")
|
||||
row.operator("bim.remove_georeferencing", icon="X", text="")
|
||||
|
||||
for key, value in Data.projected_crs.items():
|
||||
if key == "id" or key == "type" or not value:
|
||||
for key, value in GeoreferenceData.data["projected_crs"].items():
|
||||
if not value:
|
||||
continue
|
||||
if key == "MapUnit":
|
||||
unit_value = value.get("Prefix", "") or ""
|
||||
unit_value += value["Name"]
|
||||
value = unit_value
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=key)
|
||||
row.label(text=str(value))
|
||||
|
||||
if Data.map_conversion:
|
||||
if GeoreferenceData.data["map_conversion"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Map Conversion", icon="GRID")
|
||||
|
||||
for key, value in Data.map_conversion.items():
|
||||
if key == "id" or key == "type" or key == "SourceCRS" or key == "TargetCRS" or value is None:
|
||||
for key, value in GeoreferenceData.data["map_conversion"].items():
|
||||
if value is None:
|
||||
continue
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=key)
|
||||
@@ -150,26 +133,17 @@ class BIM_PT_gis(Panel):
|
||||
if key == "XAxisOrdinate":
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Derived Angle")
|
||||
row.label(
|
||||
text=str(
|
||||
round(
|
||||
ifcopenshell.util.geolocation.xaxis2angle(
|
||||
Data.map_conversion["XAxisAbscissa"], Data.map_conversion["XAxisOrdinate"]
|
||||
),
|
||||
3,
|
||||
)
|
||||
)
|
||||
)
|
||||
row.label(text=GeoreferenceData.data["map_derived_angle"])
|
||||
|
||||
if Data.true_north:
|
||||
if GeoreferenceData.data["true_north"]:
|
||||
row = self.layout.row()
|
||||
row.label(text="True North", icon="LIGHT_SUN")
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Vector")
|
||||
row.label(text=str(Data.true_north[0:2])[1:-1])
|
||||
row.label(text=str(GeoreferenceData.data["true_north"][0:2])[1:-1])
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Derived Angle")
|
||||
row.label(text=str(round(ifcopenshell.util.geolocation.yaxis2angle(*Data.true_north[0:2]), 3)))
|
||||
row.label(text=GeoreferenceData.data["true_derived_angle"])
|
||||
|
||||
|
||||
class BIM_PT_gis_utilities(Panel):
|
||||
|
||||
@@ -20,17 +20,20 @@ import bpy
|
||||
from . import ui, prop, operator
|
||||
|
||||
classes = (
|
||||
operator.LoadGroups,
|
||||
operator.DisableGroupEditingUI,
|
||||
operator.AddGroup,
|
||||
operator.EditGroup,
|
||||
operator.RemoveGroup,
|
||||
operator.ToggleAssigningGroup,
|
||||
operator.AssignGroup,
|
||||
operator.UnassignGroup,
|
||||
operator.EnableEditingGroup,
|
||||
operator.DisableEditingGroup,
|
||||
operator.DisableGroupEditingUI,
|
||||
operator.EditGroup,
|
||||
operator.EnableEditingGroup,
|
||||
operator.LoadGroups,
|
||||
operator.RemoveGroup,
|
||||
operator.SelectGroupProducts,
|
||||
operator.ToggleAssigningGroup,
|
||||
operator.ToggleGroup,
|
||||
operator.UnassignGroup,
|
||||
operator.UpdateGroup,
|
||||
prop.ExpandedGroups,
|
||||
prop.Group,
|
||||
prop.BIMGroupProperties,
|
||||
ui.BIM_PT_groups,
|
||||
@@ -42,7 +45,9 @@ classes = (
|
||||
|
||||
def register():
|
||||
bpy.types.Scene.BIMGroupProperties = bpy.props.PointerProperty(type=prop.BIMGroupProperties)
|
||||
bpy.types.Scene.ExpandedGroups = bpy.props.PointerProperty(type=prop.ExpandedGroups)
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.Scene.BIMGroupProperties
|
||||
del bpy.types.Scene.ExpandedGroups
|
||||
|
||||
@@ -20,24 +20,89 @@ import bpy
|
||||
import ifcopenshell.util.attribute
|
||||
import ifcopenshell.api
|
||||
import blenderbim.bim.helper
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from ifcopenshell.api.group.data import Data
|
||||
from ifcopenshell.util.selector import Selector
|
||||
import json
|
||||
|
||||
|
||||
class LoadGroups(bpy.types.Operator):
|
||||
bl_idname = "bim.load_groups"
|
||||
bl_label = "Load Groups"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
is_refresh: bpy.props.BoolProperty(default=False)
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMGroupProperties
|
||||
props.groups.clear()
|
||||
self.props = context.scene.BIMGroupProperties
|
||||
self.expanded_groups = json.loads(context.scene.ExpandedGroups.json_string)
|
||||
self.props.groups.clear()
|
||||
self.ifc = IfcStore.get_file()
|
||||
if not self.is_refresh:
|
||||
context.scene.ExpandedGroups.json_string = "{}"
|
||||
|
||||
for ifc_definition_id, group in Data.groups.items():
|
||||
new = props.groups.add()
|
||||
new.ifc_definition_id = ifc_definition_id
|
||||
new.name = group["Name"]
|
||||
props.is_editing = True
|
||||
if not group["HasAssignments"]:
|
||||
new = self.props.groups.add()
|
||||
new.ifc_definition_id = ifc_definition_id
|
||||
new.name = group["Name"]
|
||||
new.selection_query = group["Description"].split("*selector*")[1] if group["Description"] else ""
|
||||
new.tree_depth = 0
|
||||
|
||||
if group["IsGroupedBy"]:
|
||||
# assumes 1:1 cardinality, will need to be updated to reflect IFC4 changes
|
||||
# where the cardinality is 0:? - vulevukusej
|
||||
sub_groups = [g for g in group["IsGroupedBy"][0].RelatedObjects if g.is_a("IfcGroup")]
|
||||
new.has_children = True if len(sub_groups) != 0 else False
|
||||
|
||||
if self.is_refresh:
|
||||
self.recursively_load_sub_groups(ifc_definition_id, new)
|
||||
|
||||
self.props.is_editing = True
|
||||
bpy.ops.bim.disable_editing_group()
|
||||
Data.load(IfcStore.get_file())
|
||||
return {"FINISHED"}
|
||||
|
||||
def recursively_load_sub_groups(self, ifc_definition_id, parent):
|
||||
sub_groups = [
|
||||
k for k, v in Data.products.items() if self.ifc.by_id(k).is_a("IfcGroup") and ifc_definition_id in v
|
||||
]
|
||||
if str(ifc_definition_id) not in self.expanded_groups or sub_groups == []:
|
||||
return
|
||||
else:
|
||||
parent.is_expanded = True
|
||||
parent.has_children = True
|
||||
for group in sub_groups:
|
||||
new = self.props.groups.add()
|
||||
new.ifc_definition_id = group
|
||||
new.name = Data.groups[group]["Name"]
|
||||
new.selection_query = (
|
||||
Data.groups[group]["Description"].split("*selector*")[1]
|
||||
if Data.groups[group]["Description"]
|
||||
else ""
|
||||
)
|
||||
new.has_children = True if len(Data.groups[group]["IsGroupedBy"]) != 0 else False
|
||||
new.tree_depth = parent.tree_depth + 1
|
||||
self.recursively_load_sub_groups(new.ifc_definition_id, new)
|
||||
|
||||
|
||||
class ToggleGroup(bpy.types.Operator):
|
||||
bl_idname = "bim.toggle_group"
|
||||
bl_label = "Toggle Group"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
ifc_definition_id: bpy.props.IntProperty()
|
||||
index: bpy.props.IntProperty()
|
||||
option: bpy.props.StringProperty(name="Expand or Collapse")
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.BIMGroupProperties.groups[self.index].is_expanded = True if self.option == "Expand" else False
|
||||
json_string = json.loads(context.scene.ExpandedGroups.json_string)
|
||||
if self.option == "Expand":
|
||||
json_string.setdefault(str(self.ifc_definition_id), None)
|
||||
else:
|
||||
json_string.pop(str(self.ifc_definition_id), None)
|
||||
context.scene.ExpandedGroups.json_string = json.dumps(json_string)
|
||||
bpy.ops.bim.load_groups(is_refresh=True)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -54,17 +119,21 @@ class DisableGroupEditingUI(bpy.types.Operator):
|
||||
|
||||
class AddGroup(bpy.types.Operator):
|
||||
bl_idname = "bim.add_group"
|
||||
bl_label = "Add Group"
|
||||
bl_label = "Add New Group"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
group: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
result = ifcopenshell.api.run("group.add_group", IfcStore.get_file())
|
||||
Data.load(IfcStore.get_file())
|
||||
bpy.ops.bim.load_groups()
|
||||
bpy.ops.bim.enable_editing_group(group=result.id())
|
||||
result = ifcopenshell.api.run("group.add_group", tool.Ifc.get())
|
||||
if self.group:
|
||||
ifcopenshell.api.run(
|
||||
"group.assign_group", tool.Ifc.get(), products=[result], group=tool.Ifc.get().by_id(self.group)
|
||||
)
|
||||
Data.load(tool.Ifc.get())
|
||||
bpy.ops.bim.load_groups(is_refresh=True)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -72,6 +141,7 @@ class EditGroup(bpy.types.Operator):
|
||||
bl_idname = "bim.edit_group"
|
||||
bl_label = "Edit Group"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
copy_from_selector: bpy.props.BoolProperty(name="Copy from Selector", default=False)
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
@@ -84,12 +154,16 @@ class EditGroup(bpy.types.Operator):
|
||||
attributes[attribute.name] = None
|
||||
else:
|
||||
attributes[attribute.name] = attribute.string_value
|
||||
|
||||
if self.copy_from_selector:
|
||||
attributes["Description"] = context.scene.IFCSelector.selection_query
|
||||
|
||||
self.file = IfcStore.get_file()
|
||||
ifcopenshell.api.run(
|
||||
"group.edit_group", self.file, **{"group": self.file.by_id(props.active_group_id), "attributes": attributes}
|
||||
)
|
||||
Data.load(IfcStore.get_file())
|
||||
bpy.ops.bim.load_groups()
|
||||
bpy.ops.bim.load_groups(is_refresh=True)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -107,7 +181,7 @@ class RemoveGroup(bpy.types.Operator):
|
||||
self.file = IfcStore.get_file()
|
||||
ifcopenshell.api.run("group.remove_group", self.file, **{"group": self.file.by_id(self.group)})
|
||||
Data.load(IfcStore.get_file())
|
||||
bpy.ops.bim.load_groups()
|
||||
bpy.ops.bim.load_groups(is_refresh=True)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -120,9 +194,7 @@ class EnableEditingGroup(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMGroupProperties
|
||||
props.group_attributes.clear()
|
||||
|
||||
blenderbim.bim.helper.import_attributes("IfcGroup", props.group_attributes, Data.groups[self.group])
|
||||
|
||||
props.active_group_id = self.group
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -166,10 +238,8 @@ class AssignGroup(bpy.types.Operator):
|
||||
ifcopenshell.api.run(
|
||||
"group.assign_group",
|
||||
self.file,
|
||||
**{
|
||||
"product": self.file.by_id(product.BIMObjectProperties.ifc_definition_id),
|
||||
"group": self.file.by_id(self.group),
|
||||
}
|
||||
products=[self.file.by_id(product.BIMObjectProperties.ifc_definition_id)],
|
||||
group=self.file.by_id(self.group),
|
||||
)
|
||||
Data.load(self.file)
|
||||
return {"FINISHED"}
|
||||
@@ -219,3 +289,32 @@ class SelectGroupProducts(bpy.types.Operator):
|
||||
if self.group in product_groups:
|
||||
obj.select_set(True)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class UpdateGroup(bpy.types.Operator):
|
||||
bl_idname = "bim.update_group"
|
||||
bl_label = "Update Group"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
query: bpy.props.StringProperty()
|
||||
group_id: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
group = self.file.by_id(self.group_id)
|
||||
query = self.query
|
||||
|
||||
new_products = Selector.parse(self.file, query)
|
||||
ifcopenshell.api.run(
|
||||
"group.update_group_products",
|
||||
self.file,
|
||||
**{
|
||||
"group": group,
|
||||
"products": new_products,
|
||||
}
|
||||
)
|
||||
Data.load(IfcStore.get_file())
|
||||
bpy.ops.bim.load_groups(is_refresh=True)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -18,7 +18,10 @@
|
||||
|
||||
import bpy
|
||||
from blenderbim.bim.prop import StrProperty, Attribute
|
||||
from blenderbim.bim.helper import import_attributes
|
||||
from ifcopenshell.api.group.data import Data
|
||||
from bpy.types import PropertyGroup
|
||||
import json
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
StringProperty,
|
||||
@@ -31,9 +34,17 @@ from bpy.props import (
|
||||
)
|
||||
|
||||
|
||||
class ExpandedGroups(StrProperty):
|
||||
json_string: StringProperty(name="JSON String", default="{}")
|
||||
|
||||
|
||||
class Group(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
selection_query: StringProperty(name="Selection Query")
|
||||
is_expanded: BoolProperty(name="Is Expanded", default=False)
|
||||
has_children: BoolProperty(name="Has Children", default=False)
|
||||
tree_depth: IntProperty(name="Tree Depth")
|
||||
|
||||
|
||||
class BIMGroupProperties(PropertyGroup):
|
||||
|
||||
@@ -40,9 +40,9 @@ class BIM_PT_groups(Panel):
|
||||
self.props = context.scene.BIMGroupProperties
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="{} Groups Found".format(len(Data.groups)), icon="OUTLINER")
|
||||
row.label(text=f"{len(Data.groups)} Groups Found", icon="OUTLINER")
|
||||
if self.props.is_editing:
|
||||
row.operator("bim.add_group", text="", icon="ADD")
|
||||
row.operator("bim.add_group", text="", icon="ADD").group = 0
|
||||
row.operator("bim.disable_group_editing_ui", text="", icon="CANCEL")
|
||||
else:
|
||||
row.operator("bim.load_groups", text="", icon="GREASEPENCIL")
|
||||
@@ -115,10 +115,22 @@ class BIM_PT_object_groups(Panel):
|
||||
|
||||
|
||||
class BIM_UL_groups(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
row.label(text=item.name)
|
||||
for i in range(0, item.tree_depth):
|
||||
row.label(text="", icon="BLANK1")
|
||||
if item.has_children:
|
||||
op = row.operator(
|
||||
"bim.toggle_group", icon="TRIA_DOWN" if item.is_expanded else "TRIA_RIGHT", text="", emboss=False
|
||||
)
|
||||
op.ifc_definition_id = item.ifc_definition_id
|
||||
op.index = index
|
||||
op.option = "Collapse" if item.is_expanded else "Expand"
|
||||
else:
|
||||
row.label(text="", icon="BLANK1")
|
||||
|
||||
row.label(text=f"*{item.name}") if item.selection_query != "" else row.label(text=item.name)
|
||||
group_id = item.ifc_definition_id
|
||||
if context.scene.BIMGroupProperties.active_group_id == group_id:
|
||||
op = row.operator("bim.select_group_products", text="", icon="RESTRICT_SELECT_OFF")
|
||||
@@ -128,21 +140,46 @@ class BIM_UL_groups(UIList):
|
||||
elif context.scene.BIMGroupProperties.active_group_id:
|
||||
op = row.operator("bim.select_group_products", text="", icon="RESTRICT_SELECT_OFF")
|
||||
op.group = group_id
|
||||
op = row.operator("bim.add_group", text="", icon="ADD")
|
||||
op.group = group_id
|
||||
op = row.operator("bim.remove_group", text="", icon="X")
|
||||
op.group = group_id
|
||||
if item.selection_query != "":
|
||||
op = row.operator("bim.update_group", text="", icon="FILE_REFRESH")
|
||||
op.group_id = item.ifc_definition_id
|
||||
op.query = item.selection_query
|
||||
else:
|
||||
op = row.operator("bim.select_group_products", text="", icon="RESTRICT_SELECT_OFF")
|
||||
op.group = group_id
|
||||
op = row.operator("bim.enable_editing_group", text="", icon="GREASEPENCIL")
|
||||
op.group = group_id
|
||||
op = row.operator("bim.add_group", text="", icon="ADD")
|
||||
op.group = group_id
|
||||
op = row.operator("bim.remove_group", text="", icon="X")
|
||||
op.group = group_id
|
||||
if item.selection_query != "":
|
||||
op = row.operator("bim.update_group", text="", icon="FILE_REFRESH")
|
||||
op.group_id = item.ifc_definition_id
|
||||
op.query = item.selection_query
|
||||
|
||||
|
||||
class BIM_UL_object_groups(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
for i in range(0, item.tree_depth):
|
||||
row.label(text="", icon="BLANK1")
|
||||
if item.has_children:
|
||||
op = row.operator(
|
||||
"bim.toggle_group", icon="TRIA_DOWN" if item.is_expanded else "TRIA_RIGHT", text="", emboss=False
|
||||
)
|
||||
op.ifc_definition_id = item.ifc_definition_id
|
||||
op.index = index
|
||||
op.option = "Collapse" if item.is_expanded else "Expand"
|
||||
else:
|
||||
row.label(text="", icon="BLANK1")
|
||||
row.label(text=item.name)
|
||||
op = row.operator("bim.remove_group", text="", icon="X")
|
||||
op.group = item.ifc_definition_id
|
||||
op = row.operator("bim.assign_group", text="", icon="ADD")
|
||||
op.group = item.ifc_definition_id
|
||||
|
||||
@@ -28,6 +28,7 @@ classes = (
|
||||
operator.AddProfile,
|
||||
operator.AssignMaterial,
|
||||
operator.AssignParameterizedProfile,
|
||||
operator.ContractMaterialCategory,
|
||||
operator.CopyMaterial,
|
||||
operator.DisableEditingAssignedMaterial,
|
||||
operator.DisableEditingMaterialSetItem,
|
||||
@@ -36,6 +37,7 @@ classes = (
|
||||
operator.EditMaterialSetItem,
|
||||
operator.EnableEditingAssignedMaterial,
|
||||
operator.EnableEditingMaterialSetItem,
|
||||
operator.ExpandMaterialCategory,
|
||||
operator.LoadMaterials,
|
||||
operator.RemoveConstituent,
|
||||
operator.RemoveLayer,
|
||||
|
||||
@@ -78,9 +78,26 @@ class ObjectMaterialData:
|
||||
def load(cls):
|
||||
cls.data = {
|
||||
"materials": cls.materials(),
|
||||
"type_material": cls.type_material(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def materials(cls):
|
||||
return sorted([(str(m.id()), m.Name or "Unnamed", "") for m in tool.Ifc.get().by_type("IfcMaterial")], key=lambda x: x[1])
|
||||
return sorted(
|
||||
[(str(m.id()), m.Name or "Unnamed", "") for m in tool.Ifc.get().by_type("IfcMaterial")], key=lambda x: x[1]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def type_material(cls):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
element_type = ifcopenshell.util.element.get_type(element)
|
||||
if element_type and element_type != element:
|
||||
material = ifcopenshell.util.element.get_material(element_type)
|
||||
if not material:
|
||||
return
|
||||
if material.is_a() in ("IfcMaterialLayerSetUsage", "IfcMaterialLayerSet"):
|
||||
name_attr = "LayerSetName"
|
||||
else:
|
||||
name_attr = "Name"
|
||||
return getattr(material, name_attr, "Unnamed") or "Unnamed"
|
||||
|
||||
@@ -778,3 +778,31 @@ class CopyMaterial(bpy.types.Operator):
|
||||
if material.id() in object_material_ids:
|
||||
return
|
||||
obj.data.materials.append(IfcStore.get_element(material.id()))
|
||||
|
||||
|
||||
class ExpandMaterialCategory(bpy.types.Operator):
|
||||
bl_idname = "bim.expand_material_category"
|
||||
bl_label = "Expand Material Category"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
category: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMMaterialProperties
|
||||
for category in [c for c in props.materials if c.is_category and c.name == self.category]:
|
||||
category.is_expanded = True
|
||||
core.load_materials(tool.Material, props.material_type)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ContractMaterialCategory(bpy.types.Operator):
|
||||
bl_idname = "bim.contract_material_category"
|
||||
bl_label = "Contract Material Category"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
category: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMMaterialProperties
|
||||
for category in [c for c in props.materials if c.is_category and c.name == self.category]:
|
||||
category.is_expanded = False
|
||||
core.load_materials(tool.Material, props.material_type)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -109,6 +109,8 @@ def get_material_types(self, context):
|
||||
class Material(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
is_category: BoolProperty(name="Is Category", default=False)
|
||||
is_expanded: BoolProperty(name="Is Expanded", default=True)
|
||||
total_elements: IntProperty(name="Total Elements")
|
||||
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from ifcopenshell.api.material.data import Data
|
||||
from ifcopenshell.api.profile.data import Data as ProfileData
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.helper import draw_attributes
|
||||
from blenderbim.bim.helper import prop_with_search
|
||||
from blenderbim.bim.module.material.data import MaterialsData, ObjectMaterialData
|
||||
|
||||
|
||||
@@ -50,7 +51,7 @@ class BIM_PT_materials(Panel):
|
||||
row.operator("bim.disable_editing_materials", text="", icon="CANCEL")
|
||||
else:
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "material_type", text="")
|
||||
prop_with_search(row, self.props, "material_type", text="")
|
||||
row.operator("bim.load_materials", text="", icon="IMPORT")
|
||||
return
|
||||
|
||||
@@ -61,16 +62,18 @@ class BIM_PT_materials(Panel):
|
||||
row.operator("bim.add_material", text="", icon="ADD")
|
||||
if self.props.materials and self.props.active_material_index < len(self.props.materials):
|
||||
material = self.props.materials[self.props.active_material_index]
|
||||
op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF")
|
||||
op.material = material.ifc_definition_id
|
||||
row.operator("bim.remove_material", text="", icon="X").material = material.ifc_definition_id
|
||||
if material.ifc_definition_id:
|
||||
op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF")
|
||||
op.material = material.ifc_definition_id
|
||||
row.operator("bim.remove_material", text="", icon="X").material = material.ifc_definition_id
|
||||
else:
|
||||
row.operator("bim.add_material_set", text="", icon="ADD").set_type = self.props.material_type
|
||||
if self.props.materials and self.props.active_material_index < len(self.props.materials):
|
||||
material = self.props.materials[self.props.active_material_index]
|
||||
op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF")
|
||||
op.material = material.ifc_definition_id
|
||||
row.operator("bim.remove_material_set", text="", icon="X").material = material.ifc_definition_id
|
||||
if material.ifc_definition_id:
|
||||
op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF")
|
||||
op.material = material.ifc_definition_id
|
||||
row.operator("bim.remove_material_set", text="", icon="X").material = material.ifc_definition_id
|
||||
|
||||
self.layout.template_list("BIM_UL_materials", "", self.props, "materials", self.props, "active_material_index")
|
||||
|
||||
@@ -139,6 +142,10 @@ class BIM_PT_object_material(Panel):
|
||||
row.operator("bim.add_material", icon="ADD", text="").obj = ""
|
||||
return
|
||||
|
||||
if ObjectMaterialData.data["type_material"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Inherited Material: " + ObjectMaterialData.data["type_material"], icon="FILE_PARENT")
|
||||
|
||||
if self.product_data:
|
||||
if self.product_data["type"] == "IfcMaterialConstituentSet":
|
||||
self.material_set_id = self.product_data["id"]
|
||||
@@ -182,9 +189,9 @@ class BIM_PT_object_material(Panel):
|
||||
return self.draw_material_ui()
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "material_type", text="")
|
||||
prop_with_search(row, self.props, "material_type", text="")
|
||||
if self.props.material_type == "IfcMaterial" or self.props.material_type == "IfcMaterialList":
|
||||
row.prop(self.props, "material", text="")
|
||||
prop_with_search(row, self.props, "material", text="")
|
||||
row.operator("bim.assign_material", icon="ADD", text="")
|
||||
|
||||
def draw_material_ui(self):
|
||||
@@ -214,8 +221,7 @@ class BIM_PT_object_material(Panel):
|
||||
return self.draw_read_only_single_ui()
|
||||
|
||||
def draw_editable_single_ui(self):
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "material", text="")
|
||||
prop_with_search(self.layout, self.props, "material", text="")
|
||||
|
||||
def draw_read_only_single_ui(self):
|
||||
material = Data.materials[self.product_data["id"]]
|
||||
@@ -236,7 +242,7 @@ class BIM_PT_object_material(Panel):
|
||||
row.prop(attribute, "string_value", text=attribute.name)
|
||||
row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "material", text="")
|
||||
prop_with_search(row, self.props, "material", text="")
|
||||
|
||||
op = row.operator(f"bim.add_{self.set_item_name}", icon="ADD", text="")
|
||||
setattr(op, f"{self.set_item_name}_set", self.material_set_id)
|
||||
@@ -395,7 +401,21 @@ class BIM_UL_materials(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
row.label(text=item.name)
|
||||
row2 = row.row()
|
||||
row2.alignment = "RIGHT"
|
||||
row2.label(text=str(item.total_elements))
|
||||
|
||||
if item.is_category:
|
||||
if item.is_expanded:
|
||||
row.operator(
|
||||
"bim.contract_material_category", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN"
|
||||
).category = item.name
|
||||
else:
|
||||
row.operator(
|
||||
"bim.expand_material_category", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT"
|
||||
).category = item.name
|
||||
row.label(text=item.name or "Uncategorised")
|
||||
else:
|
||||
row.label(text="", icon="BLANK1")
|
||||
row.label(text=item.name, icon="MATERIAL")
|
||||
|
||||
row2 = row.row()
|
||||
row2.alignment = "RIGHT"
|
||||
row2.label(text=str(item.total_elements))
|
||||
|
||||
@@ -49,3 +49,8 @@ class BIM_PT_misc_utilities(bpy.types.Panel):
|
||||
row.operator("bim.draw_system_arrows")
|
||||
row = layout.row()
|
||||
row.operator("bim.clean_wireframes")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.enable_editing_sketch_extrusion_profile", text="Start Sketching")
|
||||
row.operator("bim.edit_sketch_extrusion_profile", text="", icon="FILE_REFRESH")
|
||||
row.operator("bim.disable_editing_sketch_extrusion_profile", text="", icon="CANCEL")
|
||||
|
||||
@@ -17,26 +17,57 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from . import handler, prop, ui, grid, product, wall, slab, stair, opening, pie, workspace
|
||||
from . import handler, prop, ui, grid, product, wall, slab, stair, opening, pie, workspace, profile
|
||||
|
||||
classes = (
|
||||
product.AddEmptyType,
|
||||
product.AddTypeInstance,
|
||||
product.AddConstrTypeInstance,
|
||||
product.DisplayConstrTypes,
|
||||
product.ReinvokeOperator,
|
||||
product.AlignProduct,
|
||||
product.DynamicallyVoidProduct,
|
||||
workspace.Hotkey,
|
||||
wall.JoinWall,
|
||||
wall.AlignWall,
|
||||
wall.ChangeExtrusionDepth,
|
||||
wall.ChangeLayerLength,
|
||||
wall.FlipWall,
|
||||
wall.JoinWall,
|
||||
wall.MergeWall,
|
||||
wall.RecalculateWall,
|
||||
wall.SplitWall,
|
||||
opening.AddBoolean,
|
||||
opening.AddElementOpening,
|
||||
opening.AddPotentialHalfSpaceSolid,
|
||||
opening.AddPotentialOpening,
|
||||
opening.EditOpenings,
|
||||
opening.HideBooleans,
|
||||
opening.HideOpenings,
|
||||
opening.RemoveBooleans,
|
||||
opening.ShowBooleans,
|
||||
opening.ShowOpenings,
|
||||
profile.ChangeCardinalPoint,
|
||||
profile.ChangeProfileDepth,
|
||||
profile.ExtendProfile,
|
||||
profile.RecalculateProfile,
|
||||
profile.Rotate90,
|
||||
slab.DisableEditingExtrusionProfile,
|
||||
slab.DisableEditingSketchExtrusionProfile,
|
||||
slab.EditExtrusionProfile,
|
||||
slab.EditSketchExtrusionProfile,
|
||||
slab.EnableEditingExtrusionProfile,
|
||||
slab.EnableEditingSketchExtrusionProfile,
|
||||
slab.ResetVertex,
|
||||
slab.SetArcIndex,
|
||||
prop.ConstrTypeInfo,
|
||||
prop.ConstrClassInfo,
|
||||
prop.ConstrBrowserState,
|
||||
prop.BIMModelProperties,
|
||||
ui.BIM_PT_authoring,
|
||||
ui.BIM_PT_authoring_architectural,
|
||||
ui.DisplayConstrTypesUI,
|
||||
ui.HelpConstrTypes,
|
||||
ui.BIM_MT_model,
|
||||
grid.BIM_OT_add_object,
|
||||
stair.BIM_OT_add_object,
|
||||
opening.BIM_OT_add_object,
|
||||
pie.OpenPieClass,
|
||||
pie.PieUpdateContainer,
|
||||
pie.PieAddOpening,
|
||||
@@ -53,8 +84,7 @@ def register():
|
||||
bpy.types.Scene.BIMModelProperties = bpy.props.PointerProperty(type=prop.BIMModelProperties)
|
||||
bpy.types.VIEW3D_MT_mesh_add.append(grid.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.append(stair.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.append(opening.add_object_button)
|
||||
bpy.types.VIEW3D_MT_add.append(product.add_empty_type_button)
|
||||
bpy.types.VIEW3D_MT_add.append(ui.add_menu)
|
||||
bpy.app.handlers.load_post.append(handler.load_post)
|
||||
wm = bpy.context.window_manager
|
||||
if wm.keyconfigs.addon:
|
||||
@@ -71,8 +101,7 @@ def unregister():
|
||||
bpy.app.handlers.load_post.remove(handler.load_post)
|
||||
bpy.types.VIEW3D_MT_mesh_add.remove(grid.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.remove(stair.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.remove(opening.add_object_button)
|
||||
bpy.types.VIEW3D_MT_add.remove(product.add_empty_type_button)
|
||||
bpy.types.VIEW3D_MT_add.remove(ui.add_menu)
|
||||
wm = bpy.context.window_manager
|
||||
kc = wm.keyconfigs.addon
|
||||
if kc:
|
||||
|
||||
@@ -16,8 +16,11 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import functools
|
||||
import bpy
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound
|
||||
|
||||
|
||||
def refresh():
|
||||
@@ -31,10 +34,40 @@ class AuthoringData:
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.data = {
|
||||
"ifc_classes": cls.ifc_classes(),
|
||||
"relating_types": cls.relating_types(),
|
||||
}
|
||||
if not hasattr(cls, "data"):
|
||||
cls.data = {}
|
||||
cls.props = bpy.context.scene.BIMModelProperties
|
||||
cls.load_ifc_classes()
|
||||
cls.load_relating_types()
|
||||
cls.load_relating_types_browser()
|
||||
cls.data["is_voidable_element"] = cls.is_voidable_element()
|
||||
cls.data["has_visible_openings"] = cls.has_visible_openings()
|
||||
|
||||
@classmethod
|
||||
def load_ifc_classes(cls):
|
||||
cls.data["ifc_classes"] = cls.ifc_classes()
|
||||
|
||||
@classmethod
|
||||
def load_relating_types(cls):
|
||||
cls.data["relating_types_ids"] = cls.relating_types()
|
||||
|
||||
@classmethod
|
||||
def load_relating_types_browser(cls):
|
||||
cls.data["relating_types_ids_browser"] = cls.relating_types_browser()
|
||||
|
||||
@classmethod
|
||||
def is_voidable_element(cls):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
return element and element.is_a("IfcElement") and not element.is_a("IfcOpeningElement")
|
||||
|
||||
@classmethod
|
||||
def has_visible_openings(cls):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
if element and element.is_a("IfcElement") and not element.is_a("IfcOpeningElement"):
|
||||
for opening in [r.RelatedOpeningElement for r in element.HasOpenings]:
|
||||
if tool.Ifc.get_object(opening):
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def ifc_classes(cls):
|
||||
@@ -49,16 +82,147 @@ class AuthoringData:
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def relating_types(cls):
|
||||
ifc_classes = cls.ifc_classes()
|
||||
def constr_class_entities(cls, ifc_class=None):
|
||||
ifc_classes = cls.data["ifc_classes"]
|
||||
if not ifc_classes:
|
||||
return []
|
||||
results = []
|
||||
ifc_class = bpy.context.scene.BIMModelProperties.ifc_class
|
||||
if ifc_class is None:
|
||||
ifc_class = cls.props.ifc_class
|
||||
if not ifc_class and ifc_classes:
|
||||
ifc_class = ifc_classes[0][0]
|
||||
if ifc_class:
|
||||
elements = [(str(e.id()), e.Name, e.Description or "") for e in tool.Ifc.get().by_type(ifc_class)]
|
||||
results.extend(sorted(elements, key=lambda s: s[1]))
|
||||
elements = sorted(tool.Ifc.get().by_type(ifc_class), key=lambda s: s.Name)
|
||||
results.extend(elements)
|
||||
return results
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def relating_types(cls, ifc_class=None):
|
||||
return [(str(e.id()), e.Name, e.Description or "") for e in cls.constr_class_entities(ifc_class=ifc_class)]
|
||||
|
||||
@classmethod
|
||||
def relating_types_browser(cls):
|
||||
return cls.relating_types(ifc_class=cls.props.ifc_class_browser)
|
||||
|
||||
@classmethod
|
||||
def new_constr_class_info(cls, ifc_class):
|
||||
if ifc_class not in cls.props.constr_classes:
|
||||
cls.props.constr_classes.add().name = ifc_class
|
||||
return cls.props.constr_classes[ifc_class]
|
||||
|
||||
@classmethod
|
||||
def assetize_constr_class(cls, ifc_class=None):
|
||||
selected_ifc_class = cls.props.ifc_class
|
||||
selected_relating_type_id = cls.props.relating_type_id
|
||||
if ifc_class is None:
|
||||
ifc_class = cls.props.ifc_class_browser
|
||||
if cls.constr_class_info(ifc_class) is None:
|
||||
cls.new_constr_class_info(ifc_class)
|
||||
constr_class_occurrences = cls.constr_class_entities(ifc_class)
|
||||
constr_classes = cls.props.constr_classes
|
||||
|
||||
for constr_class_entity in constr_class_occurrences:
|
||||
if (
|
||||
ifc_class not in constr_classes
|
||||
or constr_class_entity.Name not in constr_classes[ifc_class].constr_types
|
||||
):
|
||||
obj = tool.Ifc.get_object(constr_class_entity)
|
||||
cls.assetize_object(obj, ifc_class, constr_class_entity)
|
||||
cls.constr_class_info(ifc_class).fully_loaded = True
|
||||
cls.props.updating = True
|
||||
cls.props.ifc_class = selected_ifc_class
|
||||
cls.props.relating_type_id = selected_relating_type_id
|
||||
cls.props.updating = False
|
||||
|
||||
@classmethod
|
||||
def assetize_object(cls, obj, ifc_class, ifc_class_entity, from_selection=False):
|
||||
relating_type_id = ifc_class_entity.id()
|
||||
to_be_deleted = False
|
||||
if obj.type == "EMPTY":
|
||||
kwargs = {}
|
||||
if not from_selection:
|
||||
kwargs.update({"ifc_class": ifc_class, "relating_type_id": relating_type_id})
|
||||
new_obj = cls.new_relating_type(**kwargs)
|
||||
if new_obj is not None:
|
||||
to_be_deleted = True
|
||||
obj = new_obj
|
||||
obj.hide_set(True)
|
||||
obj.asset_mark()
|
||||
obj.asset_generate_preview()
|
||||
blender33_or_above = bpy.app.version >= (3, 3, 0)
|
||||
interval = 1e-4
|
||||
|
||||
def wait_for_asset_previews_generation(check_interval_seconds=interval):
|
||||
if blender33_or_above and bpy.app.is_job_running("RENDER_PREVIEW"):
|
||||
return check_interval_seconds
|
||||
else:
|
||||
if ifc_class not in cls.props.constr_classes:
|
||||
cls.props.constr_classes.add().name = ifc_class
|
||||
constr_class_info = cls.props.constr_classes[ifc_class]
|
||||
# relating_type = cls.relating_type_name_by_id(ifc_class, relating_type_id)
|
||||
if str(relating_type_id) not in constr_class_info.constr_types:
|
||||
constr_class_info.constr_types.add().name = str(relating_type_id)
|
||||
relating_type_info = constr_class_info.constr_types[str(relating_type_id)]
|
||||
relating_type_info.object = obj
|
||||
relating_type_info.icon_id = obj.preview.icon_id
|
||||
|
||||
if to_be_deleted:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element:
|
||||
tool.Ifc.delete(element)
|
||||
tool.Ifc.unlink(obj=obj)
|
||||
for collection in obj.users_collection:
|
||||
collection.objects.unlink(obj)
|
||||
return None
|
||||
|
||||
first_interval = 0 if blender33_or_above else interval
|
||||
bpy.app.timers.register(wait_for_asset_previews_generation, first_interval=first_interval)
|
||||
|
||||
@classmethod
|
||||
def assetize_relating_type_from_selection(cls, browser=False):
|
||||
ifc_class = cls.props.ifc_class_browser if browser else cls.props.ifc_class
|
||||
relating_type_id = cls.props.relating_type_id_browser if browser else cls.props.relating_type_id
|
||||
constr_class_occurrences = cls.constr_class_entities(ifc_class=ifc_class)
|
||||
constr_class_occurrences = [
|
||||
entity for entity in constr_class_occurrences if entity.id() == int(relating_type_id)
|
||||
]
|
||||
if len(constr_class_occurrences) == 0:
|
||||
raise ConstrTypeEntityNotFound()
|
||||
constr_class_entity = constr_class_occurrences[0]
|
||||
if (obj := tool.Ifc.get_object(constr_class_entity)) is None:
|
||||
raise ConstrTypeEntityNotFound()
|
||||
cls.assetize_object(obj, ifc_class, constr_class_entity, from_selection=True)
|
||||
|
||||
@staticmethod
|
||||
def constr_class_info(ifc_class):
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
return props.constr_classes[ifc_class] if ifc_class in props.constr_classes else None
|
||||
|
||||
@classmethod
|
||||
def new_relating_type(cls, ifc_class=None, relating_type_id=None):
|
||||
if ifc_class is None:
|
||||
bpy.ops.bim.add_constr_type_instance(
|
||||
ifc_class=cls.props.ifc_class, relating_type_id=int(cls.props.relating_type_id), link_to_scene=True
|
||||
)
|
||||
else:
|
||||
cls.props.updating = True
|
||||
cls.props.ifc_class = ifc_class
|
||||
cls.props.relating_type_id = str(relating_type_id)
|
||||
cls.props.updating = False
|
||||
bpy.ops.bim.add_constr_type_instance(link_to_scene=True)
|
||||
return bpy.context.selected_objects[-1]
|
||||
|
||||
@staticmethod
|
||||
def relating_type_name_by_id(ifc_class, relating_type_id):
|
||||
file = IfcStore.get_file()
|
||||
try:
|
||||
constr_class_entity = file.by_id(int(relating_type_id))
|
||||
except (RuntimeError, ValueError):
|
||||
return None
|
||||
return constr_class_entity.Name if constr_class_entity.is_a() == ifc_class else None
|
||||
|
||||
@classmethod
|
||||
def relating_type_id_by_name(cls, ifc_class, relating_type):
|
||||
relating_types = [ct[0] for ct in cls.relating_types(ifc_class=ifc_class) if ct[1] == relating_type]
|
||||
return None if len(relating_types) == 0 else relating_types[0]
|
||||
|
||||
@@ -56,14 +56,9 @@ def load_post(*args):
|
||||
product.regenerate_profile_usage,
|
||||
)
|
||||
|
||||
IfcStore.add_element_listener(opening.element_listener)
|
||||
IfcStore.add_element_listener(wall.element_listener)
|
||||
IfcStore.add_element_listener(slab.element_listener)
|
||||
IfcStore.add_element_listener(profile.element_listener)
|
||||
|
||||
ifcopenshell.api.add_post_listener(
|
||||
"geometry.add_representation", "BlenderBIM.DumbWall.GenerateAxis", wall.generate_axis
|
||||
)
|
||||
ifcopenshell.api.add_post_listener(
|
||||
"geometry.add_representation", "BlenderBIM.DumbWall.CalculateQuantities", wall.calculate_quantities
|
||||
)
|
||||
@@ -74,9 +69,6 @@ def load_post(*args):
|
||||
"type.assign_type", "BlenderBIM.DumbWall.RegenerateFromType", wall.DumbWallPlaner().regenerate_from_type
|
||||
)
|
||||
|
||||
ifcopenshell.api.add_post_listener(
|
||||
"geometry.add_representation", "BlenderBIM.DumbSlab.GenerateFootprint", slab.generate_footprint
|
||||
)
|
||||
ifcopenshell.api.add_post_listener(
|
||||
"geometry.add_representation", "BlenderBIM.DumbSlab.CalculateQuantities", slab.calculate_quantities
|
||||
)
|
||||
@@ -87,11 +79,6 @@ def load_post(*args):
|
||||
"type.assign_type", "BlenderBIM.DumbSlab.RegenerateFromType", slab.DumbSlabPlaner().regenerate_from_type
|
||||
)
|
||||
|
||||
ifcopenshell.api.add_pre_listener(
|
||||
"material.edit_profile",
|
||||
"BlenderBIM.DumbProfile.SyncObjectFromProfile",
|
||||
profile.DumbProfileRegenerator().sync_object_from_profile,
|
||||
)
|
||||
ifcopenshell.api.add_post_listener(
|
||||
"material.edit_profile",
|
||||
"BlenderBIM.DumbProfile.RegenerateFromProfile",
|
||||
|
||||
@@ -38,7 +38,7 @@ class MepGenerator:
|
||||
def __init__(self, relating_type):
|
||||
self.relating_type = relating_type
|
||||
|
||||
def generate(self):
|
||||
def generate(self, link_to_scene=True):
|
||||
self.file = tool.Ifc.get()
|
||||
self.collection = bpy.context.view_layer.active_layer_collection.collection
|
||||
|
||||
@@ -54,26 +54,25 @@ class MepGenerator:
|
||||
self.width = dimensions.get("NominalDiameterOrWidth")
|
||||
self.height = dimensions.get("NominalHeight")
|
||||
self.length = 1
|
||||
self.rotation = 0
|
||||
|
||||
return self.derive_from_cursor()
|
||||
return self.derive_from_cursor(link_to_scene=link_to_scene)
|
||||
elif self.relating_type.is_a("IfcPipeSegmentType"):
|
||||
pass
|
||||
|
||||
def derive_from_cursor(self):
|
||||
def derive_from_cursor(self, link_to_scene):
|
||||
self.location = bpy.context.scene.cursor.location
|
||||
return self.create_rectangle_segment()
|
||||
return self.create_rectangle_segment(link_to_scene)
|
||||
|
||||
def create_rectangle_segment(self):
|
||||
def create_rectangle_segment(self, link_to_scene):
|
||||
verts = [
|
||||
Vector((0, self.width, 0)),
|
||||
Vector((0, 0, 0)),
|
||||
Vector((0, self.width, self.height)),
|
||||
Vector((0, 0, self.height)),
|
||||
Vector((self.length, self.width, 0)),
|
||||
Vector((self.length, 0, 0)),
|
||||
Vector((self.length, self.width, self.height)),
|
||||
Vector((self.length, 0, self.height)),
|
||||
Vector((-self.width / 2, self.height / 2, 0)),
|
||||
Vector((-self.width / 2, -self.height / 2, 0)),
|
||||
Vector((-self.width / 2, self.height / 2, self.length)),
|
||||
Vector((-self.width / 2, -self.height / 2, self.length)),
|
||||
Vector((self.width / 2, self.height / 2, 0)),
|
||||
Vector((self.width / 2, -self.height / 2, 0)),
|
||||
Vector((self.width / 2, self.height / 2, self.length)),
|
||||
Vector((self.width / 2, -self.height / 2, self.length)),
|
||||
]
|
||||
faces = [
|
||||
[1, 3, 2, 0],
|
||||
@@ -90,9 +89,11 @@ class MepGenerator:
|
||||
ifc_class = ifc_classes[0]
|
||||
|
||||
obj = bpy.data.objects.new(tool.Model.generate_occurrence_name(self.relating_type, ifc_class), mesh)
|
||||
obj.location = self.location
|
||||
obj.rotation_euler[2] = self.rotation
|
||||
self.collection.objects.link(obj)
|
||||
if link_to_scene:
|
||||
obj.location = self.location
|
||||
obj.rotation_euler[0] = math.pi / 2
|
||||
obj.rotation_euler[2] = math.pi / 2
|
||||
self.collection.objects.link(obj)
|
||||
|
||||
bpy.ops.bim.assign_class(
|
||||
obj=obj.name,
|
||||
@@ -100,9 +101,27 @@ class MepGenerator:
|
||||
ifc_representation_class="IfcExtrudedAreaSolid/IfcRectangleProfileDef",
|
||||
)
|
||||
|
||||
blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=tool.Ifc.get_entity(obj), type=self.relating_type)
|
||||
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=self.relating_type)
|
||||
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric")
|
||||
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.Mep"})
|
||||
obj.select_set(True)
|
||||
|
||||
start_port_matrix = Matrix()
|
||||
end_port_matrix = Matrix.Translation((0, 0, self.length))
|
||||
|
||||
for mat in [start_port_matrix, end_port_matrix]:
|
||||
port = tool.Ifc.run("root.create_entity", ifc_class="IfcDistributionPort")
|
||||
tool.Ifc.run("system.assign_port", element=element, port=port)
|
||||
tool.Ifc.run("geometry.edit_object_placement", product=port, matrix=obj.matrix_world @ mat, is_si=True)
|
||||
|
||||
try:
|
||||
obj.select_set(True)
|
||||
except RuntimeError:
|
||||
def msg(self, context):
|
||||
txt = "The created object could not be assigned to a collection. "
|
||||
txt += "Has any IfcSpatialElement been deleted?"
|
||||
self.layout.label(text=txt)
|
||||
|
||||
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
|
||||
return obj
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import gpu
|
||||
import bgl
|
||||
import bmesh
|
||||
import blenderbim.bim.handler
|
||||
import ifcopenshell
|
||||
@@ -25,54 +27,13 @@ import blenderbim.tool as tool
|
||||
import blenderbim.core.geometry
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from math import pi
|
||||
from mathutils import Vector
|
||||
from mathutils import Vector, Matrix
|
||||
from bpy.types import Operator
|
||||
from bpy.types import SpaceView3D
|
||||
from bpy.props import FloatProperty
|
||||
from bpy_extras.object_utils import AddObjectHelper, object_data_add
|
||||
|
||||
|
||||
def element_listener(element, obj):
|
||||
blenderbim.bim.handler.subscribe_to(obj, "mode", mode_callback)
|
||||
|
||||
|
||||
def mode_callback(obj, data):
|
||||
for obj in set(bpy.context.selected_objects + [bpy.context.active_object]):
|
||||
if (
|
||||
obj.mode != "EDIT"
|
||||
or not obj.data
|
||||
or not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve, bpy.types.TextCurve))
|
||||
or not obj.BIMObjectProperties.ifc_definition_id
|
||||
or not bpy.context.scene.BIMProjectProperties.is_authoring
|
||||
):
|
||||
return
|
||||
product = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
if not product.is_a("IfcOpeningElement"):
|
||||
return
|
||||
for rel in product.VoidsElements:
|
||||
building_element_obj = IfcStore.get_element(rel.RelatingBuildingElement.id())
|
||||
if not building_element_obj:
|
||||
continue
|
||||
if [m for m in building_element_obj.modifiers if m.type == "BOOLEAN"]:
|
||||
continue
|
||||
representation = ifcopenshell.util.representation.get_representation(
|
||||
rel.RelatingBuildingElement, "Model", "Body", "MODEL_VIEW"
|
||||
)
|
||||
if not representation:
|
||||
continue
|
||||
blenderbim.core.geometry.switch_representation(
|
||||
tool.Geometry,
|
||||
obj=building_element_obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
enable_dynamic_voids=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
IfcStore.edited_objs.add(obj)
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
bmesh.update_edit_mesh(obj.data)
|
||||
bm.free()
|
||||
from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
|
||||
class AddElementOpening(bpy.types.Operator):
|
||||
@@ -201,31 +162,448 @@ class AddElementOpening(bpy.types.Operator):
|
||||
return obj
|
||||
|
||||
|
||||
def add_object(self, context):
|
||||
bm = bmesh.new()
|
||||
bmesh.ops.create_cube(bm, size=self.size)
|
||||
bm.verts.ensure_lookup_table()
|
||||
bm.edges.ensure_lookup_table()
|
||||
bm.faces.ensure_lookup_table()
|
||||
mesh = bpy.data.meshes.new(name="Dumb Opening")
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
obj = object_data_add(context, mesh, operator=self)
|
||||
obj.name = "Opening"
|
||||
obj.display_type = "WIRE"
|
||||
|
||||
|
||||
class BIM_OT_add_object(Operator, AddObjectHelper):
|
||||
bl_idname = "mesh.add_opening"
|
||||
bl_label = "Dumb Opening"
|
||||
class AddPotentialOpening(Operator, AddObjectHelper):
|
||||
bl_idname = "bim.add_potential_opening"
|
||||
bl_label = "Add Potential Opening"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
x: FloatProperty(name="X", default=0.5)
|
||||
y: FloatProperty(name="Y", default=0.5)
|
||||
z: FloatProperty(name="Z", default=0.5)
|
||||
|
||||
size: FloatProperty(name="Size", default=2)
|
||||
def draw_settings(context, layout, tool):
|
||||
row = self.layout.row()
|
||||
row.prop(data=self, property="x", label="Size X")
|
||||
row = self.layout.row()
|
||||
row.prop(data=self, property="y")
|
||||
row = self.layout.row()
|
||||
row.prop(data=self, property="z")
|
||||
|
||||
def execute(self, context):
|
||||
add_object(self, context)
|
||||
props = context.scene.BIMModelProperties
|
||||
|
||||
new_matrix = None
|
||||
if context.selected_objects and context.active_object:
|
||||
new_matrix = context.active_object.matrix_world.copy()
|
||||
new_matrix.col[3] = context.scene.cursor.location.to_4d().to_4d()
|
||||
|
||||
x = self.x / 2
|
||||
y = self.y / 2
|
||||
z = self.z / 2
|
||||
verts = [
|
||||
Vector((-x, -y, -z)),
|
||||
Vector((-x, -y, z)),
|
||||
Vector((-x, y, -z)),
|
||||
Vector((-x, y, z)),
|
||||
Vector((x, -y, -z)),
|
||||
Vector((x, -y, z)),
|
||||
Vector((x, y, -z)),
|
||||
Vector((x, y, z)),
|
||||
]
|
||||
edges = []
|
||||
faces = [
|
||||
[0, 1, 3, 2],
|
||||
[2, 3, 7, 6],
|
||||
[6, 7, 5, 4],
|
||||
[4, 5, 1, 0],
|
||||
[2, 6, 4, 0],
|
||||
[7, 3, 1, 5],
|
||||
]
|
||||
mesh = bpy.data.meshes.new(name="Opening")
|
||||
mesh.from_pydata(verts, edges, faces)
|
||||
obj = object_data_add(context, mesh, operator=self)
|
||||
obj.name = "Opening"
|
||||
|
||||
if new_matrix:
|
||||
obj.matrix_world = new_matrix
|
||||
|
||||
tool.Model.clear_scene_openings()
|
||||
|
||||
new = props.openings.add()
|
||||
new.obj = obj
|
||||
|
||||
DecorationsHandler.install(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def add_object_button(self, context):
|
||||
self.layout.operator(BIM_OT_add_object.bl_idname, icon="PLUGIN")
|
||||
class AddPotentialHalfSpaceSolid(Operator, AddObjectHelper):
|
||||
bl_idname = "bim.add_potential_half_space_solid"
|
||||
bl_label = "Add Potential Half Space Solid"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMModelProperties
|
||||
bm = bmesh.new()
|
||||
bmesh.ops.create_grid(bm, size=0.5)
|
||||
bm.verts.ensure_lookup_table()
|
||||
bm.edges.ensure_lookup_table()
|
||||
bm.faces.ensure_lookup_table()
|
||||
mesh = bpy.data.meshes.new(name="Dumb Opening")
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
obj = object_data_add(context, mesh, operator=self)
|
||||
obj.name = "HalfSpaceSolid"
|
||||
|
||||
tool.Model.clear_scene_openings()
|
||||
|
||||
new = props.openings.add()
|
||||
new.obj = obj
|
||||
|
||||
DecorationsHandler.install(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddBoolean(Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_boolean"
|
||||
bl_label = "Add Boolean"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMModelProperties
|
||||
if len(context.selected_objects) != 2:
|
||||
return {"FINISHED"}
|
||||
obj1, obj2 = context.selected_objects
|
||||
element1 = tool.Ifc.get_entity(obj1)
|
||||
element2 = tool.Ifc.get_entity(obj2)
|
||||
if element1 and element2:
|
||||
return {"FINISHED"}
|
||||
if not element1 and not element2:
|
||||
return {"FINISHED"}
|
||||
if element2 and not element1:
|
||||
obj1, obj2 = obj2, obj1
|
||||
if not obj1.data or not hasattr(obj1.data, "BIMMeshProperties"):
|
||||
return {"FINISHED"}
|
||||
representation = tool.Ifc.get().by_id(obj1.data.BIMMeshProperties.ifc_definition_id)
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"geometry.add_boolean",
|
||||
tool.Ifc.get(),
|
||||
representation=representation,
|
||||
operator="DIFFERENCE",
|
||||
matrix=obj1.matrix_world.inverted() @ obj2.matrix_world,
|
||||
)
|
||||
|
||||
tool.Model.clear_scene_openings()
|
||||
|
||||
blenderbim.core.geometry.switch_representation(
|
||||
tool.Geometry,
|
||||
obj=obj1,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
enable_dynamic_voids=False,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
|
||||
tool.Ifc.unlink(obj=obj2)
|
||||
bpy.data.objects.remove(obj2)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ShowBooleans(Operator, tool.Ifc.Operator, AddObjectHelper):
|
||||
bl_idname = "bim.show_booleans"
|
||||
bl_label = "Show Booleans"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
if (
|
||||
not obj.data
|
||||
or not hasattr(obj.data, "BIMMeshProperties")
|
||||
or not obj.data.BIMMeshProperties.ifc_definition_id
|
||||
):
|
||||
return {"FINISHED"}
|
||||
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
|
||||
booleans = []
|
||||
for item in representation.Items:
|
||||
booleans.extend(self.get_booleans(item))
|
||||
for boolean in booleans:
|
||||
if boolean.is_a() == "IfcHalfSpaceSolid":
|
||||
if boolean.BaseSurface.is_a("IfcPlane"):
|
||||
boolean_obj = self.create_half_space_solid()
|
||||
position = boolean.BaseSurface.Position
|
||||
position = Matrix(ifcopenshell.util.placement.get_axis2placement(position).tolist())
|
||||
boolean_obj.matrix_world = obj.matrix_world @ position
|
||||
boolean_obj.data.BIMMeshProperties.ifc_boolean_id = boolean.id()
|
||||
boolean_obj.data.BIMMeshProperties.obj = obj
|
||||
return {"FINISHED"}
|
||||
|
||||
def get_booleans(self, item):
|
||||
results = []
|
||||
if item.is_a("IfcBooleanResult"):
|
||||
results.extend(self.get_booleans(item.FirstOperand))
|
||||
results.append(item.SecondOperand)
|
||||
return results
|
||||
|
||||
def create_half_space_solid(self):
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
bm = bmesh.new()
|
||||
bmesh.ops.create_grid(bm, size=0.5)
|
||||
bm.verts.ensure_lookup_table()
|
||||
bm.edges.ensure_lookup_table()
|
||||
bm.faces.ensure_lookup_table()
|
||||
mesh = bpy.data.meshes.new(name="Dumb Opening")
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
obj = object_data_add(bpy.context, mesh, operator=self)
|
||||
obj.name = "HalfSpaceSolid"
|
||||
|
||||
tool.Model.clear_scene_openings()
|
||||
|
||||
new = props.openings.add()
|
||||
new.obj = obj
|
||||
|
||||
DecorationsHandler.install(bpy.context)
|
||||
return obj
|
||||
|
||||
|
||||
class HideBooleans(Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.hide_booleans"
|
||||
bl_label = "Hide Booleans"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
for opening in props.openings:
|
||||
obj = opening.obj
|
||||
if obj.data.BIMMeshProperties.ifc_boolean_id:
|
||||
bpy.data.objects.remove(obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemoveBooleans(Operator, tool.Ifc.Operator, AddObjectHelper):
|
||||
bl_idname = "bim.remove_booleans"
|
||||
bl_label = "Remove Booleans"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
for obj in context.selected_objects:
|
||||
if (
|
||||
not obj.data
|
||||
or not hasattr(obj.data, "BIMMeshProperties")
|
||||
or not obj.data.BIMMeshProperties.ifc_boolean_id
|
||||
):
|
||||
continue
|
||||
try:
|
||||
boolean = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_boolean_id)
|
||||
except:
|
||||
continue
|
||||
ifcopenshell.api.run("geometry.remove_boolean", tool.Ifc.get(), item=boolean)
|
||||
if obj.data.BIMMeshProperties.obj:
|
||||
upstream_obj = obj.data.BIMMeshProperties.obj
|
||||
element = tool.Ifc.get_entity(upstream_obj)
|
||||
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if body:
|
||||
blenderbim.core.geometry.switch_representation(
|
||||
tool.Geometry,
|
||||
obj=upstream_obj,
|
||||
representation=body,
|
||||
should_reload=True,
|
||||
enable_dynamic_voids=False,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
bpy.data.objects.remove(obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ShowOpenings(Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.show_openings"
|
||||
bl_label = "Show Openings"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
if tool.Ifc.is_moved(obj):
|
||||
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
openings = tool.Model.load_openings(element, [r.RelatedOpeningElement for r in element.HasOpenings])
|
||||
for opening in openings:
|
||||
new = props.openings.add()
|
||||
new.obj = opening
|
||||
DecorationsHandler.install(bpy.context)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class HideOpenings(Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.hide_openings"
|
||||
bl_label = "Hide Openings"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
to_delete = set()
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
openings = [r.RelatedOpeningElement for r in element.HasOpenings]
|
||||
for opening in openings:
|
||||
opening_obj = tool.Ifc.get_object(opening)
|
||||
if opening_obj:
|
||||
to_delete.add(opening_obj)
|
||||
for opening_obj in to_delete:
|
||||
tool.Ifc.unlink(element=opening, obj=opening_obj)
|
||||
bpy.data.objects.remove(opening_obj)
|
||||
tool.Model.clear_scene_openings()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EditOpenings(Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.edit_openings"
|
||||
bl_label = "Edit Openings"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
openings = [r.RelatedOpeningElement for r in element.HasOpenings]
|
||||
for opening in openings:
|
||||
opening_obj = tool.Ifc.get_object(opening)
|
||||
if opening_obj:
|
||||
tool.Geometry.run_geometry_update_representation(obj=opening_obj)
|
||||
tool.Ifc.unlink(element=opening, obj=opening_obj)
|
||||
bpy.data.objects.remove(opening_obj)
|
||||
|
||||
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
blenderbim.core.geometry.switch_representation(
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=body,
|
||||
should_reload=True,
|
||||
enable_dynamic_voids=False,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DecorationsHandler:
|
||||
installed = None
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(cls.installed, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.installed = None
|
||||
|
||||
def __call__(self, context):
|
||||
bgl.glLineWidth(2)
|
||||
bgl.glPointSize(6)
|
||||
bgl.glEnable(bgl.GL_BLEND)
|
||||
bgl.glEnable(bgl.GL_LINE_SMOOTH)
|
||||
|
||||
for opening in context.scene.BIMModelProperties.openings:
|
||||
obj = opening.obj
|
||||
|
||||
if not obj:
|
||||
continue
|
||||
|
||||
white = (1, 1, 1, 1)
|
||||
white_t = (1, 1, 1, 0.1)
|
||||
green = (0.545, 0.863, 0, 1)
|
||||
red = (1, 0.2, 0.322, 1)
|
||||
red_t = (1, 0.2, 0.322, 0.1)
|
||||
blue = (0.157, 0.565, 1, 1)
|
||||
blue_t = (0.157, 0.565, 1, 0.1)
|
||||
grey = (0.2, 0.2, 0.2, 1)
|
||||
|
||||
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
|
||||
|
||||
verts = []
|
||||
selected_edges = []
|
||||
unselected_edges = []
|
||||
selected_vertices = []
|
||||
unselected_vertices = []
|
||||
|
||||
if obj.mode == "EDIT":
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
|
||||
for vertex in bm.verts:
|
||||
co = tuple(obj.matrix_world @ vertex.co)
|
||||
verts.append(co)
|
||||
if vertex.hide:
|
||||
continue
|
||||
|
||||
if vertex.select:
|
||||
selected_vertices.append(co)
|
||||
else:
|
||||
unselected_vertices.append(co)
|
||||
|
||||
for edge in bm.edges:
|
||||
edge_indices = [v.index for v in edge.verts]
|
||||
if edge.hide:
|
||||
continue
|
||||
if edge.select:
|
||||
selected_edges.append(edge_indices)
|
||||
else:
|
||||
unselected_edges.append(edge_indices)
|
||||
|
||||
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=unselected_edges)
|
||||
self.shader.bind()
|
||||
self.shader.uniform_float("color", white)
|
||||
batch.draw(self.shader)
|
||||
|
||||
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=selected_edges)
|
||||
self.shader.uniform_float("color", green)
|
||||
batch.draw(self.shader)
|
||||
|
||||
batch = batch_for_shader(self.shader, "POINTS", {"pos": unselected_vertices})
|
||||
self.shader.uniform_float("color", white)
|
||||
batch.draw(self.shader)
|
||||
|
||||
batch = batch_for_shader(self.shader, "POINTS", {"pos": selected_vertices})
|
||||
self.shader.uniform_float("color", green)
|
||||
batch.draw(self.shader)
|
||||
else:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
|
||||
verts = [tuple(obj.matrix_world @ v.co) for v in bm.verts]
|
||||
edges = [tuple([v.index for v in e.verts]) for e in bm.edges]
|
||||
|
||||
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges)
|
||||
self.shader.bind()
|
||||
self.shader.uniform_float("color", green if obj in context.selected_objects else blue)
|
||||
batch.draw(self.shader)
|
||||
|
||||
obj.data.calc_loop_triangles()
|
||||
tris = [tuple(t.vertices) for t in obj.data.loop_triangles]
|
||||
|
||||
batch = batch_for_shader(self.shader, "TRIS", {"pos": verts}, indices=tris)
|
||||
self.shader.bind()
|
||||
self.shader.uniform_float("color", blue_t)
|
||||
batch.draw(self.shader)
|
||||
|
||||
if "HalfSpaceSolid" in obj.name:
|
||||
# Arrow shape
|
||||
verts = [
|
||||
tuple(obj.matrix_world @ Vector((0, 0, 0))),
|
||||
tuple(obj.matrix_world @ Vector((0, 0, 0.5))),
|
||||
tuple(obj.matrix_world @ Vector((0.05, 0, 0.45))),
|
||||
tuple(obj.matrix_world @ Vector((-0.05, 0, 0.45))),
|
||||
tuple(obj.matrix_world @ Vector((0, 0.05, 0.45))),
|
||||
tuple(obj.matrix_world @ Vector((0, -0.05, 0.45))),
|
||||
]
|
||||
edges = [(0, 1), (1, 2), (1, 3), (1, 4), (1, 5)]
|
||||
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges)
|
||||
self.shader.bind()
|
||||
self.shader.uniform_float("color", green if obj in context.selected_objects else blue)
|
||||
batch.draw(self.shader)
|
||||
|
||||
if obj.mode != "EDIT":
|
||||
bm.free()
|
||||
|
||||
@@ -18,18 +18,24 @@
|
||||
|
||||
import bpy
|
||||
import mathutils
|
||||
from functools import reduce
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.system
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.representation
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.type
|
||||
import blenderbim.core.geometry
|
||||
from . import wall, slab, profile, mep
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.model.data import AuthoringData
|
||||
from blenderbim.bim.module.model.prop import store_cursor_position
|
||||
from ifcopenshell.api.pset.data import Data as PsetData
|
||||
from mathutils import Vector, Matrix
|
||||
from bpy_extras.object_utils import AddObjectHelper
|
||||
from . import prop
|
||||
|
||||
|
||||
class AddEmptyType(bpy.types.Operator, AddObjectHelper):
|
||||
@@ -51,13 +57,18 @@ def add_empty_type_button(self, context):
|
||||
self.layout.operator(AddEmptyType.bl_idname, icon="FILE_3D")
|
||||
|
||||
|
||||
class AddTypeInstance(bpy.types.Operator):
|
||||
bl_idname = "bim.add_type_instance"
|
||||
bl_label = "Add Type Instance"
|
||||
class AddConstrTypeInstance(bpy.types.Operator):
|
||||
bl_idname = "bim.add_constr_type_instance"
|
||||
bl_label = "Add"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Add the selected Type Instance to the model"
|
||||
bl_description = "Add Type Instance to the model"
|
||||
ifc_class: bpy.props.StringProperty()
|
||||
relating_type: bpy.props.IntProperty()
|
||||
relating_type_id: bpy.props.IntProperty()
|
||||
from_invoke: bpy.props.BoolProperty(default=False)
|
||||
link_to_scene: bpy.props.BoolProperty(default=True)
|
||||
|
||||
def invoke(self, context, event):
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
@@ -65,29 +76,34 @@ class AddTypeInstance(bpy.types.Operator):
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMModelProperties
|
||||
ifc_class = self.ifc_class or props.ifc_class
|
||||
relating_type_id = self.relating_type or props.relating_type
|
||||
relating_type_id = self.relating_type_id or props.relating_type_id
|
||||
|
||||
if not ifc_class or not relating_type_id:
|
||||
return {"FINISHED"}
|
||||
|
||||
if self.from_invoke:
|
||||
props.ifc_class = self.ifc_class
|
||||
props.relating_type_id = str(self.relating_type_id)
|
||||
|
||||
self.file = IfcStore.get_file()
|
||||
instance_class = ifcopenshell.util.type.get_applicable_entities(ifc_class, self.file.schema)[0]
|
||||
relating_type = self.file.by_id(int(relating_type_id))
|
||||
material = ifcopenshell.util.element.get_material(relating_type)
|
||||
|
||||
if material and material.is_a("IfcMaterialProfileSet"):
|
||||
if profile.DumbProfileGenerator(relating_type).generate():
|
||||
if profile.DumbProfileGenerator(relating_type).generate(link_to_scene=self.link_to_scene):
|
||||
return {"FINISHED"}
|
||||
elif material and material.is_a("IfcMaterialLayerSet"):
|
||||
if self.generate_layered_element(ifc_class, relating_type):
|
||||
if self.generate_layered_element(ifc_class, relating_type, link_to_scene=self.link_to_scene):
|
||||
return {"FINISHED"}
|
||||
if relating_type.is_a("IfcFlowSegmentType"):
|
||||
if mep.MepGenerator(relating_type).generate():
|
||||
if relating_type.is_a("IfcFlowSegmentType") and not relating_type.RepresentationMaps:
|
||||
if mep.MepGenerator(relating_type).generate(link_to_scene=self.link_to_scene):
|
||||
return {"FINISHED"}
|
||||
|
||||
building_obj = None
|
||||
if len(context.selected_objects) == 1 and context.active_object:
|
||||
building_obj = context.active_object
|
||||
building_element = tool.Ifc.get_entity(building_obj)
|
||||
|
||||
# A cube
|
||||
verts = [
|
||||
@@ -112,36 +128,52 @@ class AddTypeInstance(bpy.types.Operator):
|
||||
mesh = bpy.data.meshes.new(name="Instance")
|
||||
mesh.from_pydata(verts, edges, faces)
|
||||
obj = bpy.data.objects.new(tool.Model.generate_occurrence_name(relating_type, instance_class), mesh)
|
||||
obj.location = context.scene.cursor.location
|
||||
collection = context.view_layer.active_layer_collection.collection
|
||||
collection.objects.link(obj)
|
||||
collection_obj = bpy.data.objects.get(collection.name)
|
||||
if self.link_to_scene:
|
||||
obj.location = context.scene.cursor.location
|
||||
collection = context.view_layer.active_layer_collection.collection
|
||||
collection.objects.link(obj)
|
||||
collection_obj = bpy.data.objects.get(collection.name)
|
||||
bpy.ops.bim.assign_class(obj=obj.name, ifc_class=instance_class)
|
||||
blenderbim.core.type.assign_type(
|
||||
tool.Ifc,
|
||||
tool.Type,
|
||||
element=tool.Ifc.get_entity(obj),
|
||||
type=tool.Ifc.get().by_id(int(props.relating_type)),
|
||||
)
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type)
|
||||
if self.link_to_scene:
|
||||
# Update required as core.type.assign_type may change obj.data
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
if building_obj:
|
||||
if (
|
||||
building_obj
|
||||
and building_element
|
||||
and building_element.is_a() in ["IfcWall", "IfcWallStandardCase", "IfcCovering"]
|
||||
):
|
||||
if instance_class in ["IfcWindow", "IfcDoor"]:
|
||||
# TODO For now we are hardcoding windows and doors as a prototype
|
||||
bpy.ops.bim.add_element_opening(
|
||||
voided_building_element=building_obj.name, filling_building_element=obj.name
|
||||
)
|
||||
if instance_class == "IfcDoor":
|
||||
if instance_class == "IfcDoor" and self.link_to_scene:
|
||||
obj.location[2] = building_obj.location[2] - min([v[2] for v in obj.bound_box])
|
||||
else:
|
||||
elif self.link_to_scene:
|
||||
if collection_obj and collection_obj.BIMObjectProperties.ifc_definition_id:
|
||||
obj.location[2] = collection_obj.location[2] - min([v[2] for v in obj.bound_box])
|
||||
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
for port in ifcopenshell.util.system.get_ports(relating_type):
|
||||
mat = ifcopenshell.util.placement.get_local_placement(port.ObjectPlacement)
|
||||
mat[0][3] *= unit_scale
|
||||
mat[1][3] *= unit_scale
|
||||
mat[2][3] *= unit_scale
|
||||
mat = obj.matrix_world @ mathutils.Matrix(mat)
|
||||
new_port = tool.Ifc.run("root.create_entity", ifc_class="IfcDistributionPort")
|
||||
tool.Ifc.run("system.assign_port", element=element, port=new_port)
|
||||
tool.Ifc.run("geometry.edit_object_placement", product=new_port, matrix=mat, is_si=True)
|
||||
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
obj.select_set(True)
|
||||
context.view_layer.objects.active = obj
|
||||
return {"FINISHED"}
|
||||
|
||||
def generate_layered_element(self, ifc_class, relating_type):
|
||||
@staticmethod
|
||||
def generate_layered_element(ifc_class, relating_type, link_to_scene=True):
|
||||
layer_set_direction = None
|
||||
|
||||
parametric = ifcopenshell.util.element.get_psets(relating_type).get("EPset_Parametric")
|
||||
@@ -154,15 +186,75 @@ class AddTypeInstance(bpy.types.Operator):
|
||||
layer_set_direction = "AXIS2"
|
||||
|
||||
if layer_set_direction == "AXIS3":
|
||||
if slab.DumbSlabGenerator(relating_type).generate():
|
||||
if slab.DumbSlabGenerator(relating_type).generate(link_to_scene=link_to_scene):
|
||||
return True
|
||||
elif layer_set_direction == "AXIS2":
|
||||
if wall.DumbWallGenerator(relating_type).generate():
|
||||
if wall.DumbWallGenerator(relating_type).generate(link_to_scene=link_to_scene):
|
||||
return True
|
||||
else:
|
||||
pass # Dumb block generator? Eh? :)
|
||||
|
||||
|
||||
class DisplayConstrTypes(bpy.types.Operator):
|
||||
bl_idname = "bim.display_constr_types"
|
||||
bl_label = "Browse Construction Types"
|
||||
bl_options = {"REGISTER"}
|
||||
bl_description = "Display all available Construction Types to add new instances"
|
||||
|
||||
def invoke(self, context, event):
|
||||
if not AuthoringData.is_loaded:
|
||||
AuthoringData.load()
|
||||
props = context.scene.BIMModelProperties
|
||||
ifc_class = props.ifc_class
|
||||
constr_class_info = AuthoringData.constr_class_info(ifc_class)
|
||||
if constr_class_info is None or not constr_class_info.fully_loaded:
|
||||
AuthoringData.assetize_constr_class(ifc_class)
|
||||
bpy.ops.bim.display_constr_types_ui("INVOKE_DEFAULT")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ReinvokeOperator(bpy.types.Operator):
|
||||
bl_idname = "bim.reinvoke_operator"
|
||||
bl_label = "Reinvoke Popup Operator"
|
||||
bl_options = {"REGISTER"}
|
||||
bl_description = "Reinvoke a popup operator"
|
||||
operator: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
browser_state = context.scene.BIMModelProperties.constr_browser_state
|
||||
store_cursor_position(context, event, window=False)
|
||||
cursor_x, cursor_y = browser_state.cursor_x, browser_state.cursor_y
|
||||
window_x, window_y = browser_state.window_x, browser_state.window_y
|
||||
window = context.window
|
||||
operator = self.operator
|
||||
self.move_cursor_away(context, window)
|
||||
|
||||
def move_cursor_to_window():
|
||||
window.cursor_warp(window_x, window_y)
|
||||
|
||||
def run_operator(operator, *args, **kwargs):
|
||||
reduce(lambda x, arg: getattr(x, arg), operator.split("."), bpy.ops)(*args, **kwargs)
|
||||
|
||||
def reinvoke():
|
||||
run_operator(operator, "INVOKE_DEFAULT", reinvoked=True)
|
||||
window.cursor_warp(cursor_x, cursor_y)
|
||||
|
||||
bpy.app.timers.register(move_cursor_to_window, first_interval=browser_state.update_delay)
|
||||
bpy.app.timers.register(reinvoke, first_interval=3*browser_state.update_delay)
|
||||
return {"FINISHED"}
|
||||
|
||||
def move_cursor_away(self, context, window): # closes current popup
|
||||
browser_state = context.scene.BIMModelProperties.constr_browser_state
|
||||
window.cursor_warp(browser_state.far_away_x, browser_state.far_away_y)
|
||||
|
||||
@staticmethod
|
||||
def run_operator(operator, *args, **kwargs):
|
||||
reduce(lambda x, arg: getattr(x, arg), operator.split("."), bpy.ops)(*args, **kwargs)
|
||||
|
||||
|
||||
class AlignProduct(bpy.types.Operator):
|
||||
bl_idname = "bim.align_product"
|
||||
bl_label = "Align Product"
|
||||
@@ -331,10 +423,13 @@ def ensure_material_assigned(usecase_path, ifc_file, settings):
|
||||
if om is not None and om.BIMObjectProperties.ifc_definition_id
|
||||
]
|
||||
|
||||
if material[0].id() in object_material_ids:
|
||||
if material and material[0].id() in object_material_ids:
|
||||
continue
|
||||
|
||||
if len(obj.data.materials) == 1:
|
||||
obj.data.materials.clear()
|
||||
|
||||
if not material:
|
||||
continue
|
||||
|
||||
obj.data.materials.append(IfcStore.get_element(material[0].id()))
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import copy
|
||||
import math
|
||||
import bmesh
|
||||
import mathutils.geometry
|
||||
@@ -96,92 +97,114 @@ class DumbProfileGenerator:
|
||||
def __init__(self, relating_type):
|
||||
self.relating_type = relating_type
|
||||
|
||||
def generate(self):
|
||||
def generate(self, link_to_scene=True):
|
||||
self.file = IfcStore.get_file()
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file())
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file())
|
||||
material = ifcopenshell.util.element.get_material(self.relating_type)
|
||||
if material and material.is_a("IfcMaterialProfileSet"):
|
||||
self.profile_set = material
|
||||
else:
|
||||
return
|
||||
|
||||
self.body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
||||
self.axis_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Axis", "GRAPH_VIEW")
|
||||
self.collection = bpy.context.view_layer.active_layer_collection.collection
|
||||
self.collection_obj = bpy.data.objects.get(self.collection.name)
|
||||
self.length = 3
|
||||
self.depth = 3
|
||||
self.rotation = 0
|
||||
self.location = Vector((0, 0, 0))
|
||||
return self.derive_from_cursor()
|
||||
self.cardinal_point = int(bpy.context.scene.BIMModelProperties.cardinal_point)
|
||||
return self.derive_from_cursor(link_to_scene=link_to_scene)
|
||||
|
||||
def derive_from_cursor(self):
|
||||
def derive_from_cursor(self, link_to_scene):
|
||||
self.location = bpy.context.scene.cursor.location
|
||||
return self.create_profile()
|
||||
|
||||
def create_profile(self):
|
||||
# A cube
|
||||
verts = [
|
||||
Vector((-1, -1, -1)),
|
||||
Vector((-1, -1, 1)),
|
||||
Vector((-1, 1, -1)),
|
||||
Vector((-1, 1, 1)),
|
||||
Vector((1, -1, -1)),
|
||||
Vector((1, -1, 1)),
|
||||
Vector((1, 1, -1)),
|
||||
Vector((1, 1, 1)),
|
||||
]
|
||||
edges = []
|
||||
faces = [
|
||||
[0, 2, 3, 1],
|
||||
[2, 3, 7, 6],
|
||||
[4, 5, 7, 6],
|
||||
[0, 1, 5, 4],
|
||||
[1, 3, 7, 5],
|
||||
[0, 2, 6, 4],
|
||||
]
|
||||
return self.create_profile(link_to_scene)
|
||||
|
||||
def create_profile(self, link_to_scene):
|
||||
ifc_classes = ifcopenshell.util.type.get_applicable_entities(self.relating_type.is_a(), self.file.schema)
|
||||
# Standard cases are deprecated, so let's cull them
|
||||
ifc_class = [c for c in ifc_classes if "StandardCase" not in c][0]
|
||||
|
||||
mesh = bpy.data.meshes.new(name="Dumb Profile")
|
||||
mesh.from_pydata(verts, edges, faces)
|
||||
mesh = bpy.data.meshes.new("Dummy")
|
||||
obj = bpy.data.objects.new(tool.Model.generate_occurrence_name(self.relating_type, ifc_class), mesh)
|
||||
obj.location = self.location
|
||||
if self.collection_obj and self.collection_obj.BIMObjectProperties.ifc_definition_id:
|
||||
if link_to_scene and self.collection_obj and self.collection_obj.BIMObjectProperties.ifc_definition_id:
|
||||
obj.location[2] = self.collection_obj.location[2]
|
||||
self.collection.objects.link(obj)
|
||||
bpy.context.view_layer.update()
|
||||
if link_to_scene:
|
||||
self.collection.objects.link(obj)
|
||||
|
||||
bpy.ops.bim.assign_class(obj=obj.name, ifc_class=ifc_class, should_add_representation=False)
|
||||
element = blenderbim.core.root.assign_class(
|
||||
tool.Ifc,
|
||||
tool.Collector,
|
||||
tool.Root,
|
||||
obj=obj,
|
||||
ifc_class=ifc_class,
|
||||
should_add_representation=False,
|
||||
context=self.body_context,
|
||||
)
|
||||
ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=self.relating_type)
|
||||
material = ifcopenshell.util.element.get_material(element)
|
||||
material.CardinalPoint = self.cardinal_point
|
||||
|
||||
if self.relating_type.is_a() in ["IfcBeamType", "IfcMemberType"]:
|
||||
obj.rotation_euler[0] = math.pi / 2
|
||||
obj.rotation_euler[2] = math.pi / 2
|
||||
|
||||
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=tool.Ifc.get_entity(obj), type=self.relating_type)
|
||||
profile_set_usage = ifcopenshell.util.element.get_material(element)
|
||||
if self.axis_context:
|
||||
representation = ifcopenshell.api.run(
|
||||
"geometry.add_axis_representation",
|
||||
tool.Ifc.get(),
|
||||
context=self.axis_context,
|
||||
axis=[(0.0, 0.0, 0.0), (0.0, 0.0, self.depth)],
|
||||
)
|
||||
ifcopenshell.api.run(
|
||||
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=representation
|
||||
)
|
||||
|
||||
representation = ifcopenshell.api.run(
|
||||
"geometry.add_profile_representation",
|
||||
tool.Ifc.get(),
|
||||
context=self.body_context,
|
||||
profile=self.profile_set.CompositeProfile or self.profile_set.MaterialProfiles[0].Profile,
|
||||
cardinal_point=self.cardinal_point,
|
||||
depth=self.depth,
|
||||
)
|
||||
ifcopenshell.api.run(
|
||||
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=representation
|
||||
)
|
||||
blenderbim.core.geometry.switch_representation(
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
enable_dynamic_voids=False,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
|
||||
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric")
|
||||
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbProfile"})
|
||||
MaterialData.load(self.file)
|
||||
obj.select_set(True)
|
||||
|
||||
if link_to_scene:
|
||||
obj.select_set(True)
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
class DumbProfileRegenerator:
|
||||
def regenerate_from_profile(self, usecase_path, ifc_file, settings):
|
||||
self.file = ifc_file
|
||||
objs = []
|
||||
profile = settings["profile"].Profile
|
||||
if not profile:
|
||||
return
|
||||
for element in self.get_elements_using_profile(profile):
|
||||
self.change_profile(element)
|
||||
|
||||
def sync_object_from_profile(self, usecase_path, ifc_file, settings):
|
||||
self.file = ifc_file
|
||||
profile = settings["profile"].Profile
|
||||
if not profile:
|
||||
return
|
||||
for element in self.get_elements_using_profile(profile):
|
||||
self.sync_object(element)
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj:
|
||||
objs.append(obj)
|
||||
DumbProfileRecalculator().recalculate(objs)
|
||||
|
||||
def get_elements_using_profile(self, profile):
|
||||
results = []
|
||||
@@ -202,162 +225,630 @@ class DumbProfileRegenerator:
|
||||
return results
|
||||
|
||||
def regenerate_from_type(self, usecase_path, ifc_file, settings):
|
||||
obj = tool.Ifc.get_object(settings["related_object"])
|
||||
if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id:
|
||||
return
|
||||
new_material = ifcopenshell.util.element.get_material(settings["relating_type"])
|
||||
if not new_material or not new_material.is_a("IfcMaterialProfileSet"):
|
||||
return
|
||||
self.change_profile(settings["related_object"])
|
||||
|
||||
def change_profile(self, element):
|
||||
obj = IfcStore.get_element(element.id())
|
||||
if not obj:
|
||||
return
|
||||
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if representation:
|
||||
blenderbim.core.geometry.switch_representation(
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
enable_dynamic_voids=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
|
||||
def sync_object(self, element):
|
||||
obj = IfcStore.get_element(element.id())
|
||||
if not obj or obj not in IfcStore.edited_objs:
|
||||
return
|
||||
bpy.ops.bim.update_representation(obj=obj.name)
|
||||
DumbProfileRecalculator().recalculate([obj])
|
||||
|
||||
|
||||
class ExtendProfile(bpy.types.Operator):
|
||||
class ExtendProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.extend_profile"
|
||||
bl_label = "Extend Profile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
join_type: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
def _execute(self, context):
|
||||
selected_objs = context.selected_objects
|
||||
for obj in selected_objs:
|
||||
bpy.ops.bim.dynamically_void_product(obj=obj.name)
|
||||
if len(selected_objs) == 0:
|
||||
joiner = DumbProfileJoiner()
|
||||
if not self.join_type:
|
||||
for obj in selected_objs:
|
||||
joiner.unjoin(obj)
|
||||
return {"FINISHED"}
|
||||
if not context.active_object:
|
||||
return {"FINISHED"}
|
||||
if len(selected_objs) == 1:
|
||||
DumbProfileExtender(context.active_object, target_coordinate=context.scene.cursor.location).extend()
|
||||
IfcStore.edited_objs.add(context.active_object)
|
||||
joiner.join_E(context.active_object, context.scene.cursor.location)
|
||||
return {"FINISHED"}
|
||||
if len(selected_objs) == 2:
|
||||
if self.join_type == "L":
|
||||
joiner.join_L([o for o in selected_objs if o != context.active_object][0], context.active_object)
|
||||
elif self.join_type == "V":
|
||||
joiner.join_V([o for o in selected_objs if o != context.active_object][0], context.active_object)
|
||||
if len(selected_objs) < 2:
|
||||
return {"FINISHED"}
|
||||
for obj in selected_objs:
|
||||
if obj == context.active_object:
|
||||
continue
|
||||
DumbProfileExtender(obj, context.active_object).extend()
|
||||
IfcStore.edited_objs.add(obj)
|
||||
if self.join_type == "T":
|
||||
for obj in selected_objs:
|
||||
if obj == context.active_object:
|
||||
continue
|
||||
joiner.join_T(obj, context.active_object)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DumbProfileExtender:
|
||||
# A profile is a prismatic extrusion along its local Z axis.
|
||||
def __init__(self, profile, target=None, target_coordinate=None):
|
||||
self.profile = profile
|
||||
self.target = target
|
||||
self.target_coordinate = target_coordinate
|
||||
class DumbProfileJoiner:
|
||||
def __init__(self):
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
self.axis_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Axis", "GRAPH_VIEW")
|
||||
self.body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
||||
|
||||
# An extension of a profile is determined by casting a ray from the cardinal
|
||||
# point of either extreme along the profiles local Z axis to a target
|
||||
# object. The nearest result from this raycast will be the target extension
|
||||
# point.
|
||||
def extend(self):
|
||||
extension_data = self.get_closest_extension_point()
|
||||
if not extension_data:
|
||||
def unjoin(self, profile1):
|
||||
element1 = tool.Ifc.get_entity(profile1)
|
||||
if not element1:
|
||||
return
|
||||
if extension_data["end"] == "bottom":
|
||||
self.profile.matrix_world.translation = extension_data["contact"]
|
||||
if extension_data["direction"] == "up":
|
||||
self.shift_top_faces(-extension_data["distance"])
|
||||
elif extension_data["direction"] == "down":
|
||||
self.shift_top_faces(extension_data["distance"])
|
||||
if extension_data["end"] == "top":
|
||||
if extension_data["direction"] == "up":
|
||||
self.shift_top_faces(extension_data["distance"])
|
||||
elif extension_data["direction"] == "down":
|
||||
self.shift_top_faces(-extension_data["distance"])
|
||||
|
||||
def shift_top_faces(self, z):
|
||||
vertices = []
|
||||
for f in self.get_profile_top_faces():
|
||||
vertices.extend(f.vertices)
|
||||
for v in set(vertices):
|
||||
self.profile.data.vertices[v].co.z += z
|
||||
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type="ATSTART")
|
||||
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type="ATEND")
|
||||
|
||||
# An top face is defined as having at least one vertex on the max
|
||||
# Z-axis, and a non-insignificant Z component of its face normal
|
||||
def get_profile_top_faces(self):
|
||||
faces = []
|
||||
max_z = max([v[2] for v in self.profile.bound_box])
|
||||
for f in self.profile.data.polygons:
|
||||
if abs(f.normal.z) < 0.1:
|
||||
continue
|
||||
for v in f.vertices:
|
||||
if self.profile.data.vertices[v].co.z == max_z:
|
||||
faces.append(f)
|
||||
break
|
||||
return faces
|
||||
axis1 = self.get_profile_axis(profile1)
|
||||
axis = copy.deepcopy(axis1)
|
||||
body = copy.deepcopy(axis1)
|
||||
self.recreate_profile(element1, profile1, axis, body)
|
||||
|
||||
def get_closest_extension_point(self):
|
||||
top = self.profile.matrix_world @ Vector((0, 0, self.profile.dimensions[2]))
|
||||
bottom = self.profile.matrix_world.translation
|
||||
up = self.profile.matrix_world.to_quaternion() @ Vector((0, 0, 1))
|
||||
down = self.profile.matrix_world.to_quaternion() @ Vector((0, 0, -1))
|
||||
|
||||
if self.target:
|
||||
return self.get_closest_extension_point_from_target_obj(top, bottom, up, down)
|
||||
elif self.target_coordinate:
|
||||
return self.get_closest_extension_point_from_target_coordinate(top, bottom, up, down)
|
||||
|
||||
def get_closest_extension_point_from_target_coordinate(self, top, bottom, up, down):
|
||||
point = mathutils.geometry.intersect_line_plane(
|
||||
bottom, top, self.target_coordinate, self.profile.matrix_world.to_quaternion() @ Vector((0, 0, 1))
|
||||
)
|
||||
if not point:
|
||||
def join_E(self, profile1, target):
|
||||
element1 = tool.Ifc.get_entity(profile1)
|
||||
if not element1:
|
||||
return
|
||||
top_distance = (point - top).length
|
||||
bottom_distance = (point - bottom).length
|
||||
if top_distance < bottom_distance:
|
||||
intersection_point, signed_distance = mathutils.geometry.intersect_point_line(point, top, bottom)
|
||||
if signed_distance > 0:
|
||||
return {"contact": point, "distance": top_distance, "end": "top", "direction": "down"}
|
||||
else:
|
||||
return {"contact": point, "distance": top_distance, "end": "top", "direction": "up"}
|
||||
axis1 = self.get_profile_axis(profile1)
|
||||
intersect, connection = mathutils.geometry.intersect_point_line(target, *axis1)
|
||||
connection = "ATEND" if connection > 0.5 else "ATSTART"
|
||||
|
||||
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type=connection)
|
||||
|
||||
axis = copy.deepcopy(axis1)
|
||||
body = copy.deepcopy(axis1)
|
||||
axis[1 if connection == "ATEND" else 0] = intersect
|
||||
body[1 if connection == "ATEND" else 0] = intersect
|
||||
self.recreate_profile(element1, profile1, axis, body)
|
||||
|
||||
def set_depth(self, profile1, length):
|
||||
element1 = tool.Ifc.get_entity(profile1)
|
||||
if not element1:
|
||||
return
|
||||
|
||||
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type="ATEND")
|
||||
|
||||
axis1 = self.get_profile_axis(profile1)
|
||||
axis = copy.deepcopy(axis1)
|
||||
body = copy.deepcopy(axis1)
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
si_length = unit_scale * length
|
||||
end = profile1.matrix_world @ Vector((si_length, 0, 0))
|
||||
axis[1] = end
|
||||
body[1] = end
|
||||
self.recreate_profile(element1, profile1, axis, body)
|
||||
|
||||
def join_T(self, profile1, profile2):
|
||||
element1 = tool.Ifc.get_entity(profile1)
|
||||
element2 = tool.Ifc.get_entity(profile2)
|
||||
axis1 = self.get_profile_axis(profile1)
|
||||
axis2 = self.get_profile_axis(profile2)
|
||||
intersect = tool.Cad.intersect_edges(axis1, axis2)
|
||||
if intersect:
|
||||
intersect, _ = intersect
|
||||
else:
|
||||
intersection_point, signed_distance = mathutils.geometry.intersect_point_line(point, bottom, top)
|
||||
if signed_distance > 0:
|
||||
return {"contact": point, "distance": bottom_distance, "end": "bottom", "direction": "up"}
|
||||
return
|
||||
connection = "ATEND" if tool.Cad.edge_percent(intersect, axis1) > 0.5 else "ATSTART"
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"geometry.connect_path",
|
||||
tool.Ifc.get(),
|
||||
related_element=element1,
|
||||
relating_element=element2,
|
||||
relating_connection="ATPATH",
|
||||
related_connection=connection,
|
||||
description="BUTT",
|
||||
)
|
||||
|
||||
self.recreate_profile(element1, profile1, axis1, axis1)
|
||||
|
||||
def join_V(self, profile1, profile2):
|
||||
element1 = tool.Ifc.get_entity(profile1)
|
||||
element2 = tool.Ifc.get_entity(profile2)
|
||||
axis1 = self.get_profile_axis(profile1)
|
||||
axis2 = self.get_profile_axis(profile2)
|
||||
intersect = tool.Cad.intersect_edges(axis1, axis2)
|
||||
if intersect:
|
||||
intersect, _ = intersect
|
||||
else:
|
||||
return
|
||||
profile1_end = "ATEND" if tool.Cad.edge_percent(intersect, axis1) > 0.5 else "ATSTART"
|
||||
profile2_end = "ATEND" if tool.Cad.edge_percent(intersect, axis2) > 0.5 else "ATSTART"
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"geometry.connect_path",
|
||||
tool.Ifc.get(),
|
||||
relating_element=element1,
|
||||
related_element=element2,
|
||||
relating_connection=profile1_end,
|
||||
related_connection=profile2_end,
|
||||
description="MITRE",
|
||||
)
|
||||
|
||||
self.recreate_profile(element1, profile1, axis1, axis1)
|
||||
self.recreate_profile(element2, profile2, axis2, axis2)
|
||||
|
||||
def join_L(self, profile1, profile2):
|
||||
element1 = tool.Ifc.get_entity(profile1)
|
||||
element2 = tool.Ifc.get_entity(profile2)
|
||||
axis1 = self.get_profile_axis(profile1)
|
||||
axis2 = self.get_profile_axis(profile2)
|
||||
intersect = tool.Cad.intersect_edges(axis1, axis2)
|
||||
if intersect:
|
||||
intersect, _ = intersect
|
||||
else:
|
||||
return
|
||||
profile1_end = "ATEND" if tool.Cad.edge_percent(intersect, axis1) > 0.5 else "ATSTART"
|
||||
profile2_end = "ATEND" if tool.Cad.edge_percent(intersect, axis2) > 0.5 else "ATSTART"
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"geometry.connect_path",
|
||||
tool.Ifc.get(),
|
||||
relating_element=element1,
|
||||
related_element=element2,
|
||||
relating_connection=profile1_end,
|
||||
related_connection=profile2_end,
|
||||
description="BUTT",
|
||||
)
|
||||
|
||||
self.recreate_profile(element1, profile1, axis1, axis1)
|
||||
self.recreate_profile(element2, profile2, axis2, axis2)
|
||||
|
||||
def recreate_profile(self, element, obj, axis=None, body=None):
|
||||
if axis is None or body is None:
|
||||
axis = body = self.get_profile_axis(obj)
|
||||
self.axis = copy.deepcopy(axis)
|
||||
self.body = copy.deepcopy(body)
|
||||
material = ifcopenshell.util.element.get_material(element, should_skip_usage=False)
|
||||
usage = None
|
||||
if material.is_a("IfcMaterialProfileSetUsage"):
|
||||
usage = material
|
||||
material = material.ForProfileSet
|
||||
self.profile = material.CompositeProfile or material.MaterialProfiles[0].Profile
|
||||
self.clippings = []
|
||||
|
||||
for rel in element.ConnectedTo:
|
||||
connection = rel.RelatingConnectionType
|
||||
other = tool.Ifc.get_object(rel.RelatedElement)
|
||||
if connection not in ["ATPATH", "NOTDEFINED"]:
|
||||
self.join(
|
||||
obj, other, connection, rel.RelatedConnectionType, is_relating=True, description=rel.Description
|
||||
)
|
||||
for rel in element.ConnectedFrom:
|
||||
connection = rel.RelatedConnectionType
|
||||
other = tool.Ifc.get_object(rel.RelatingElement)
|
||||
if connection not in ["ATPATH", "NOTDEFINED"]:
|
||||
self.join(
|
||||
obj, other, connection, rel.RelatingConnectionType, is_relating=False, description=rel.Description
|
||||
)
|
||||
|
||||
new_matrix = copy.deepcopy(obj.matrix_world)
|
||||
new_matrix.col[3] = self.body[0].to_4d()
|
||||
new_matrix.invert()
|
||||
self.clippings = [new_matrix @ c for c in self.clippings]
|
||||
|
||||
depth = (self.body[1] - self.body[0]).length
|
||||
|
||||
if self.axis_context:
|
||||
axis = [(new_matrix @ a) for a in self.axis]
|
||||
new_axis = ifcopenshell.api.run(
|
||||
"geometry.add_axis_representation", tool.Ifc.get(), context=self.axis_context, axis=axis
|
||||
)
|
||||
old_axis = ifcopenshell.util.representation.get_representation(element, "Model", "Axis", "GRAPH_VIEW")
|
||||
if old_axis:
|
||||
for inverse in tool.Ifc.get().get_inverse(old_axis):
|
||||
ifcopenshell.util.element.replace_attribute(inverse, old_axis, new_axis)
|
||||
blenderbim.core.geometry.remove_representation(
|
||||
tool.Ifc, tool.Geometry, obj=obj, representation=old_axis
|
||||
)
|
||||
else:
|
||||
return {"contact": point, "distance": bottom_distance, "end": "bottom", "direction": "down"}
|
||||
ifcopenshell.api.run(
|
||||
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=new_axis
|
||||
)
|
||||
|
||||
def get_closest_extension_point_from_target_obj(self, top, bottom, up, down):
|
||||
t_top = self.target.matrix_world.inverted() @ top
|
||||
t_bottom = self.target.matrix_world.inverted() @ bottom
|
||||
t_up = self.target.matrix_world.inverted().to_quaternion() @ up
|
||||
t_down = self.target.matrix_world.inverted().to_quaternion() @ down
|
||||
new_body = ifcopenshell.api.run(
|
||||
"geometry.add_profile_representation",
|
||||
tool.Ifc.get(),
|
||||
context=self.body_context,
|
||||
profile=self.profile,
|
||||
depth=depth,
|
||||
cardinal_point=usage.CardinalPoint if usage else None,
|
||||
clippings=self.clippings,
|
||||
)
|
||||
|
||||
results = []
|
||||
results.append((self.target.ray_cast(t_top, t_up, distance=50), top, "top", "up"))
|
||||
results.append((self.target.ray_cast(t_top, t_down, distance=50), top, "top", "down"))
|
||||
results.append((self.target.ray_cast(t_bottom, t_up, distance=50), bottom, "bottom", "up"))
|
||||
results.append((self.target.ray_cast(t_bottom, t_down, distance=50), bottom, "bottom", "down"))
|
||||
old_body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if old_body:
|
||||
for inverse in tool.Ifc.get().get_inverse(old_body):
|
||||
ifcopenshell.util.element.replace_attribute(inverse, old_body, new_body)
|
||||
obj.data.BIMMeshProperties.ifc_definition_id = int(new_body.id())
|
||||
obj.data.name = f"{self.body_context.id()}/{new_body.id()}"
|
||||
blenderbim.core.geometry.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_body)
|
||||
else:
|
||||
ifcopenshell.api.run(
|
||||
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=new_body
|
||||
)
|
||||
|
||||
min_distance = float(inf)
|
||||
final = None
|
||||
obj.location[0], obj.location[1], obj.location[2] = self.body[0]
|
||||
bpy.context.view_layer.update()
|
||||
if tool.Ifc.is_moved(obj):
|
||||
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
blenderbim.core.geometry.switch_representation(
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=new_body,
|
||||
should_reload=True,
|
||||
enable_dynamic_voids=False,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
tool.Geometry.record_object_materials(obj)
|
||||
|
||||
for result in results:
|
||||
if result[0][0]:
|
||||
contact = self.target.matrix_world @ result[0][1]
|
||||
distance = (contact - result[1]).length
|
||||
if distance < min_distance:
|
||||
min_distance = distance
|
||||
final = {"contact": contact, "distance": distance, "end": result[2], "direction": result[3]}
|
||||
def join(self, profile1, profile2, connection1, connection2, is_relating=True, description="BUTT"):
|
||||
element1 = tool.Ifc.get_entity(profile1)
|
||||
element2 = tool.Ifc.get_entity(profile2)
|
||||
axis1 = self.get_profile_axis(profile1)
|
||||
axis2 = self.get_profile_axis(profile2)
|
||||
|
||||
return final
|
||||
angle = tool.Cad.angle_edges(axis1, axis2, signed=False, degrees=True)
|
||||
if tool.Cad.is_x(angle, (0, 180)):
|
||||
return False
|
||||
|
||||
intersect, _ = tool.Cad.intersect_edges(axis1, axis2)
|
||||
|
||||
proposed_axis = [self.axis[0], intersect] if connection1 == "ATEND" else [intersect, self.axis[1]]
|
||||
|
||||
if tool.Cad.is_x(tool.Cad.angle_edges(self.axis, proposed_axis, degrees=True), 180):
|
||||
# The user has moved the wall into an invalid position that cannot connect at the desired end
|
||||
return False
|
||||
|
||||
self.axis[1 if connection1 == "ATEND" else 0] = intersect
|
||||
|
||||
# Work out body extents
|
||||
|
||||
if connection1 == "ATEND":
|
||||
axisl = (profile2.matrix_world.inverted() @ axis1[1]) - (profile2.matrix_world.inverted() @ axis1[0])
|
||||
elif connection1 == "ATSTART":
|
||||
axisl = (profile2.matrix_world.inverted() @ axis1[0]) - (profile2.matrix_world.inverted() @ axis1[1])
|
||||
xy_angle = math.degrees(Vector((1, 0)).angle_signed(axisl.normalized().to_2d()))
|
||||
if xy_angle >= -135 and xy_angle <= -45:
|
||||
closest_plane = "bottom"
|
||||
furthest_plane = "top"
|
||||
elif xy_angle >= 45 and xy_angle <= 135:
|
||||
closest_plane = "top"
|
||||
furthest_plane = "bottom"
|
||||
elif xy_angle >= -45 and xy_angle <= 45:
|
||||
closest_plane = "left"
|
||||
furthest_plane = "right"
|
||||
else:
|
||||
closest_plane = "right"
|
||||
furthest_plane = "left"
|
||||
|
||||
is_orthogonal = tool.Cad.is_x(tool.Cad.angle_edges(axis1, axis2, degrees=True), 90, tolerance=0.001)
|
||||
|
||||
if description == "MITRE":
|
||||
# Mitre joints are an unofficial convention
|
||||
|
||||
# Check the closest plane from the perspective of the other element
|
||||
if connection2 == "ATEND":
|
||||
axisl = (profile1.matrix_world.inverted() @ axis2[1]) - (profile1.matrix_world.inverted() @ axis2[0])
|
||||
elif connection2 == "ATSTART":
|
||||
axisl = (profile1.matrix_world.inverted() @ axis2[0]) - (profile1.matrix_world.inverted() @ axis2[1])
|
||||
xy_angle2 = math.degrees(Vector((1, 0)).angle_signed(axisl.normalized().to_2d()))
|
||||
if xy_angle2 >= -135 and xy_angle2 <= -45:
|
||||
closest_plane2 = "bottom"
|
||||
furthest_plane2 = "top"
|
||||
elif xy_angle2 >= 45 and xy_angle2 <= 135:
|
||||
closest_plane2 = "top"
|
||||
furthest_plane2 = "bottom"
|
||||
elif xy_angle2 >= -45 and xy_angle2 <= 45:
|
||||
closest_plane2 = "left"
|
||||
furthest_plane2 = "right"
|
||||
else:
|
||||
closest_plane2 = "right"
|
||||
furthest_plane2 = "left"
|
||||
|
||||
if connection1 == "ATEND":
|
||||
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
||||
plane = self.get_profile_plane(profile2, furthest_plane)
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()
|
||||
)
|
||||
self.body[1] = intersect
|
||||
else:
|
||||
plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False)
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()
|
||||
)
|
||||
max_dim = self.get_max_bound_box_dimension(profile1)
|
||||
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
||||
|
||||
# Mitre clip
|
||||
if connection1 == connection2:
|
||||
plane1 = self.get_profile_plane(profile1, furthest_plane2)
|
||||
plane2 = self.get_profile_plane(profile2, furthest_plane)
|
||||
clip1, direction1 = mathutils.geometry.intersect_plane_plane(
|
||||
plane1.col[3].to_3d(), plane1.col[2].to_3d(), plane2.col[3].to_3d(), plane2.col[2].to_3d()
|
||||
)
|
||||
|
||||
plane1 = self.get_profile_plane(profile1, closest_plane2)
|
||||
plane2 = self.get_profile_plane(profile2, closest_plane)
|
||||
clip2, direction2 = mathutils.geometry.intersect_plane_plane(
|
||||
plane1.col[3].to_3d(), plane1.col[2].to_3d(), plane2.col[3].to_3d(), plane2.col[2].to_3d()
|
||||
)
|
||||
else:
|
||||
plane1 = self.get_profile_plane(profile1, furthest_plane2)
|
||||
plane2 = self.get_profile_plane(profile2, furthest_plane)
|
||||
clip1, direction1 = mathutils.geometry.intersect_plane_plane(
|
||||
plane1.col[3].to_3d(), plane1.col[2].to_3d(), plane2.col[3].to_3d(), plane2.col[2].to_3d()
|
||||
)
|
||||
|
||||
plane1 = self.get_profile_plane(profile1, closest_plane2)
|
||||
plane2 = self.get_profile_plane(profile2, closest_plane)
|
||||
clip2, direction2 = mathutils.geometry.intersect_plane_plane(
|
||||
plane1.col[3].to_3d(), plane1.col[2].to_3d(), plane2.col[3].to_3d(), plane2.col[2].to_3d()
|
||||
)
|
||||
|
||||
y_axis = direction2
|
||||
x_axis = (clip2 - clip1).normalized().to_3d()
|
||||
z_axis = x_axis.cross(y_axis)
|
||||
self.clippings.append(self.create_matrix(clip1, x_axis, y_axis, z_axis))
|
||||
elif connection1 == "ATSTART":
|
||||
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
||||
plane = self.get_profile_plane(profile2, furthest_plane)
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()
|
||||
)
|
||||
self.body[0] = intersect
|
||||
else:
|
||||
plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False)
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()
|
||||
)
|
||||
max_dim = self.get_max_bound_box_dimension(profile1)
|
||||
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
||||
|
||||
if connection1 == connection2:
|
||||
plane1 = self.get_profile_plane(profile1, furthest_plane2)
|
||||
plane2 = self.get_profile_plane(profile2, furthest_plane)
|
||||
clip1, direction1 = mathutils.geometry.intersect_plane_plane(
|
||||
plane1.col[3].to_3d(), plane1.col[2].to_3d(), plane2.col[3].to_3d(), plane2.col[2].to_3d()
|
||||
)
|
||||
|
||||
plane1 = self.get_profile_plane(profile1, closest_plane2)
|
||||
plane2 = self.get_profile_plane(profile2, closest_plane)
|
||||
clip2, direction2 = mathutils.geometry.intersect_plane_plane(
|
||||
plane1.col[3].to_3d(), plane1.col[2].to_3d(), plane2.col[3].to_3d(), plane2.col[2].to_3d()
|
||||
)
|
||||
else:
|
||||
plane1 = self.get_profile_plane(profile1, furthest_plane2)
|
||||
plane2 = self.get_profile_plane(profile2, furthest_plane)
|
||||
clip1, direction1 = mathutils.geometry.intersect_plane_plane(
|
||||
plane1.col[3].to_3d(), plane1.col[2].to_3d(), plane2.col[3].to_3d(), plane2.col[2].to_3d()
|
||||
)
|
||||
|
||||
plane1 = self.get_profile_plane(profile1, closest_plane2)
|
||||
plane2 = self.get_profile_plane(profile2, closest_plane)
|
||||
clip2, direction2 = mathutils.geometry.intersect_plane_plane(
|
||||
plane1.col[3].to_3d(), plane1.col[2].to_3d(), plane2.col[3].to_3d(), plane2.col[2].to_3d()
|
||||
)
|
||||
|
||||
y_axis = direction2
|
||||
x_axis = (clip2 - clip1).normalized().to_3d()
|
||||
z_axis = x_axis.cross(y_axis)
|
||||
self.clippings.append(self.create_matrix(clip1, x_axis, y_axis, z_axis))
|
||||
|
||||
else:
|
||||
# This is the standard L and T joints described by IFC
|
||||
if connection1 == "ATEND":
|
||||
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
||||
plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane)
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()
|
||||
)
|
||||
self.body[1] = intersect
|
||||
else:
|
||||
plane = self.get_profile_plane(
|
||||
profile2,
|
||||
furthest_plane if is_relating else closest_plane,
|
||||
z_inwards=False if is_relating else True,
|
||||
)
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()
|
||||
)
|
||||
max_dim = self.get_max_bound_box_dimension(profile1)
|
||||
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
||||
self.clippings.append(plane)
|
||||
elif connection1 == "ATSTART":
|
||||
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
||||
plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane)
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()
|
||||
)
|
||||
self.body[0] = intersect
|
||||
else:
|
||||
plane = self.get_profile_plane(
|
||||
profile2,
|
||||
furthest_plane if is_relating else closest_plane,
|
||||
z_inwards=False if is_relating else True,
|
||||
)
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()
|
||||
)
|
||||
max_dim = self.get_max_bound_box_dimension(profile1)
|
||||
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
||||
self.clippings.append(plane)
|
||||
|
||||
def get_max_bound_box_dimension(self, obj):
|
||||
x = [v[0] for v in obj.bound_box]
|
||||
y = [v[1] for v in obj.bound_box]
|
||||
x_dim = max(x) - min(x)
|
||||
y_dim = max(y) - min(y)
|
||||
return x_dim if x_dim > y_dim else y_dim
|
||||
|
||||
def get_profile_plane(self, obj, plane, z_inwards=True):
|
||||
# Get a matrix for one of the bounding planes of the profile to be used as cutting plane
|
||||
# Here's an example of looking at the end of an I-Beam profile with a +Z extrusion out of the screen
|
||||
# top
|
||||
# +-----+ Y
|
||||
# |=====| ^
|
||||
# | | | |
|
||||
# left | | | right Z->X
|
||||
# | | |
|
||||
# |=====|
|
||||
# +-----+
|
||||
# bottom
|
||||
if plane == "top":
|
||||
max_y = max([v[1] for v in obj.bound_box])
|
||||
p = obj.matrix_world @ Vector((0, max_y, 0))
|
||||
x_axis = obj.matrix_world.to_quaternion() @ Vector((0, 0, 1))
|
||||
if z_inwards:
|
||||
y_axis = obj.matrix_world.to_quaternion() @ Vector((-1, 0, 0))
|
||||
z_axis = obj.matrix_world.to_quaternion() @ Vector((0, -1, 0))
|
||||
else:
|
||||
y_axis = obj.matrix_world.to_quaternion() @ Vector((1, 0, 0))
|
||||
z_axis = obj.matrix_world.to_quaternion() @ Vector((0, 1, 0))
|
||||
elif plane == "bottom":
|
||||
min_y = min([v[1] for v in obj.bound_box])
|
||||
p = obj.matrix_world @ Vector((0, min_y, 0))
|
||||
x_axis = obj.matrix_world.to_quaternion() @ Vector((0, 0, 1))
|
||||
if z_inwards:
|
||||
y_axis = obj.matrix_world.to_quaternion() @ Vector((1, 0, 0))
|
||||
z_axis = obj.matrix_world.to_quaternion() @ Vector((0, 1, 0))
|
||||
else:
|
||||
y_axis = obj.matrix_world.to_quaternion() @ Vector((-1, 0, 0))
|
||||
z_axis = obj.matrix_world.to_quaternion() @ Vector((0, -1, 0))
|
||||
elif plane == "right":
|
||||
max_x = max([v[0] for v in obj.bound_box])
|
||||
p = obj.matrix_world @ Vector((max_x, 0, 0))
|
||||
x_axis = obj.matrix_world.to_quaternion() @ Vector((0, 0, 1))
|
||||
if z_inwards:
|
||||
y_axis = obj.matrix_world.to_quaternion() @ Vector((0, 1, 0))
|
||||
z_axis = obj.matrix_world.to_quaternion() @ Vector((-1, 0, 0))
|
||||
else:
|
||||
y_axis = obj.matrix_world.to_quaternion() @ Vector((0, -1, 0))
|
||||
z_axis = obj.matrix_world.to_quaternion() @ Vector((1, 0, 0))
|
||||
elif plane == "left":
|
||||
min_x = min([v[0] for v in obj.bound_box])
|
||||
p = obj.matrix_world @ Vector((min_x, 0, 0))
|
||||
x_axis = obj.matrix_world.to_quaternion() @ Vector((0, 0, 1))
|
||||
if z_inwards:
|
||||
y_axis = obj.matrix_world.to_quaternion() @ Vector((0, -1, 0))
|
||||
z_axis = obj.matrix_world.to_quaternion() @ Vector((1, 0, 0))
|
||||
else:
|
||||
y_axis = obj.matrix_world.to_quaternion() @ Vector((0, 1, 0))
|
||||
z_axis = obj.matrix_world.to_quaternion() @ Vector((-1, 0, 0))
|
||||
return self.create_matrix(p, x_axis, y_axis, z_axis)
|
||||
|
||||
def create_matrix(self, p, x, y, z):
|
||||
return Matrix(
|
||||
(
|
||||
(x[0], y[0], z[0], p[0]),
|
||||
(x[1], y[1], z[1], p[1]),
|
||||
(x[2], y[2], z[2], p[2]),
|
||||
(0.0, 0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
|
||||
def get_profile_axis(self, obj):
|
||||
z_values = [v[2] for v in obj.bound_box]
|
||||
return [
|
||||
(obj.matrix_world @ Vector((0.0, 0.0, min(z_values)))),
|
||||
(obj.matrix_world @ Vector((0.0, 0.0, max(z_values)))),
|
||||
]
|
||||
|
||||
|
||||
class RecalculateProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.recalculate_profile"
|
||||
bl_label = "Recalculate Profile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
|
||||
def _execute(self, context):
|
||||
DumbProfileRecalculator().recalculate(context.selected_objects)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DumbProfileRecalculator:
|
||||
def recalculate(self, profiles):
|
||||
queue = set()
|
||||
for profile in profiles:
|
||||
element = tool.Ifc.get_entity(profile)
|
||||
queue.add((element, profile))
|
||||
for rel in getattr(element, "ConnectedTo", []):
|
||||
queue.add((rel.RelatedElement, tool.Ifc.get_object(rel.RelatedElement)))
|
||||
for rel in getattr(element, "ConnectedFrom", []):
|
||||
queue.add((rel.RelatingElement, tool.Ifc.get_object(rel.RelatingElement)))
|
||||
joiner = DumbProfileJoiner()
|
||||
for element, profile in queue:
|
||||
if element.is_a() in ("IfcColumn", "IfcBeam", "IfcMember") and profile:
|
||||
joiner.recreate_profile(element, profile)
|
||||
|
||||
|
||||
class ChangeProfileDepth(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.change_profile_depth"
|
||||
bl_label = "Change Profile Length"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
depth: bpy.props.FloatProperty()
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
|
||||
def _execute(self, context):
|
||||
joiner = DumbProfileJoiner()
|
||||
for obj in context.selected_objects:
|
||||
joiner.set_depth(obj, self.depth)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ChangeCardinalPoint(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.change_cardinal_point"
|
||||
bl_label = "Change Cardinal Point"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
cardinal_point: bpy.props.IntProperty()
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
|
||||
def _execute(self, context):
|
||||
objs = []
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
material = ifcopenshell.util.element.get_material(element, should_skip_usage=False)
|
||||
if material.is_a("IfcMaterialProfileSetUsage"):
|
||||
material.CardinalPoint = self.cardinal_point
|
||||
objs.append(obj)
|
||||
DumbProfileRecalculator().recalculate(objs)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class Rotate90(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.rotate_90"
|
||||
bl_label = "Rotate 90"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
axis: bpy.props.StringProperty()
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
|
||||
def _execute(self, context):
|
||||
objs = []
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element.ConnectedTo or element.ConnectedFrom:
|
||||
objs.append(obj)
|
||||
rotate_matrix = Matrix.Rotation(pi / 2, 4, self.axis)
|
||||
obj.matrix_world @= rotate_matrix
|
||||
bpy.context.view_layer.update()
|
||||
DumbProfileRecalculator().recalculate(objs)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -17,21 +17,10 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import ifcopenshell.util.type
|
||||
from blenderbim.bim.prop import ObjProperty
|
||||
from blenderbim.bim.module.model.data import AuthoringData
|
||||
from blenderbim.bim.prop import StrProperty, Attribute
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
StringProperty,
|
||||
EnumProperty,
|
||||
BoolProperty,
|
||||
IntProperty,
|
||||
FloatProperty,
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
|
||||
|
||||
def get_ifc_class(self, context):
|
||||
@@ -43,18 +32,174 @@ def get_ifc_class(self, context):
|
||||
def get_relating_type(self, context):
|
||||
if not AuthoringData.is_loaded:
|
||||
AuthoringData.load()
|
||||
return AuthoringData.data["relating_types"]
|
||||
return AuthoringData.data["relating_types_ids"]
|
||||
|
||||
|
||||
def get_relating_type_browser(self, context):
|
||||
if not AuthoringData.is_loaded:
|
||||
AuthoringData.load()
|
||||
return AuthoringData.data["relating_types_ids_browser"]
|
||||
|
||||
|
||||
def store_cursor_position(context, event, cursor=True, window=True):
|
||||
browser_state = context.scene.BIMModelProperties.constr_browser_state
|
||||
if cursor:
|
||||
browser_state.cursor_x, browser_state.cursor_y = event.mouse_x, event.mouse_y
|
||||
if window:
|
||||
browser_state.window_x, browser_state.window_y = event.mouse_x, event.mouse_y
|
||||
|
||||
|
||||
def update_icon_id(self, context, browser=False):
|
||||
if context == "lost_context" or (context.region is not None and context.region.type != "TOOL_HEADER"):
|
||||
ifc_class = self.ifc_class_browser if browser else self.ifc_class
|
||||
relating_type_id = self.relating_type_id_browser if browser else self.relating_type_id
|
||||
# relating_type = AuthoringData.relating_type_name_by_id(ifc_class, relating_type_id)
|
||||
|
||||
if ifc_class not in self.constr_classes or relating_type_id not in self.constr_classes[ifc_class].constr_types:
|
||||
try:
|
||||
AuthoringData.assetize_relating_type_from_selection(browser=browser)
|
||||
except ConstrTypeEntityNotFound:
|
||||
return
|
||||
|
||||
def set_icon(update_interval_seconds=1e-4):
|
||||
if (ifc_class not in self.constr_classes
|
||||
or relating_type_id not in self.constr_classes[ifc_class].constr_types):
|
||||
return update_interval_seconds
|
||||
else:
|
||||
self.icon_id = self.constr_classes[ifc_class].constr_types[relating_type_id].icon_id
|
||||
|
||||
bpy.app.timers.register(set_icon)
|
||||
|
||||
|
||||
def update_ifc_class(self, context):
|
||||
AuthoringData.is_loaded = False
|
||||
AuthoringData.load_ifc_classes()
|
||||
AuthoringData.load_relating_types()
|
||||
if not self.updating:
|
||||
update_icon_id(self, context)
|
||||
|
||||
|
||||
def update_ifc_class_browser(self, context):
|
||||
if context.region is not None and context.region.type != "TOOL_HEADER":
|
||||
AuthoringData.load_ifc_classes()
|
||||
AuthoringData.load_relating_types_browser()
|
||||
if self.updating:
|
||||
return
|
||||
ifc_class = self.ifc_class_browser
|
||||
constr_class_info = AuthoringData.constr_class_info(ifc_class)
|
||||
|
||||
if constr_class_info is None or not constr_class_info.fully_loaded:
|
||||
AuthoringData.assetize_constr_class(ifc_class)
|
||||
|
||||
|
||||
def update_relating_type(self, context):
|
||||
AuthoringData.load_relating_types()
|
||||
if not self.updating:
|
||||
update_icon_id(self, context)
|
||||
|
||||
|
||||
def update_relating_type_browser(self, context):
|
||||
AuthoringData.load_relating_types_browser()
|
||||
if not self.updating:
|
||||
update_icon_id(self, context, browser=True)
|
||||
|
||||
|
||||
def update_relating_type_by_name(self, context):
|
||||
AuthoringData.load_relating_types()
|
||||
relating_type_id = AuthoringData.relating_type_id_by_name(self.ifc_class, self.relating_type)
|
||||
if relating_type_id is not None:
|
||||
self.relating_type_id = relating_type_id
|
||||
|
||||
|
||||
def get_constr_class_info(props, ifc_class):
|
||||
return props.constr_classes[ifc_class] if ifc_class in props.constr_classes else None
|
||||
|
||||
|
||||
def update_preview_multiple(self, context):
|
||||
if context.region is not None and context.region.type != "TOOL_HEADER":
|
||||
if self.preview_multiple_constr_types:
|
||||
ifc_class = self.ifc_class
|
||||
constr_class_info = get_constr_class_info(self, ifc_class)
|
||||
if constr_class_info is None or not constr_class_info.fully_loaded:
|
||||
AuthoringData.assetize_constr_class(ifc_class)
|
||||
else:
|
||||
update_relating_type(self, context)
|
||||
|
||||
|
||||
class ConstrTypeInfo(PropertyGroup):
|
||||
name: bpy.props.StringProperty(name="Construction type ID")
|
||||
icon_id: bpy.props.IntProperty(name="Icon ID")
|
||||
object: bpy.props.PointerProperty(name="Object", type=bpy.types.Object)
|
||||
|
||||
|
||||
class ConstrClassInfo(PropertyGroup):
|
||||
name: bpy.props.StringProperty(name="Construction class")
|
||||
constr_types: bpy.props.CollectionProperty(type=ConstrTypeInfo)
|
||||
fully_loaded: bpy.props.BoolProperty(default=False)
|
||||
|
||||
|
||||
class ConstrBrowserState(PropertyGroup):
|
||||
cursor_x: bpy.props.IntProperty()
|
||||
cursor_y: bpy.props.IntProperty()
|
||||
window_x: bpy.props.IntProperty()
|
||||
window_y: bpy.props.IntProperty()
|
||||
far_away_x: bpy.props.IntProperty(default=10) # lower left corner to temporarily warp the mouse
|
||||
far_away_y: bpy.props.IntProperty(default=10) # useful to close popup operators
|
||||
updating: bpy.props.BoolProperty()
|
||||
update_delay: bpy.props.FloatProperty(default=3e-2)
|
||||
|
||||
|
||||
class BIMModelProperties(PropertyGroup):
|
||||
ifc_class: bpy.props.EnumProperty(items=get_ifc_class, name="IFC Class", update=update_ifc_class)
|
||||
relating_type: bpy.props.EnumProperty(items=get_relating_type, name="Relating Type")
|
||||
ifc_class: bpy.props.EnumProperty(items=get_ifc_class, name="Construction Class", update=update_ifc_class)
|
||||
ifc_class_browser: bpy.props.EnumProperty(
|
||||
items=get_ifc_class, name="Construction Class", update=update_ifc_class_browser
|
||||
)
|
||||
relating_type: bpy.props.StringProperty(update=update_relating_type_by_name)
|
||||
relating_type_id: bpy.props.EnumProperty(
|
||||
items=get_relating_type, name="Construction Type", update=update_relating_type
|
||||
)
|
||||
relating_type_id_browser: bpy.props.EnumProperty(
|
||||
items=get_relating_type_browser, name="Construction Type", update=update_relating_type_browser
|
||||
)
|
||||
icon_id: bpy.props.IntProperty()
|
||||
preview_multiple_constr_types: bpy.props.BoolProperty(default=False, update=update_preview_multiple)
|
||||
updating: bpy.props.BoolProperty(default=False)
|
||||
occurrence_name_style: bpy.props.EnumProperty(
|
||||
items=[("CLASS", "By Class", ""), ("TYPE", "By Type", ""), ("CUSTOM", "Custom", "")],
|
||||
name="Occurrence Name Style",
|
||||
)
|
||||
occurrence_name_function: bpy.props.StringProperty(name="Occurrence Name Function")
|
||||
getter_enum = {"ifc_class": get_ifc_class, "relating_type": get_relating_type}
|
||||
constr_classes: bpy.props.CollectionProperty(type=ConstrClassInfo)
|
||||
constr_browser_state: bpy.props.PointerProperty(type=ConstrBrowserState)
|
||||
extrusion_depth: bpy.props.FloatProperty(default=42.0)
|
||||
cardinal_point: bpy.props.EnumProperty(
|
||||
items=(
|
||||
# TODO: complain to buildingSMART
|
||||
("1", "bottom left", ""),
|
||||
("2", "bottom centre", ""),
|
||||
("3", "bottom right", ""),
|
||||
("4", "mid-depth left", ""),
|
||||
("5", "mid-depth centre", ""),
|
||||
("6", "mid-depth right", ""),
|
||||
("7", "top left", ""),
|
||||
("8", "top centre", ""),
|
||||
("9", "top right", ""),
|
||||
("10", "geometric centroid", ""),
|
||||
("11", "bottom in line with the geometric centroid", ""),
|
||||
("12", "left in line with the geometric centroid", ""),
|
||||
("13", "right in line with the geometric centroid", ""),
|
||||
("14", "top in line with the geometric centroid", ""),
|
||||
("15", "shear centre", ""),
|
||||
("16", "bottom in line with the shear centre", ""),
|
||||
("17", "left in line with the shear centre", ""),
|
||||
("18", "right in line with the shear centre", ""),
|
||||
("19", "top in line with the shear centre", ""),
|
||||
),
|
||||
name="Cardinal Point",
|
||||
default="5",
|
||||
)
|
||||
length: bpy.props.FloatProperty(default=42.0)
|
||||
openings: bpy.props.CollectionProperty(type=ObjProperty)
|
||||
x: bpy.props.FloatProperty(name="X", default=0.5)
|
||||
y: bpy.props.FloatProperty(name="Y", default=0.5)
|
||||
z: bpy.props.FloatProperty(name="Z", default=0.5)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user