mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-06 16:01:36 +00:00
Compare commits
643 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0844550d06 | |||
| 1d0b4675c0 | |||
| 497ebc55e7 | |||
| 97a501036b | |||
| 592c51b526 | |||
| 3dd7290ee1 | |||
| d7ac5f2441 | |||
| 461404cd45 | |||
| bfd5b1bede | |||
| 0d0b6cc6b2 | |||
| 57bc709b6f | |||
| b7b6e4aadf | |||
| 9c39ac0a48 | |||
| a77f1b9fd1 | |||
| 63921e3fd8 | |||
| 27d0b04db5 | |||
| 87283874a1 | |||
| 584d37de50 | |||
| ca2058d56b | |||
| 481676e51b | |||
| fe502577c9 | |||
| 65e8ca17d2 | |||
| 6bd7f02208 | |||
| 452d0f6023 | |||
| 07005a9d3b | |||
| 450ce689ba | |||
| 7c210721e6 | |||
| 7e48d712a2 | |||
| 9eedbc6f72 | |||
| 68b021b0e6 | |||
| a559b72e18 | |||
| 32496c40b3 | |||
| 1b249b1013 | |||
| d3fcf39c76 | |||
| 692ef5ecea | |||
| f56a97512a | |||
| 4f5ebef5da | |||
| 57a724e80f | |||
| 1b8652a789 | |||
| c1aa702896 | |||
| 5b43d2b75e | |||
| 3330e79556 | |||
| 88d09c83b7 | |||
| 5e6b96ee99 | |||
| bd07078def | |||
| 1d01d1f184 | |||
| 3cee0811d4 | |||
| a46228a57e | |||
| 158fe92644 | |||
| 12166ddbba | |||
| 36a0c22217 | |||
| 59cc9d4a23 | |||
| f3d918f76c | |||
| 613015e37e | |||
| be0eea1bed | |||
| 37bbc0b693 | |||
| 30093499fa | |||
| 8e95bf73f1 | |||
| 05f7aac853 | |||
| b135dd6994 | |||
| a5e7d7c4cc | |||
| 9680f6b29c | |||
| bc8dd3a3ab | |||
| 9caf47b41f | |||
| 5e1787b146 | |||
| 07147f4b7f | |||
| 4dfe15bd88 | |||
| 5896630630 | |||
| 48b1493c5c | |||
| ee75ea1182 | |||
| 1876a788a9 | |||
| 9d28628157 | |||
| 9b257b0a10 | |||
| db7900027e | |||
| 2af7470950 | |||
| e1c3c44fb3 | |||
| e7c422469a | |||
| 52d3bae543 | |||
| b130925c5f | |||
| 890665b77b | |||
| 99c95bc92d | |||
| 8647f9c357 | |||
| 300855276f | |||
| 1a8831012b | |||
| 1bc4e82b7d | |||
| fc197c1d32 | |||
| 33cfa9e2b1 | |||
| 9e9d664511 | |||
| 1b9acadd81 | |||
| c5f6d47c44 | |||
| d19b6d72ef | |||
| 919231b096 | |||
| cead5cc75d | |||
| 9396cb226f | |||
| 56a63cab6e | |||
| 449d13bd5f | |||
| 487a4f345d | |||
| d6daa64c48 | |||
| bf0aa247a0 | |||
| a7c8c908fe | |||
| 64686dde5c | |||
| 29ab446f8c | |||
| 51e2e7cd82 | |||
| e879dac4bd | |||
| 145c629d92 | |||
| 3fe9279bb4 | |||
| 428d88f01c | |||
| 3986d4e85b | |||
| 02d35b6047 | |||
| 3cc5931eae | |||
| db5ec77730 | |||
| bec46562f5 | |||
| 1508c4ab05 | |||
| 99ae768ddf | |||
| fb902a04f5 | |||
| e46a99870b | |||
| 3fbba405d4 | |||
| 989415f423 | |||
| 7a3c7704b5 | |||
| 5d7407e2b9 | |||
| 4a5f1b01a1 | |||
| a038fa62ba | |||
| bb1127d815 | |||
| 40cf322fb4 | |||
| f32ccec154 | |||
| dc0b73f46f | |||
| 36cb625c4e | |||
| 89314733ff | |||
| d4e945222d | |||
| fe80620a5d | |||
| 166ddb4799 | |||
| 8e2a310f92 | |||
| 339a0606c0 | |||
| b6f1119eaa | |||
| 139a024d5f | |||
| 9491096485 | |||
| a34e42a519 | |||
| 6a7ecf7f58 | |||
| 96da78c670 | |||
| 9adc8e484a | |||
| e8eb5e4d0a | |||
| 080222ffe6 | |||
| abef655482 | |||
| 85b3f831dc | |||
| d8ece32d62 | |||
| 0df7117d37 | |||
| 91d67e6371 | |||
| 87d1b0637a | |||
| b8adddc648 | |||
| 25d3edc537 | |||
| 7f8cb80281 | |||
| a2ce2ea23a | |||
| 7f7aef0e6e | |||
| 821b5380ea | |||
| e8bcbc83a4 | |||
| 091af3c1c9 | |||
| 4cc50301dd | |||
| f643a1615e | |||
| 71e78c67f8 | |||
| e7c4a19d9e | |||
| 6c31d43e72 | |||
| 83b56d591b | |||
| e5b26067ab | |||
| a4fae94be8 | |||
| af4851f03a | |||
| 155d4c44ae | |||
| 48119baf97 | |||
| dd1989aebb | |||
| 0806430700 | |||
| 2d97f6638b | |||
| b10e5ad4d6 | |||
| 24f991ceb1 | |||
| ac11baa0c4 | |||
| 1609a77b77 | |||
| dd2f091a52 | |||
| a7bf0e7d5e | |||
| 19bdd3e622 | |||
| e1bf891ec6 | |||
| 4009834b97 | |||
| 77f5e1fbad | |||
| c4eb77d544 | |||
| 8a4ea43239 | |||
| f056e22f60 | |||
| 05e81040ce | |||
| 8d8133e780 | |||
| dceb35782e | |||
| 2c5f0c6cf2 | |||
| abd667684d | |||
| 31b5f88497 | |||
| 77a7026d33 | |||
| 9c5f3f9754 | |||
| 5d56025014 | |||
| ad62c75f4f | |||
| 7a5bd03216 | |||
| 593e0e28a9 | |||
| ed2b2de79a | |||
| 5a511eecc1 | |||
| 83e0fa1287 | |||
| a2dbdbf8cc | |||
| 3d68bbc0ca | |||
| 40f9f261ae | |||
| c08cc58904 | |||
| ebdb122a22 | |||
| 5de2752685 | |||
| d2245a95c7 | |||
| 47075e95bd | |||
| 9d4c980e02 | |||
| 53c1a1537d | |||
| 7db2f66685 | |||
| 6dd34b1fa4 | |||
| 862713cda4 | |||
| 06c1668e42 | |||
| 5f9f0f26eb | |||
| 8db22cdaf7 | |||
| 3bed9262e5 | |||
| 0c171a457b | |||
| b5e8dfd863 | |||
| c9df87fc25 | |||
| 30925a4b3c | |||
| ccf0efffce | |||
| 169c4392a9 | |||
| e543785959 | |||
| 3617d119cb | |||
| 5d1f28c3cb | |||
| 6f04eb12d0 | |||
| b46136e9f8 | |||
| c3fe2d0ee3 | |||
| aeed371888 | |||
| a50c8869a5 | |||
| 04d1fe4052 | |||
| 4e1d0bcf22 | |||
| d2126e5929 | |||
| 3bdfbebc02 | |||
| 1a9bd1891e | |||
| 1599bc14b3 | |||
| 30b74ab09c | |||
| 24bee01f01 | |||
| 7329c485ea | |||
| 9c93b12084 | |||
| b97c7d39ad | |||
| fa0c1c5d2a | |||
| e4c49ce57b | |||
| 3ae77c68e8 | |||
| 8b1cb8e7ae | |||
| ce0348518d | |||
| a660027b93 | |||
| 44ef409335 | |||
| 089edff1db | |||
| bd5a1b5bda | |||
| ba3517f3aa | |||
| 7f144b7b95 | |||
| 19ac0b6c38 | |||
| 3562437293 | |||
| e8ece71d22 | |||
| 3f2cad0490 | |||
| 5ec974ff94 | |||
| 2c585b649a | |||
| 36c28f3953 | |||
| d1763237d2 | |||
| e8544d78ee | |||
| e715a9467d | |||
| 1f8c12a4c8 | |||
| 89a6eac535 | |||
| d1ffb329c8 | |||
| 4396126a43 | |||
| 7448feed44 | |||
| 6bc4046b17 | |||
| 1a83e52935 | |||
| f16d42ba14 | |||
| 75135fe45d | |||
| de89ab31cf | |||
| c8a4f1bbd2 | |||
| 5be1858767 | |||
| ace1db739f | |||
| 80504310b6 | |||
| a4dae9650f | |||
| c27a995480 | |||
| 5b4c54abd2 | |||
| 57d0beedee | |||
| d0ba675b1b | |||
| f4f2fa9535 | |||
| 67707914b9 | |||
| 12766d61d4 | |||
| 34374cba41 | |||
| f8bb04eae3 | |||
| aee8cbe479 | |||
| cb36e10279 | |||
| 7ca44b5677 | |||
| a6921b83a1 | |||
| c3c94b9c59 | |||
| 576ef336fc | |||
| b1fb541912 | |||
| 6345b052da | |||
| 061554d921 | |||
| bcaa412f4b | |||
| 9f6536b263 | |||
| 226581f76e | |||
| e833d0cd13 | |||
| ed3c95d9d7 | |||
| 7a4823a1bc | |||
| 80c0932174 | |||
| 73a238e7b0 | |||
| b95900f154 | |||
| da66e3c908 | |||
| e04cc47022 | |||
| ce427698d9 | |||
| d3beae1374 | |||
| 389d481364 | |||
| 3efd88b0d6 | |||
| 0a5388f386 | |||
| 71fa002707 | |||
| dd80115aa2 | |||
| bc66710fec | |||
| 921241fbdd | |||
| 44b9dd64f6 | |||
| bfa77255a5 | |||
| b125c651a8 | |||
| eb1c81514b | |||
| 6df5632f91 | |||
| 391a412e65 | |||
| b10415b1b3 | |||
| b1c44d42e3 | |||
| 5f65e21878 | |||
| 0d3be9d101 | |||
| eea585a3cd | |||
| 1789406299 | |||
| b503061dc0 | |||
| 90ccaf4aab | |||
| 05ff47fcff | |||
| c40ecdd550 | |||
| 3a0622e9df | |||
| 0bb03750dc | |||
| 89defa9b6b | |||
| 459aad54aa | |||
| 6f3065f6ce | |||
| 69863ce4a9 | |||
| e5ae19fd34 | |||
| afe617fb93 | |||
| d922f0b9ad | |||
| f9d2d494b1 | |||
| 806aa14bd0 | |||
| 80066ba9e7 | |||
| 7cb1f07a0b | |||
| bdb52dcffd | |||
| 933ad6c801 | |||
| 1594b9505c | |||
| 325745a822 | |||
| 74f8b047fb | |||
| 0f8c6ffe9e | |||
| fa46fe0cf0 | |||
| f0a1efa1f2 | |||
| 71c1058e9e | |||
| 7575bd4dfb | |||
| d697ed3384 | |||
| aed70c8a02 | |||
| e82ca0e932 | |||
| 5910b39766 | |||
| ea9e0c9ab4 | |||
| 2e951eb952 | |||
| 2497611abd | |||
| 90bf56d9ba | |||
| e1b73ef0ef | |||
| bcb5fd7810 | |||
| 40c5145a13 | |||
| e304c3ad31 | |||
| 0494bd9677 | |||
| fc0cdfabb9 | |||
| 2dac137801 | |||
| 8ffa6fcf47 | |||
| 8824e6e2c2 | |||
| 5dbfe539ac | |||
| d1453e1139 | |||
| 5b5dcb3c6d | |||
| 4f467d199e | |||
| a6d20c0cc4 | |||
| d5d942bb2e | |||
| 9e2c7e653f | |||
| 200c0bf773 | |||
| 381b0d379a | |||
| f48b5b9d92 | |||
| 1f47aadc94 | |||
| bb3cd7e49c | |||
| edd01d44de | |||
| 52b899d070 | |||
| 008292df0f | |||
| 87bfd182d7 | |||
| 8fcc622b8a | |||
| 9415878f57 | |||
| 919ac41a74 | |||
| 80093f3777 | |||
| 28f5d6e91c | |||
| 550779deee | |||
| e2cf60f0c1 | |||
| 546a042d40 | |||
| 86b89f2153 | |||
| 34f4aa430a | |||
| 67678cb7f1 | |||
| f05b0d946d | |||
| 486651e0b7 | |||
| 5885924bb7 | |||
| ead849625d | |||
| f52aafdd6a | |||
| b0ae0a2c82 | |||
| 4d688170e0 | |||
| 322a2179e5 | |||
| 8014980b88 | |||
| 65237cab65 | |||
| 155827528a | |||
| fb15435296 | |||
| 6bba3ebeeb | |||
| 3343f3fb35 | |||
| 1b451d45cf | |||
| f5f676cf51 | |||
| 1dd477d382 | |||
| 96d05300bb | |||
| 0073a4c0a2 | |||
| d3a5071d9d | |||
| 471bd34fc6 | |||
| 44cc12763b | |||
| b68aca04ae | |||
| dd22264877 | |||
| cc6ef0268f | |||
| feecb89c8e | |||
| cb5c488cfd | |||
| b68e8dedd2 | |||
| 0975eb455e | |||
| df0e8bba8c | |||
| 71d5239823 | |||
| 92ae79f64e | |||
| 9c1f636cd4 | |||
| 23c60b38fd | |||
| 89c039252b | |||
| 60d85b4259 | |||
| e8a382b02a | |||
| 2b3ec66017 | |||
| 97dd37b345 | |||
| e4877e8246 | |||
| e216d345c1 | |||
| 14c1b70768 | |||
| e15f222a19 | |||
| 4ab2664529 | |||
| bd0cfa5366 | |||
| 2e4b0e83a2 | |||
| cf61111386 | |||
| 3aa5ddb070 | |||
| 25dacc8fed | |||
| 7cba63af9c | |||
| 3c1d6abc7e | |||
| 8da4b9f23f | |||
| 99fa77edee | |||
| 839c5ad932 | |||
| b68ce08811 | |||
| e662308665 | |||
| a2ccff67ca | |||
| 8e045d772c | |||
| 16fd614886 | |||
| f31df307c6 | |||
| 11873a7c51 | |||
| ccccbe23f4 | |||
| 2173a562be | |||
| a11f81f2ce | |||
| d3b553c4fb | |||
| 5eb11c3516 | |||
| c195799801 | |||
| ac5a44a962 | |||
| 86880e9e62 | |||
| f8ccdf1170 | |||
| e3d8473d47 | |||
| bd5d4cecbf | |||
| 2ede8a2206 | |||
| cb77f9bd26 | |||
| 0bb21fedae | |||
| 478f90c2d4 | |||
| a016f6e5c8 | |||
| 1b2fa4f994 | |||
| 2d8383efc6 | |||
| add5132dec | |||
| fe333051df | |||
| 8d1b533f91 | |||
| fb304e141a | |||
| bd094f930e | |||
| b61000d2fe | |||
| c72cadb234 | |||
| 10642177a9 | |||
| 0b8ea28463 | |||
| 9b24f54f0d | |||
| 289645dacc | |||
| c365acb517 | |||
| 785cf56621 | |||
| 60617eecce | |||
| 26c68c00b7 | |||
| ab0a602f9f | |||
| 066326ab3b | |||
| 02713dbd39 | |||
| d58dae5d2f | |||
| ca3685900f | |||
| 5e10e61aca | |||
| 92e25ce3cb | |||
| 804a670454 | |||
| efa8382fc5 | |||
| 92f9d5f506 | |||
| 8800c14105 | |||
| ec2d0bfa4a | |||
| d684d47dc8 | |||
| babf95785c | |||
| f6ebf80fab | |||
| ea2cff3311 | |||
| bf4aa8cbb1 | |||
| 8b751577f4 | |||
| 6fbe982623 | |||
| 0b93694097 | |||
| 2027c71d99 | |||
| 6da51c2a6e | |||
| 53def55dc9 | |||
| 8722350c0b | |||
| 34c92d4d9c | |||
| 725cb40ef2 | |||
| 0c81b41ac7 | |||
| a8a82450a5 | |||
| 1b5b9fdcf6 | |||
| af1e542064 | |||
| 78154d17bf | |||
| 9bc8012aa4 | |||
| 407962ec93 | |||
| 21fd09b424 | |||
| 2264e69a1e | |||
| e02d6c3f4b | |||
| ec0abb7b37 | |||
| 477becab0b | |||
| 60bf424461 | |||
| 923e8423f9 | |||
| e228095391 | |||
| 1492a660c8 | |||
| 7a931ea6f2 | |||
| 2d59bb9cb8 | |||
| 06f5ded038 | |||
| 91f371a46e | |||
| 573e53ebfd | |||
| a492be5971 | |||
| ceac85ca02 | |||
| bfa981709a | |||
| c64b477e53 | |||
| 4f7501e1e8 | |||
| a1f20ec7bb | |||
| e2ee6c6e56 | |||
| 6940a05f2b | |||
| ff8f4ecd3c | |||
| de2bd3ff1a | |||
| cfa21b3cbf | |||
| db7c3e3b01 | |||
| 0532ceb029 | |||
| 0fe869c169 | |||
| bcde68d851 | |||
| 960a90e36c | |||
| aea0395fd6 | |||
| 416d51f7c7 | |||
| a9b13fd056 | |||
| 080056806e | |||
| 7a33a9eb02 | |||
| 07ba483a13 | |||
| 4925741060 | |||
| 7c4505b2fd | |||
| f9ee838cd2 | |||
| cce0c39ab2 | |||
| cd57fd4b1d | |||
| 6b705f0448 | |||
| 9cff6c87f0 | |||
| cac41064e0 | |||
| 350cf0406d | |||
| 47fd52b7c1 | |||
| 8a33e2fea1 | |||
| d4c02a312a | |||
| c20294b993 | |||
| 7500631b81 | |||
| 7cc1a849a6 | |||
| 4df93af1fd | |||
| fc1c8f2071 | |||
| deedffa84e | |||
| e8d924426f | |||
| 5a3f809d03 | |||
| 58ffd8bf62 | |||
| b22452aaac | |||
| 6c05e2ea8c | |||
| a1773b6f46 | |||
| b3583d4191 | |||
| 50e8d9f67e | |||
| 0d4819d6e7 | |||
| ce91d296b6 | |||
| 21137b9268 | |||
| 0dc319e510 | |||
| 798beb0030 | |||
| a717b348e0 | |||
| 14ed4d1d08 | |||
| bd3250aff6 | |||
| 1ee5bb73fa | |||
| 9a6dc54f6c | |||
| ab59173135 | |||
| b6abb24858 | |||
| 2b24d5ebe5 | |||
| 10a99fff05 | |||
| 64aa1d07fb | |||
| 730eafa74f | |||
| c077727374 | |||
| 2e4241e435 | |||
| 8a03a1379e | |||
| 8bb7ef5e25 | |||
| da4508ef9a | |||
| f3dedba320 | |||
| f659b9bb7b | |||
| 488dee82d7 | |||
| 3c4020be8d | |||
| ede4e49c0c | |||
| 3d2f361585 | |||
| 4505ed1c98 | |||
| 4cbad8804f | |||
| 3ec7b71565 | |||
| c0ec2e7e2b | |||
| 048a94ddc5 | |||
| ec43415822 | |||
| 235eb3c83c | |||
| 4d0737c49b | |||
| b499686c7c | |||
| 1857d06245 | |||
| 663d233e3c | |||
| fa1936c10a | |||
| 8c89ba497b | |||
| 2284ddd68c | |||
| 8daf6b0b04 | |||
| e7a6f86299 | |||
| cae711b393 | |||
| 566c9afa77 | |||
| 867852688e | |||
| 94851016d7 | |||
| 1dd3c298df | |||
| c384ac26cd | |||
| a57080da74 | |||
| 71e59fe4c2 | |||
| aedae0b3c1 | |||
| 658f9b87a5 | |||
| fa9105e174 | |||
| d0d5f972df | |||
| f5220fba56 | |||
| 365f4f114b |
@@ -0,0 +1,8 @@
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: macos
|
||||
runner: macos-13
|
||||
runner: macos-14
|
||||
arch: x64
|
||||
oldarch:
|
||||
- os: macos
|
||||
@@ -21,12 +21,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Checkout Build Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: IfcOpenShell/build-outputs
|
||||
path: ./build
|
||||
@@ -34,14 +34,10 @@ jobs:
|
||||
lfs: true
|
||||
token: ${{ secrets.BUILD_REPO_TOKEN }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
brew update
|
||||
# preinstalled: xz, cmake
|
||||
brew install git bison autoconf automake libffi findutils
|
||||
echo "$(brew --prefix findutils)/libexec/gnubin" >> $GITHUB_PATH
|
||||
|
||||
@@ -55,16 +51,44 @@ jobs:
|
||||
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
with:
|
||||
key: ${GITHUB_WORKFLOW}-${{ matrix.os }}
|
||||
key: mac-${{ matrix.arch }}
|
||||
|
||||
- name: Run Build Script
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "${{ matrix.os }}" == "macos" ]; then
|
||||
DARWIN_C_SOURCE=-D_DARWIN_C_SOURCE
|
||||
fi
|
||||
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release python3 ./nix/build-all.py --diskcleanup
|
||||
if [ "${{ matrix.arch }}" == "x64" ]; then
|
||||
arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
MAC_INTEL=-mac-cross-compile-intel
|
||||
# We don't use gmpcxx, but it comes preinstalled on macos runner as arm64 bottle
|
||||
# and CGAL detects it and breaks cross compilation.
|
||||
brew uninstall --ignore-dependencies gmp
|
||||
# Otherwise Python will fallback to use arm64 `pkg-config`,
|
||||
# will pick up arm64 libraries ('zstd' in particular),
|
||||
# and break the build.
|
||||
/usr/local/bin/brew install pkg-config
|
||||
# Required by Python.
|
||||
/usr/local/bin/brew install gettext openssl
|
||||
fi
|
||||
set -o pipefail
|
||||
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release \
|
||||
python3 ./nix/build-all.py -v --diskcleanup ${MAC_INTEL} \
|
||||
| tee build.log
|
||||
|
||||
- name: Upload Build Logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: build-logs-osx-${{ matrix.arch }}
|
||||
path: |
|
||||
build.log
|
||||
build/*/*/*/logs/*.log
|
||||
build/*/*/*/build/ifcopenshell/**/CMakeCache.txt
|
||||
retention-days: 30
|
||||
|
||||
- name: Pack Dependencies
|
||||
run: |
|
||||
@@ -115,7 +139,7 @@ jobs:
|
||||
cd ..
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
|
||||
|
||||
@@ -9,41 +9,75 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
path: IfcOpenShell
|
||||
|
||||
- name: Checkout Pyodide
|
||||
uses: actions/checkout@v3
|
||||
- name: Checkout Build Repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
repository: pyodide/pyodide
|
||||
ref: '0.28.0a3'
|
||||
repository: IfcOpenShell/build-outputs
|
||||
path: ifcopenshell_build
|
||||
ref: wasm
|
||||
lfs: true
|
||||
token: ${{ secrets.BUILD_REPO_TOKEN }}
|
||||
path: pyodide
|
||||
|
||||
- name: Unpack Dependencies
|
||||
run: |
|
||||
cd ifcopenshell_build
|
||||
python ../IfcOpenShell/pyodide/cache_dependencies.py unpack
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
VERSION=`cat IfcOpenShell/VERSION`
|
||||
sed -i s/0.8.0/$VERSION/g IfcOpenShell/pyodide/meta.yaml
|
||||
sed -i s/0.8.0/$VERSION/g IfcOpenShell/pyodide/setup.py
|
||||
echo '#!/usr/bin/bash' > script.sh
|
||||
echo 'cd pyodide' >> script.sh
|
||||
echo 'make' >> script.sh
|
||||
echo 'cd ..' >> script.sh
|
||||
echo 'mkdir -p packages/ifcopenshell' >> script.sh
|
||||
echo 'cp IfcOpenShell/pyodide/meta.yaml packages/ifcopenshell' >> script.sh
|
||||
echo 'PYODIDE_ROOT=/src/pyodide \' >> script.sh
|
||||
echo 'PATH=/src/pyodide/emsdk/emsdk:/src/pyodide/emsdk/emsdk/node/22.16.0_64bit/bin:/src/pyodide/emsdk/emsdk/upstream/emscripten:$PATH \' >> script.sh
|
||||
echo 'pyodide build-recipes ifcopenshell --install' >> script.sh
|
||||
chmod +x script.sh
|
||||
sed -i s/--tty// pyodide/run_docker
|
||||
pyodide/run_docker ./script.sh
|
||||
mv dist/ifcopenshell-$VERSION-py3-none-any.whl dist/ifcopenshell-$VERSION+${GITHUB_SHA:0:7}-cp313-cp313-emscripten_4_0_9_wasm32.whl
|
||||
./IfcOpenShell/pyodide/build_pyodide.sh
|
||||
FILE=`echo dist/ifcopenshell-*.whl`
|
||||
NEW_FILE=`echo $FILE | sed "s/-/+${GITHUB_SHA:0:7}-/2"`
|
||||
mv $FILE $NEW_FILE
|
||||
|
||||
- name: Upload Build Logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: build-logs-pyodide
|
||||
path: |
|
||||
ifcopenshell_build/*/*/logs/*.log
|
||||
retention-days: 30
|
||||
|
||||
- name: Run wheel tests
|
||||
run: |
|
||||
cp -r IfcOpenShell/pyodide/test test
|
||||
# venv set up in build_pyodide.sh.
|
||||
source .venv/bin/activate
|
||||
uv pip install pytest-pyodide
|
||||
PYODIDE_ROOT_DIST=`pyodide config get pyodide_root`/dist
|
||||
# `pytest-pyodide` requires pyodide in 'pyodide' directory in cwd, when running `pytest`.
|
||||
cp -r $PYODIDE_ROOT_DIST test/pyodide
|
||||
cp dist/ifcopenshell-*.whl test/pyodide
|
||||
cd test
|
||||
pytest --capture=no
|
||||
|
||||
- name: Pack Dependencies
|
||||
run: |
|
||||
cd ifcopenshell_build
|
||||
python ../IfcOpenShell/pyodide/cache_dependencies.py pack
|
||||
|
||||
- name: Commit and Push Changes to Build Repository
|
||||
run: |
|
||||
cd ifcopenshell_build
|
||||
git config user.name "IfcOpenBot"
|
||||
git config user.email "ifcopenbot@ifcopenshell.org"
|
||||
git add */*/install/cache-*.tar.gz
|
||||
git commit -m "Update build artifacts [skip ci]" || echo "No changes to commit"
|
||||
git push || echo "Push failed"
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \
|
||||
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
|
||||
readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \
|
||||
findutils xz
|
||||
findutils xz byacc
|
||||
python3 -m pip install typing_extensions
|
||||
git config --global --add safe.directory '*'
|
||||
|
||||
@@ -29,12 +29,12 @@ jobs:
|
||||
aws --version
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Checkout Build Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: IfcOpenShell/build-outputs
|
||||
path: ./build
|
||||
@@ -47,15 +47,27 @@ jobs:
|
||||
install_root=$(find ./build -maxdepth 4 -type d -name install 2>/dev/null | head -n 1 || true)
|
||||
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
|
||||
|
||||
# Not supported on docker
|
||||
# - name: ccache
|
||||
# uses: hendrikmuhs/ccache-action@v1
|
||||
# with:
|
||||
# key: ${GITHUB_WORKFLOW}-rockylinux8-x64
|
||||
- name: ccache
|
||||
# TODO: Use tag after 1.2.20 releases.
|
||||
uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
|
||||
|
||||
- name: Run Build Script
|
||||
shell: bash
|
||||
run: |
|
||||
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release python3 ./nix/build-all.py --diskcleanup
|
||||
set -o pipefail
|
||||
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release python3 ./nix/build-all.py -v --diskcleanup 2>&1 | tee build.log
|
||||
|
||||
- name: Upload Build Logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: build-logs-rocky
|
||||
path: |
|
||||
build.log
|
||||
build/*/*/logs/*.log
|
||||
retention-days: 30
|
||||
|
||||
- name: Pack Dependencies
|
||||
run: |
|
||||
@@ -106,7 +118,7 @@ jobs:
|
||||
cd ..
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \
|
||||
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
|
||||
readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \
|
||||
findutils xz
|
||||
findutils xz byacc
|
||||
python3 -m pip install typing_extensions
|
||||
git config --global --add safe.directory '*'
|
||||
|
||||
@@ -29,12 +29,12 @@ jobs:
|
||||
aws --version
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Checkout Build Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: IfcOpenShell/build-outputs
|
||||
path: ./build
|
||||
@@ -47,15 +47,27 @@ jobs:
|
||||
install_root=$(find ./build -maxdepth 4 -type d -name install 2>/dev/null | head -n 1 || true)
|
||||
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
|
||||
|
||||
# Not supported on docker
|
||||
# - name: ccache
|
||||
# uses: hendrikmuhs/ccache-action@v1
|
||||
# with:
|
||||
# key: ${GITHUB_WORKFLOW}-rockylinux8-x64
|
||||
- name: ccache
|
||||
# TODO: Use tag after 1.2.20 releases.
|
||||
uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
|
||||
|
||||
- name: Run Build Script
|
||||
shell: bash
|
||||
run: |
|
||||
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release python3 ./nix/build-all.py --diskcleanup
|
||||
set -o pipefail
|
||||
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release python3 ./nix/build-all.py -v --diskcleanup 2>&1 | tee build.log
|
||||
|
||||
- name: Upload Build Logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: build-logs-rocky-arm64
|
||||
path: |
|
||||
build.log
|
||||
build/*/*/logs/*.log
|
||||
retention-days: 30
|
||||
|
||||
- name: Pack Dependencies
|
||||
run: |
|
||||
@@ -106,7 +118,7 @@ jobs:
|
||||
cd ..
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
|
||||
|
||||
@@ -9,16 +9,15 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python: ['3.9.11', '3.10.3', '3.11.8', '3.12.1', '3.13.0']
|
||||
arch: ['x64']
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Checkout Build Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: IfcOpenShell/build-outputs
|
||||
path: _deps-vs2022-x64-installed
|
||||
@@ -30,14 +29,6 @@ jobs:
|
||||
run: |
|
||||
choco install -y sed 7zip.install awscli
|
||||
|
||||
- name: Install Python
|
||||
run: |
|
||||
$installer = "python-${{ matrix.python }}-amd64.exe"
|
||||
$url = "https://www.python.org/ftp/python/${{ matrix.python }}/$installer"
|
||||
Invoke-WebRequest -Uri $url -OutFile $installer
|
||||
Start-Process -Wait -FilePath .\$installer -ArgumentList '/quiet InstallAllUsers=0 PrependPath=0 Include_test=0 TargetDir=C:\Python\${{ matrix.python }}'
|
||||
Remove-Item .\$installer
|
||||
|
||||
- name: Unpack Dependencies
|
||||
run: |
|
||||
cd _deps-vs2022-x64-installed
|
||||
@@ -45,21 +36,21 @@ jobs:
|
||||
7z x $_.FullName
|
||||
}
|
||||
|
||||
- name: Run Build Script
|
||||
- name: ccache
|
||||
# TODO: Use tag after 1.2.20 releases.
|
||||
uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639
|
||||
with:
|
||||
key: win-${{ matrix.arch }}
|
||||
# Windows ccache needs ~1GB
|
||||
# and with default 500MB some cache gets deleted, leading to misses.
|
||||
max-size: 5000MB
|
||||
|
||||
- name: Run Build Script And Pack .zip Archives
|
||||
shell: cmd
|
||||
run: |
|
||||
setlocal EnableDelayedExpansion
|
||||
SET PYTHON_VERSION=${{ matrix.python }}
|
||||
for /f "tokens=1,2,3 delims=." %%a in ("%PYTHON_VERSION%") do (
|
||||
set PY_VER_MAJOR_MINOR=%%a%%b
|
||||
)
|
||||
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
|
||||
SET IFCOS_INSTALL_PYTHON=FALSE
|
||||
cd win
|
||||
echo y | call build-deps.cmd vs2022-x64 Release
|
||||
SET PYTHONHOME=C:\Python\${{ matrix.python }}
|
||||
call run-cmake.bat vs2022-x64 -DENABLE_BUILD_OPTIMIZATIONS=On -DGLTF_SUPPORT=ON -DADD_COMMIT_SHA=ON -DVERSION_OVERRIDE=ON
|
||||
call install-ifcopenshell.bat vs2022-x64 Release
|
||||
python build-all-win.py
|
||||
|
||||
- name: Pack Dependencies
|
||||
run: |
|
||||
@@ -81,37 +72,8 @@ jobs:
|
||||
git commit -m "Update build artifacts [skip ci]" || echo "No changes to commit"
|
||||
git push || echo "Push failed"
|
||||
|
||||
- name: Package .zip Archives
|
||||
run: |
|
||||
$VERSION = 'v' + ((Get-Content VERSION).Trim())
|
||||
$SHA = ${env:GITHUB_SHA}.Substring(0, 7)
|
||||
$OUTPUT_DIR = "$env:USERPROFILE\output"
|
||||
New-Item -ItemType Directory -Force -Path $OUTPUT_DIR
|
||||
|
||||
if ("${{ matrix.python }}" -eq "3.9.11") {
|
||||
# only for the first python version the executables are assembled for upload
|
||||
cd _installed-vs2022-x64/bin
|
||||
Get-ChildItem -Path . | ForEach-Object {
|
||||
echo $_
|
||||
$exe = $_.Name
|
||||
$baseName = $exe.Substring(0, $exe.Length - 4)
|
||||
$zipName = "${baseName}-$VERSION-$SHA-win64.zip"
|
||||
7z a $zipName $exe
|
||||
}
|
||||
mv *.zip $OUTPUT_DIR
|
||||
}
|
||||
|
||||
$pyVersion = "${{ matrix.python }}"
|
||||
$pyVersionMajor = ($pyVersion -split '\.')[0..1] -join ''
|
||||
cd C:\Python\${{ matrix.python }}\Lib\site-packages
|
||||
Remove-Item -Recurse -Force ifcopenshell\__pycache__ -ErrorAction SilentlyContinue
|
||||
Get-ChildItem -Path ifcopenshell -Filter "*.pyc" -Recurse | Remove-Item -Force
|
||||
$zipName = "ifcopenshell-python-$pyVersionMajor-$VERSION-$SHA-win64.zip"
|
||||
7z a $zipName ifcopenshell
|
||||
mv $zipName $OUTPUT_DIR
|
||||
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
|
||||
|
||||
@@ -19,8 +19,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2 # https://github.com/actions/checkout
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5 # https://github.com/actions/checkout
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # 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
|
||||
|
||||
@@ -9,15 +9,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Action - checkout repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Action - install python
|
||||
uses: actions/setup-python@v5.3.0
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.9"
|
||||
|
||||
- name: Action - install python
|
||||
uses: actions/setup-python@v5.3.0
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
@@ -26,6 +26,7 @@ jobs:
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
uv tool install ruff
|
||||
uv tool install black
|
||||
uv tool install poethepoet
|
||||
|
||||
# black doesn't catch all syntax errors, so we check them explicitly.
|
||||
- name: Check syntax errors
|
||||
@@ -40,13 +41,16 @@ jobs:
|
||||
- name: Black formatter
|
||||
id: black
|
||||
run: |
|
||||
uvx black --diff --check .
|
||||
black --diff --check .
|
||||
continue-on-error: true
|
||||
|
||||
- name: Ruff check
|
||||
id: ruff
|
||||
run: |
|
||||
uvx ruff check
|
||||
ERROR=0
|
||||
poe ruff-main || ERROR=1
|
||||
poe ruff-old || ERROR=1
|
||||
exit $ERROR
|
||||
continue-on-error: true
|
||||
|
||||
- name: Final check
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-tags: true
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -54,8 +54,8 @@ jobs:
|
||||
short_name: macosm1,
|
||||
}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
python-version: '3.11'
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout bonsai_unstable_repo repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: IfcOpenShell/bonsai_unstable_repo
|
||||
token: ${{ secrets.IFCOPENBOT_TOKEN }}
|
||||
|
||||
@@ -43,8 +43,8 @@ jobs:
|
||||
short_name: macosm1,
|
||||
}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
python-version: '3.11'
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
- name: Compile
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
- name: Compile
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
- name: Compile
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
- name: Compile
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
- name: Compile
|
||||
|
||||
@@ -37,8 +37,8 @@ jobs:
|
||||
short_name: macosm164
|
||||
}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # 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
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
- name: Compile
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
- name: Compile
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
- name: Compile
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
if: |
|
||||
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||
steps:
|
||||
- uses: mamba-org/setup-micromamba@v1 # https://github.com/mamba-org/setup-micromamba
|
||||
- uses: mamba-org/setup-micromamba@v2 # https://github.com/mamba-org/setup-micromamba
|
||||
with:
|
||||
environment-name: test-env
|
||||
create-args: >-
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
date: ${{ steps.date.outputs.date }}
|
||||
verdate: ${{ steps.verdate.outputs.verdate }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set env
|
||||
run: echo ok go
|
||||
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
echo "ARTIFACTS_DIR=/home/runner/work/artifacts" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: activate
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
make package
|
||||
working-directory: build
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
# Artifact name
|
||||
name: ifcos-artifacts
|
||||
@@ -89,31 +89,31 @@ jobs:
|
||||
name: Docker Build, Tag, Push
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
lfs: true
|
||||
|
||||
- name: Download
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
uses: actions/download-artifact@v6.0.0
|
||||
with:
|
||||
# Artifact name
|
||||
name: ifcos-artifacts
|
||||
path: artifacts/
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Login to Dockerhub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: aecgeeks
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
-
|
||||
name: Build container image
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: artifacts
|
||||
repository: aecgeeks/ifcopenshell
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pyver: [py39, py310, py311, py312, py313]
|
||||
pyver: [py39, py310, py311, py312, py313, py314]
|
||||
config:
|
||||
- {
|
||||
name: "Windows 64bit",
|
||||
@@ -34,6 +34,10 @@ jobs:
|
||||
name: "Linux 64bit",
|
||||
short_name: linux64
|
||||
}
|
||||
- {
|
||||
name: "Linux ARM 64bit",
|
||||
short_name: linuxarm64
|
||||
}
|
||||
- {
|
||||
name: "MacOS Intel 64bit",
|
||||
short_name: macos64
|
||||
@@ -43,10 +47,10 @@ jobs:
|
||||
short_name: macosm164
|
||||
}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # 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
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pyver: [py39, py310, py311, py312, py313]
|
||||
pyver: [py39, py310, py311, py312, py313, py314]
|
||||
config:
|
||||
- {
|
||||
name: "Windows 64bit",
|
||||
@@ -38,10 +38,10 @@ jobs:
|
||||
short_name: macosm164
|
||||
}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # 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
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
- name: Compile
|
||||
|
||||
@@ -25,8 +25,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
python-version: '3.11'
|
||||
|
||||
@@ -19,8 +19,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
python-version: '3.11'
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
name: ci-ifctester-org
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- src/ifctester/**
|
||||
|
||||
jobs:
|
||||
publish_website:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Checkout ifctester_org_static_html
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: IfcOpenShell/ifctester_org_static_html
|
||||
token: ${{ secrets.IFCOPENBOT_TOKEN }}
|
||||
path: ifctester_org_static_html
|
||||
- name: Build webapp
|
||||
working-directory: ./src/ifctester
|
||||
run: |
|
||||
sudo apt update && sudo apt install -y nodejs
|
||||
git config --global user.name 'IfcOpenBot'
|
||||
git config --global user.email 'IfcOpenBot@users.noreply.github.com'
|
||||
make webapp-build
|
||||
- name: Commit and push
|
||||
run: |
|
||||
cp -r src/ifctester/webapp/dist/* ifctester_org_static_html/
|
||||
git -C ifctester_org_static_html add .
|
||||
git -C ifctester_org_static_html commit --allow-empty -m "$(git log --oneline -1)"
|
||||
git -C ifctester_org_static_html push
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
- name: Compile
|
||||
|
||||
+69
-35
@@ -35,12 +35,12 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: activate
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.11
|
||||
|
||||
@@ -49,7 +49,8 @@ jobs:
|
||||
python -m pip install --upgrade pip
|
||||
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely
|
||||
pip install src/bcf --no-deps
|
||||
pip install https://github.com/Andrej730/aud/archive/refs/heads/master-reduced-size.zip
|
||||
pip install git+https://github.com/zdhoward/aud
|
||||
pip install pytest-xdist==3.8.0
|
||||
|
||||
- name: Install C++ dependencies
|
||||
run: |
|
||||
@@ -63,22 +64,28 @@ jobs:
|
||||
libboost-regex-dev \
|
||||
libboost-system-dev \
|
||||
libboost-thread-dev \
|
||||
swig libpcre3-dev libxml2-dev \
|
||||
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 libeigen3-dev
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1
|
||||
# TODO: temporarily pointing to 1.2.19 to get notified by dependabot when 1.2.20 is released
|
||||
# to update hardcoded references to commits in some other workflows.
|
||||
# Then we can switch back to 1.2 in all actions.
|
||||
uses: hendrikmuhs/ccache-action@v1.2.19
|
||||
with:
|
||||
key: ${GITHUB_WORKFLOW}
|
||||
key: ubuntu-22.04-${{ runner.arch }}
|
||||
|
||||
# rocksdb on debian distros misses RTTI?
|
||||
# RTTI is only enabled by default in Debug builds of rocksdb.
|
||||
# Distros are using Release builds, so we're compiling it ourselves with RTTI forced on.
|
||||
# https://github.com/facebook/rocksdb/blob/a3aa44a7167b8336f9bc15c8aba063260268ff68/CMakeLists.txt#L433
|
||||
- name: build rocksdb
|
||||
run: |
|
||||
git clone https://github.com/facebook/rocksdb --branch v9.11.2
|
||||
cd rocksdb
|
||||
mkdir build && cd build
|
||||
# rocksdb is using ccache automatically.
|
||||
cmake -DFAIL_ON_WARNINGS=Off \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr/local \
|
||||
@@ -93,15 +100,50 @@ jobs:
|
||||
..
|
||||
sudo make -j$(nproc) install
|
||||
|
||||
# OpenCOLLADA is ancient, but we still have it in the main build.
|
||||
# So adding it to CI to catch any breakages.
|
||||
- name: build OpenCOLLADA
|
||||
run: |
|
||||
git clone https://github.com/KhronosGroup/OpenCOLLADA
|
||||
cd OpenCOLLADA
|
||||
git checkout v1.6.68
|
||||
patch -p1 --batch --forward -i ../nix/patches/opencollada/pr622_and_disable_subdirs.patch
|
||||
patch -p1 --batch --forward -i ../nix/patches/opencollada/allow_static_libraries_config_on_unix.patch
|
||||
mkdir build && cd build
|
||||
cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_FLAGS_INIT="-fPIC"
|
||||
sudo make -j$(nproc) install
|
||||
|
||||
# Ubuntu has `swig` package, but we build it to match the version we use in the main build.
|
||||
# To avoid failing tests when checking stub generation.
|
||||
- name: build swig
|
||||
run: |
|
||||
# Remove default swig to avoid conflicts.
|
||||
sudo apt remove --purge swig swig4.0
|
||||
sudo apt-get install -y libpcre2-dev bison
|
||||
git clone https://github.com/swig/swig
|
||||
cd swig
|
||||
git checkout v4.1.0
|
||||
mkdir build && cd build
|
||||
cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache
|
||||
sudo make -j$(nproc) install
|
||||
|
||||
- name: Build ifcopenshell
|
||||
run: |
|
||||
echo $Python3_ROOT_DIR
|
||||
echo ${{ env.pythonLocation }}
|
||||
|
||||
mkdir build && cd build
|
||||
# Ubuntu 22.04's libocct-foundation-dev package doesn't have Config.cmake, so we provide OCC paths directly.
|
||||
# In later versions of Ubuntu, this can be simplified and the OCC paths can be removed.
|
||||
cmake \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_STANDARD=17 \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_PREFIX_PATH=/usr \
|
||||
@@ -110,39 +152,31 @@ jobs:
|
||||
-DOCC_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
|
||||
-DPYTHON_EXECUTABLE:FILEPATH=${{ env.pythonLocation }}/bin/python \
|
||||
-DPYTHON_INCLUDE_DIR:PATH=${{ env.pythonLocation }}/include/python3.11 \
|
||||
-DPYTHON_LIBRARY:FILEPATH=${{ env.pythonLocation }}/lib/libpython3.11.so \
|
||||
-DCOLLADA_SUPPORT=Off \
|
||||
-DUSE_MMAP=On \
|
||||
"-DSCHEMA_VERSIONS=2x3;4;4x3_add2" \
|
||||
-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 \
|
||||
-DEIGEN_DIR=/usr/include/eigen3 \
|
||||
-DROCKSDB_INCLUDE_DIR=/usr/local/include \
|
||||
-DROCKSDB_LIBRARY_DIR=/usr/local/lib \
|
||||
-DWITH_ROCKSDB=On \
|
||||
../cmake
|
||||
sudo make -j $(nproc)
|
||||
sudo make install
|
||||
|
||||
- name: Run IfcConvert on Sample files
|
||||
run: |
|
||||
(find test/input src/bonsai/test/files -name '*.ifc' | while read i; do \
|
||||
echo $i | tee -a log; \
|
||||
timeout 1m "$(which IfcConvert)" -yv "$i" "$i.obj" --validate >> log 2>&1; \
|
||||
echo $i $? >> statuses; \
|
||||
done) || true
|
||||
echo Failed
|
||||
grep -v 0$ statuses
|
||||
grep -v 0$ statuses | wc -l
|
||||
echo Succeeded
|
||||
grep 0$ statuses
|
||||
grep 0$ statuses | wc -l
|
||||
# - name: Run IfcConvert on Sample files
|
||||
# run: |
|
||||
# (find test/input src/bonsai/test/files -name '*.ifc' | while read i; do \
|
||||
# echo $i | tee -a log; \
|
||||
# timeout 1m "$(which IfcConvert)" -yv "$i" "$i.obj" --validate >> log 2>&1; \
|
||||
# echo $i $? >> statuses; \
|
||||
# done) || true
|
||||
# echo Failed
|
||||
# grep -v 0$ statuses
|
||||
# grep -v 0$ statuses | wc -l
|
||||
# echo Succeeded
|
||||
# grep 0$ statuses
|
||||
# grep 0$ statuses | wc -l
|
||||
|
||||
- name: Run IfcConvert on Sample file
|
||||
run: |
|
||||
IfcConvert test/input/acad2010_walls.ifc test/input/acad2010_walls.obj
|
||||
|
||||
- name: Test ifcopenshell-python
|
||||
run: |
|
||||
@@ -152,7 +186,7 @@ jobs:
|
||||
mv ifcopenshell ifcopenshell-local # Force testing on installed module
|
||||
pip install -e ../ifcpatch --no-deps # Needed for sql.py tests.
|
||||
ERROR=0
|
||||
make test || ERROR=1
|
||||
make test-parallel || ERROR=1
|
||||
cd ../bcf && make test || ERROR=1
|
||||
pip install requests
|
||||
cd ../bsdd && make test || ERROR=1
|
||||
|
||||
@@ -11,10 +11,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout (recursive)
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Upload static files as artifact
|
||||
id: deployment
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
path: src/pyodide/demo-app/
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install C++ dependencies
|
||||
|
||||
+6
-6
@@ -17,6 +17,7 @@
|
||||
# General Python residue
|
||||
__pycache__
|
||||
*.py.bak
|
||||
venv
|
||||
|
||||
# Visual Studio Code files
|
||||
.vscode
|
||||
@@ -78,8 +79,6 @@ src/bonsai/bonsai/translations.py
|
||||
|
||||
# bonsai test temp files
|
||||
src/bonsai/test/files/temp
|
||||
src/bonsai/test/files/basic.ifc.cache.blend
|
||||
src/bonsai/test/files/basic.ifc.cache.sqlite
|
||||
|
||||
# bonsai data
|
||||
src/bonsai/bonsai/bim/data/build/
|
||||
@@ -87,9 +86,7 @@ src/bonsai/bonsai/bim/data/gantt/index.html
|
||||
src/bonsai/bonsai/bim/data/gantt/jsgantt.js
|
||||
src/bonsai/bonsai/bim/data/gantt/jsgantt.css
|
||||
src/bonsai/bonsai/bim/data/webui/static/js/jquery.min.js
|
||||
|
||||
src/bonsai/drawings
|
||||
src/bonsai/layouts
|
||||
src/bonsai/bonsai/bim/data/webui/running_pid.json
|
||||
|
||||
# ifcopenshell swig and compiled files
|
||||
src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper*.so
|
||||
@@ -109,4 +106,7 @@ src/bonsai/bonsai/bim/schema/Brick.ttl
|
||||
bonsaiDecoratorForLoads.code-workspace
|
||||
dev_environment.bat
|
||||
|
||||
.pixi/
|
||||
.pixi/
|
||||
|
||||
src/ifcopenshell-python/ifcopenshell/express/*.exp
|
||||
src/ifcopenshell-python/ifcopenshell/express/*.exp.cache.dat
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
[submodule "src/ifcopenshell-python/test/Sample-BIM-Files"]
|
||||
path = src/ifcopenshell-python/test/Sample-BIM-Files
|
||||
url = https://github.com/IfcOpenShell/ids-test-files
|
||||
[submodule "src/ifcconvert/cityjson"]
|
||||
path = src/ifcconvert/cityjson
|
||||
url = https://github.com/IfcOpenShell/ifc-to-cityjson
|
||||
[submodule "docs/cpp-api/assets/doxygen-awesome-css"]
|
||||
path = docs/cpp-api/assets/doxygen-awesome-css
|
||||
url = https://github.com/jothepro/doxygen-awesome-css.git
|
||||
|
||||
@@ -37,25 +37,25 @@ Contents
|
||||
|
||||
| Name | Description | License | Service |
|
||||
| ------------------------- | --------------------------------------------------------------------- | ------------------- | ------- |
|
||||
| bcf | Library to read and write BCF-XML and query OpenCDE BCF-API modules | LGPL-3.0-or-later | [](https://pypi.org/project/bcf-client/) [](https://anaconda.org/conda-forge/bcf-client) |
|
||||
| bonsai | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [](https://bonsaibim.org/download.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=bonsai&expanded=true) [](https://community.chocolatey.org/packages/blenderbim-nightly/) |
|
||||
| bsdd | Library to query the bSDD API | LGPL-3.0-or-later | [](https://pypi.org/project/bsdd/) |
|
||||
| ifc2ca | Utility to convert IFC structural analysis models to Code_Aster | LGPL-3.0-or-later |
|
||||
| ifc4d | Convert to and from IFC and project management software | LGPL-3.0-or-later | [](https://pypi.org/project/ifc4d/) |
|
||||
| ifc5d | Report and optimise cost information from IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifc5d/) |
|
||||
| ifcbimtester | Wrapper for Gherkin based unit testing for IFC models | LGPL-3.0-or-later |
|
||||
| [bcf](https://docs.ifcopenshell.org/bcf.html) | Library to read and write BCF-XML and query OpenCDE BCF-API modules | LGPL-3.0-or-later | [](https://pypi.org/project/bcf-client/) [](https://anaconda.org/conda-forge/bcf-client) |
|
||||
| [bonsai](https://docs.ifcopenshell.org/bonsai.html) | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [](https://bonsaibim.org/download.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=bonsai&expanded=true) [](https://community.chocolatey.org/packages/blenderbim-nightly/) |
|
||||
| [bsdd](https://docs.ifcopenshell.org/bsdd.html) | Library to query the bSDD API | LGPL-3.0-or-later | [](https://pypi.org/project/bsdd/) |
|
||||
| [ifc2ca](https://docs.ifcopenshell.org/ifc2ca.html) | Utility to convert IFC structural analysis models to Code_Aster | LGPL-3.0-or-later |
|
||||
| [ifc4d](https://docs.ifcopenshell.org/ifc4d.html) | Convert to and from IFC and project management software | LGPL-3.0-or-later | [](https://pypi.org/project/ifc4d/) |
|
||||
| [ifc5d](https://docs.ifcopenshell.org/ifc5d.html) | Report and optimise cost information from IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifc5d/) |
|
||||
| [ifcbimtester](https://docs.ifcopenshell.org/bimtester.html) | Wrapper for Gherkin based unit testing for IFC models | LGPL-3.0-or-later |
|
||||
| ifcblender | Historic Blender IFC import add-on | LGPL-3.0-or-later\* |
|
||||
| ifccityjson | Convert CityJSON to IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifccityjson/) |
|
||||
| ifcclash | Clash detection library and CLI app | LGPL-3.0-or-later | [](https://pypi.org/project/ifcclash/) |
|
||||
| ifcconvert | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcconvert/installation.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcconvert&expanded=true)
|
||||
| ifccsv | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifccsv/) |
|
||||
| ifcdiff | Compare changes between IFC models | LGPL-3.0-or-later | [](https://pypi.org/project/ifcdiff/) |
|
||||
| ifcfm | Extract IFC data for FM handover requirements | LGPL-3.0-or-later | [](https://pypi.org/project/ifcfm/) |
|
||||
| ifcmax | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcmax.html)
|
||||
| ifcopenshell-python | Python library for IFC manipulation | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [](https://pypi.org/project/ifcopenshell/) [](https://anaconda.org/conda-forge/ifcopenshell) [](https://anaconda.org/ifcopenshell/ifcopenshell) [](https://hub.docker.com/r/aecgeeks/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell-git) |
|
||||
| ifcpatch | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later | [](https://pypi.org/project/ifcpatch/) |
|
||||
| ifcsverchok | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true)
|
||||
| ifctester | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later | [](https://pypi.org/project/ifctester/) |
|
||||
| [ifccityjson](https://docs.ifcopenshell.org/ifccityjson.html) | Convert CityJSON to IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifccityjson/) |
|
||||
| [ifcclash](https://docs.ifcopenshell.org/ifcclash.html) | Clash detection library and CLI app | LGPL-3.0-or-later | [](https://pypi.org/project/ifcclash/) |
|
||||
| [ifcconvert](https://docs.ifcopenshell.org/ifcconvert.html) | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcconvert/installation.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcconvert&expanded=true)
|
||||
| [ifccsv](https://docs.ifcopenshell.org/ifccsv.html) | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifccsv/) |
|
||||
| [ifcdiff](https://docs.ifcopenshell.org/ifcdiff.html) | Compare changes between IFC models | LGPL-3.0-or-later | [](https://pypi.org/project/ifcdiff/) |
|
||||
| [ifcfm](https://docs.ifcopenshell.org/ifcfm.html) | Extract IFC data for FM handover requirements | LGPL-3.0-or-later | [](https://pypi.org/project/ifcfm/) |
|
||||
| [ifcmax](https://docs.ifcopenshell.org/ifcmax.html) | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcmax.html)
|
||||
| [ifcopenshell-python](https://docs.ifcopenshell.org/ifcopenshell-python.html) | Python library for IFC manipulation | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [](https://pypi.org/project/ifcopenshell/) [](https://anaconda.org/conda-forge/ifcopenshell) [](https://anaconda.org/ifcopenshell/ifcopenshell) [](https://hub.docker.com/r/aecgeeks/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell-git) [Pyodide WASM Wheels](https://github.com/IfcOpenShell/wasm-wheels#pyodide-test-wheels) |
|
||||
| [ifcpatch](https://docs.ifcopenshell.org/ifcpatch.html) | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later | [](https://pypi.org/project/ifcpatch/) |
|
||||
| [ifcsverchok](https://docs.ifcopenshell.org/ifcsverchok.html) | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true)
|
||||
| [ifctester](https://docs.ifcopenshell.org/ifctester.html) | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later | [](https://pypi.org/project/ifctester/) |
|
||||
|
||||
The IfcOpenShell C++ codebase is split into multiple interal libraries:
|
||||
|
||||
|
||||
+117
-792
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,6 @@
|
||||
"IFCXML_SUPPORT": "ON",
|
||||
"HDF5_SUPPORT": "ON",
|
||||
"SCHEMA_VERSIONS": "4x3_add2",
|
||||
"CITYJSON_SUPPORT": "OFF",
|
||||
"CMAKE_GENERATOR_PLATFORM": "",
|
||||
"CMAKE_GENERATOR_TOOLSET": ""
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#
|
||||
# Input variables:
|
||||
# - `CGAL_INCLUDE_DIR`
|
||||
# - `CGAL_LIBRARY_DIR`
|
||||
# - `GMP_INCLUDE_DIR`
|
||||
# - `GMP_LIBRARY_DIR`
|
||||
# - `MPFR_INCLUDE_DIR`
|
||||
# - `MPFR_LIBRARY_DIR`
|
||||
# If input variables are not specified, try to find HDF5 config.
|
||||
# Input variables could also be provided as environment variables.
|
||||
#
|
||||
# Output variables:
|
||||
# - `CGAL_INCLUDE_DIR`
|
||||
#
|
||||
|
||||
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)
|
||||
|
||||
if(NOT CGAL_INCLUDE_DIR)
|
||||
# CGAL is not respecting default Boost_USE_STATIC_LIBS value
|
||||
# and sometiems it's getting in the way.
|
||||
if(NOT DEFINED Boost_USE_STATIC_LIBS)
|
||||
set(CGAL_Boost_USE_STATIC_LIBS OFF)
|
||||
else()
|
||||
set(CGAL_Boost_USE_STATIC_LIBS "${Boost_USE_STATIC_LIBS}")
|
||||
endif()
|
||||
find_package(CGAL CONFIG REQUIRED)
|
||||
if(NOT CGAL_DIR)
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"CGAL_SUPPORT enabled, but CGAL_INCLUDE_DIR wasn't provided and CGAL package couldn't be found."
|
||||
)
|
||||
endif()
|
||||
message(STATUS "CGAL: found config at '${CGAL_DIR}'.")
|
||||
endif()
|
||||
|
||||
add_definitions(-DIFOPSH_WITH_CGAL)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DIFOPSH_WITH_CGAL)
|
||||
@@ -0,0 +1,27 @@
|
||||
#
|
||||
# Input variables:
|
||||
# - `EIGEN_DIR`
|
||||
# If input variables are not specified, try to find Eigen3 config.
|
||||
# Input variables could also be provided as environment variables.
|
||||
#
|
||||
# Output targets:
|
||||
# - `Eigen3::Eigen`
|
||||
|
||||
UNIFY_ENVVARS_AND_CACHE(EIGEN_DIR)
|
||||
|
||||
if(EXISTS "${EIGEN_DIR}")
|
||||
# Mimic Eigen3Config.cmake target.
|
||||
add_library(Eigen3::Eigen INTERFACE IMPORTED)
|
||||
target_include_directories(Eigen3::Eigen INTERFACE "${EIGEN_DIR}")
|
||||
else()
|
||||
find_package(Eigen3 CONFIG)
|
||||
if(Eigen3_DIR)
|
||||
message(STATUS "Eigen3: found config at '${Eigen3_DIR}'.")
|
||||
else()
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"EIGEN_DIR is not provided or provided folder doesn't exist (current value: '${EIGEN_DIR}'). "
|
||||
"Also couldn't find Eigen3 as a package."
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
@@ -0,0 +1,109 @@
|
||||
#
|
||||
# Input variables:
|
||||
# - `HDF5_INCLUDE_DIR`
|
||||
# - `HDF5_LIBRARY_DIR`
|
||||
# - `HDF5_LIBRARIES`
|
||||
# If input variables are not specified, try to find HDF5 config.
|
||||
# Input variables could also be provided as environment variables.
|
||||
#
|
||||
# Output variables:
|
||||
# - `HDF5_INCLUDE_DIR`
|
||||
# - `HDF5_LIBRARY_DIR`
|
||||
# - `HDF5_LIBRARIES`
|
||||
#
|
||||
|
||||
UNIFY_ENVVARS_AND_CACHE(HDF5_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARY_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARIES)
|
||||
|
||||
# To avoid cyclic calls to this file
|
||||
list(REMOVE_ITEM CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
|
||||
|
||||
if("${HDF5_INCLUDE_DIR}" STREQUAL "")
|
||||
message(STATUS "No HDF5 include directory specified")
|
||||
else()
|
||||
set(HDF5_INCLUDE_DIR "${HDF5_INCLUDE_DIR}" CACHE FILEPATH "HDF5 header files")
|
||||
endif()
|
||||
|
||||
if("${HDF5_LIBRARY_DIR}" STREQUAL "")
|
||||
message(STATUS "No HDF5 library directory specified")
|
||||
else()
|
||||
set(HDF5_LIBRARY_DIR "${HDF5_LIBRARY_DIR}" CACHE FILEPATH "HDF5 library files")
|
||||
endif()
|
||||
|
||||
if(HDF5_LIBRARY_DIR)
|
||||
# result of the HDF5 ctest package
|
||||
# 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(${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
|
||||
# macOS
|
||||
set(zlib_post libz)
|
||||
set(lib_ext dylib)
|
||||
set(HDF5_LIBRARIES
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5_cpp.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/${zlib_post}.${lib_ext}"
|
||||
)
|
||||
else()
|
||||
# linux and windows
|
||||
# Find HDF5 package
|
||||
find_package(HDF5 REQUIRED COMPONENTS C CXX)
|
||||
# Find ZLIB package
|
||||
find_package(ZLIB REQUIRED)
|
||||
# Include directories
|
||||
include_directories(${HDF5_INCLUDE_DIRS} ${ZLIB_INCLUDE_DIRS})
|
||||
# Link libraries
|
||||
set(HDF5_LIBRARIES ${HDF5_LIBRARIES} ${ZLIB_LIBRARIES})
|
||||
message(STATUS "HDF5 libraries: ${HDF5_LIBRARIES}")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(NOT HDF5_INCLUDE_DIR OR NOT HDF5_LIBRARY_DIR)
|
||||
# First try to find it as a config.
|
||||
find_package(HDF5 CONFIG)
|
||||
if(HDF5_DIR)
|
||||
message(STATUS "HDF5: found config at '${HDF5_DIR}'.")
|
||||
set(HDF5_LIBRARIES hdf5_cpp-static)
|
||||
else()
|
||||
# If it failed, still try to find as a module.
|
||||
# E.g. on Ubuntu `libhdf5-dev` doesn't provie hdf5-config.cmake.
|
||||
# Will automatically fill HDF5_LIBRARIES and HDF5_INCLUDE_DIR.
|
||||
find_package(HDF5 COMPONENTS CXX)
|
||||
if(NOT HDF5_INCLUDE_DIR)
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"HDF5_INCLUDE_DIR is not provided (current value: '${HDF5_INCLUDE_DIR}'). "
|
||||
"HDF5_LIBRARY_DIR is not provided (current value: '${HDF5_LIBRARY_DIR}'). "
|
||||
"Also could not find HDF5 package (neither module or config)."
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Restore module path.
|
||||
list(PREPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
|
||||
@@ -0,0 +1,49 @@
|
||||
#
|
||||
# Input variables:
|
||||
# - `LIBXML2_INCLUDE_DIR`
|
||||
# - `LIBXML2_LIBRARIES`
|
||||
# If input variables are not specified, try to find LibXml2 config.
|
||||
# Input variables could also be provided as environment variables.
|
||||
#
|
||||
# Output variables:
|
||||
# - `LIBXML2_INCLUDE_DIR`
|
||||
# - `LIBXML2_LIBRARIES`
|
||||
#
|
||||
|
||||
# To avoid cyclic calls to this file
|
||||
list(REMOVE_ITEM CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
|
||||
|
||||
UNIFY_ENVVARS_AND_CACHE(LIBXML2_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(LIBXML2_LIBRARIES)
|
||||
|
||||
if((NOT LIBXML2_INCLUDE_DIR AND NOT LIBXML2_LIBRARIES))
|
||||
# First try config mode (probably works with vcpkg, Conan, macOS brew installs, but not on ubuntu 22.04)
|
||||
# CONFIG is provided using root path, so no need to clear sysroot here.
|
||||
find_package(LibXml2 QUIET CONFIG)
|
||||
|
||||
if(NOT LibXml2_FOUND)
|
||||
# Fallback to CMake's builtin FindLibXml2 module (works on Ubuntu)
|
||||
find_package(LibXml2 REQUIRED)
|
||||
else()
|
||||
message(STATUS "Found LibXml2 config: ${LibXml2_DIR}")
|
||||
endif()
|
||||
|
||||
if(TARGET LibXml2::LibXml2)
|
||||
# Config mode already gives us the target
|
||||
set(LIBXML2_LIBRARIES LibXml2::LibXml2)
|
||||
get_target_property(LIBXML2_INCLUDE_DIR LibXml2::LibXml2 INTERFACE_INCLUDE_DIRECTORIES)
|
||||
else()
|
||||
# Module mode (Ubuntu)
|
||||
set(LIBXML2_LIBRARIES ${LibXml2_LIBRARIES})
|
||||
set(LIBXML2_INCLUDE_DIR ${LibXml2_INCLUDE_DIRS})
|
||||
endif()
|
||||
else()
|
||||
find_package(LibXml2 REQUIRED)
|
||||
endif()
|
||||
|
||||
if(MSVC AND NOT LibXml2_DIR)
|
||||
add_debug_variants(LIBXML2_LIBRARIES "${LIBXML2_LIBRARIES}" d)
|
||||
endif()
|
||||
|
||||
# Restore module path.
|
||||
list(PREPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
|
||||
@@ -0,0 +1,138 @@
|
||||
#
|
||||
# Input variables:
|
||||
# - `OCC_INCLUDE_DIR`
|
||||
# - `OCC_LIBRARY_DIR`
|
||||
# If input variables are not specified, try to find OpenCASCADE config.
|
||||
# Input variables could also be provided as environment variables.
|
||||
#
|
||||
# Output variables
|
||||
# - `OpenCASCADE_LIBRARIES`
|
||||
#
|
||||
|
||||
UNIFY_ENVVARS_AND_CACHE(OCC_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(OCC_LIBRARY_DIR)
|
||||
|
||||
if(OCC_INCLUDE_DIR)
|
||||
set(OCC_INCLUDE_DIR ${OCC_INCLUDE_DIR} CACHE FILEPATH "Open CASCADE header files")
|
||||
message(STATUS "Looking for Open CASCADE include files in: ${OCC_INCLUDE_DIR}")
|
||||
endif()
|
||||
|
||||
if(OCC_LIBRARY_DIR)
|
||||
set(OCC_LIBRARY_DIR ${OCC_LIBRARY_DIR} CACHE FILEPATH "Open CASCADE library files")
|
||||
message(STATUS "Looking for Open CASCADE library files in: ${OCC_LIBRARY_DIR}")
|
||||
endif()
|
||||
|
||||
|
||||
if(NOT OCC_INCLUDE_DIR AND NOT OCC_LIBRARY_DIR)
|
||||
# OCE is not supported for find_package, because it's using a different name (`oce`)
|
||||
# and also has an odd directory structure (install/lib/oce-0.18/*.cmake).
|
||||
# find_package creates variables:
|
||||
# - `OpenCASCADE_INCLUDE_DIR`
|
||||
# - `OpenCASCADE_LIBRARIES`
|
||||
find_package(OpenCASCADE CONFIG REQUIRED)
|
||||
return()
|
||||
endif()
|
||||
|
||||
# No specific paths specified, try to find package.
|
||||
if(OCC_INCLUDE_DIR AND OCC_LIBRARY_DIR)
|
||||
message(
|
||||
STATUS
|
||||
"Using provided OCC_INCLUDE_DIR ('${OCC_INCLUDE_DIR}') "
|
||||
"and OCC_LIBRARY_DIR ('${OCC_LIBRARY_DIR}')."
|
||||
)
|
||||
# Parse OCC_VERSION_STRING.
|
||||
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MAJOR
|
||||
REGEX "#define OCC_VERSION_MAJOR.*"
|
||||
)
|
||||
string(REGEX MATCH "[0-9]+" OCC_MAJOR ${OCC_MAJOR})
|
||||
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MINOR
|
||||
REGEX "#define OCC_VERSION_MINOR.*"
|
||||
)
|
||||
string(REGEX MATCH "[0-9]+" OCC_MINOR ${OCC_MINOR})
|
||||
file(STRINGS ${OCC_INCLUDE_DIR}/Standard_Version.hxx OCC_MAINT
|
||||
REGEX "#define OCC_VERSION_MAINTENANCE.*"
|
||||
)
|
||||
string(REGEX MATCH "[0-9]+" OCC_MAINT ${OCC_MAINT})
|
||||
set(OCC_VERSION_STRING "${OCC_MAJOR}.${OCC_MINOR}.${OCC_MAINT}")
|
||||
else()
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"Couldn't find Open CASCADE installation. "
|
||||
"Either both OCC_INCLUDE_DIR ('${OCC_INCLUDE_DIR}') and OCC_LIBRARY_DIR ('${OCC_LIBRARY_DIR}') "
|
||||
"must be specified or OpenCASCADE package should be discoverable. "
|
||||
"If you're using OCE, then providing a package is not available "
|
||||
"and you need to provide OCE_INCLUDE_DIR and OCE_LIBRARY_DIR directly."
|
||||
)
|
||||
endif()
|
||||
|
||||
set(
|
||||
OpenCASCADE_LIBRARIES
|
||||
TKernel TKMath TKBRep TKGeomBase TKGeomAlgo TKG3d TKG2d TKShHealing TKTopAlgo TKMesh TKPrim TKBool TKBO
|
||||
TKFillet TKXSBase TKOffset TKHLR
|
||||
|
||||
# @todo investigate the exact conditions when this is necessary
|
||||
TKBin
|
||||
)
|
||||
|
||||
if(OCC_VERSION_STRING VERSION_LESS 7.8.0)
|
||||
list(APPEND OpenCASCADE_LIBRARIES TKIGES TKSTEPBase TKSTEPAttr TKSTEP209 TKSTEP)
|
||||
else(OCC_VERSION_STRING VERSION_LESS 7.8.0)
|
||||
list(APPEND OpenCASCADE_LIBRARIES TKDESTEP TKDEIGES)
|
||||
endif(OCC_VERSION_STRING VERSION_LESS 7.8.0)
|
||||
|
||||
find_library(libTKernel NAMES TKernel TKerneld PATHS ${OCC_LIBRARY_DIR} NO_DEFAULT_PATH)
|
||||
|
||||
if(libTKernel)
|
||||
message(STATUS "Required Open Cascade Library files found")
|
||||
else()
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"Unable to find Open Cascade library files in OCC_LIBRARY_DIR ('${OCC_LIBRARY_DIR}'), aborting"
|
||||
)
|
||||
endif()
|
||||
|
||||
if(MSVC)
|
||||
add_definitions(-DHAVE_NO_DLL)
|
||||
add_debug_variants(OpenCASCADE_LIBRARIES "${OpenCASCADE_LIBRARIES}" d)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
# OCC might require linking to Winsock depending on the version and build configuration
|
||||
list(APPEND OpenCASCADE_LIBRARIES ws2_32.lib)
|
||||
endif()
|
||||
|
||||
# Make sure cross-referenced symbols between static OCC libraries get
|
||||
# resolved. Also add thread and rt libraries.
|
||||
get_filename_component(libTKernelExt ${libTKernel} EXT)
|
||||
if("${libTKernelExt}" STREQUAL ".a")
|
||||
set(OCCT_STATIC ON)
|
||||
endif()
|
||||
|
||||
if(OCCT_STATIC)
|
||||
find_package(Threads)
|
||||
|
||||
if(WASM_BUILD)
|
||||
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
|
||||
else()
|
||||
# OpenCASCADE_LIBRARIES repeated N times below in order to fix cyclic dependencies
|
||||
# tfk: --start-group ... --end-group didn't work on the apple linker when last tested
|
||||
if(APPLE)
|
||||
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES} ${OpenCASCADE_LIBRARIES} ${OpenCASCADE_LIBRARIES} ${OpenCASCADE_LIBRARIES} ${OpenCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
|
||||
else()
|
||||
set(OpenCASCADE_LIBRARIES -Wl,--start-group ${OpenCASCADE_LIBRARIES} -Wl,--end-group ${CMAKE_THREAD_LIBS_INIT})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(NOT APPLE AND NOT WIN32)
|
||||
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES} "rt")
|
||||
endif()
|
||||
if(NOT WIN32)
|
||||
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES} "dl")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_library(OpenCASCADE_INTERFACE INTERFACE IMPORTED)
|
||||
target_include_directories(OpenCASCADE_INTERFACE INTERFACE "${OCC_INCLUDE_DIR}")
|
||||
target_link_libraries(OpenCASCADE_INTERFACE INTERFACE ${OpenCASCADE_LIBRARIES})
|
||||
target_link_directories(OpenCASCADE_INTERFACE INTERFACE "${OCC_LIBRARY_DIR}")
|
||||
set(OpenCASCADE_LIBRARIES OpenCASCADE_INTERFACE)
|
||||
@@ -0,0 +1,118 @@
|
||||
|
||||
#
|
||||
# Input variables:
|
||||
# - `OPENCOLLADA_INCLUDE_DIR`
|
||||
# - `OPENCOLLADA_LIBRARY_DIR`
|
||||
# - `PCRE_LIBRARY_DIR`
|
||||
# If input variables are not specified, try to find OpenCOLLADA config.
|
||||
# Input variables could also be provided as environment variables.
|
||||
#
|
||||
# Output variables:
|
||||
# - `OPENCOLLADA_INCLUDE_DIR`
|
||||
# - `OPENCOLLADA_LIBRARY_DIR`
|
||||
# - `OPENCOLLADA_LIBRARIES`
|
||||
#
|
||||
|
||||
UNIFY_ENVVARS_AND_CACHE(OPENCOLLADA_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(OPENCOLLADA_LIBRARY_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(PCRE_LIBRARY_DIR)
|
||||
|
||||
if(NOT OPENCOLLADA_INCLUDE_DIR AND NOT OPENCOLLADA_LIBRARY_DIR)
|
||||
# If package is found, automatically sets
|
||||
# OPENCOLLADA_INCLUDE_DIRS and OPENCOLLADA_LIBRARIES (list of targets, not paths).
|
||||
find_package(OpenCOLLADA CONFIG)
|
||||
if(OpenCOLLADA_DIR)
|
||||
message(STATUS "Found OpenCOLLADA: '${OpenCOLLADA_DIR}'.")
|
||||
set(OPENCOLLADA_FOUND TRUE)
|
||||
else()
|
||||
message(STATUS "OpenCOLLADA package not found, falling back to manual search.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(NOT OpenCOLLADA_DIR)
|
||||
# Find OpenCOLLADA
|
||||
if("${OPENCOLLADA_INCLUDE_DIR}" STREQUAL "")
|
||||
message(STATUS "No OpenCOLLADA include directory specified")
|
||||
set(OPENCOLLADA_INCLUDE_DIR "/usr/include/opencollada" CACHE FILEPATH "OpenCOLLADA header files")
|
||||
else()
|
||||
set(OPENCOLLADA_INCLUDE_DIR "${OPENCOLLADA_INCLUDE_DIR}" CACHE FILEPATH "OpenCOLLADA header files")
|
||||
endif()
|
||||
|
||||
if("${OPENCOLLADA_LIBRARY_DIR}" STREQUAL "")
|
||||
message(STATUS "No OpenCOLLADA library directory specified")
|
||||
find_library(OPENCOLLADA_FRAMEWORK_LIB NAMES OpenCOLLADAFramework
|
||||
PATHS /usr/lib64/opencollada /usr/lib/opencollada /usr/lib64 /usr/lib /usr/local/lib64 /usr/local/lib)
|
||||
get_filename_component(OPENCOLLADA_LIBRARY_DIR ${OPENCOLLADA_FRAMEWORK_LIB} PATH)
|
||||
endif()
|
||||
|
||||
find_library(OpenCOLLADAFramework NAMES OpenCOLLADAFramework OpenCOLLADAFrameworkd PATHS ${OPENCOLLADA_LIBRARY_DIR} NO_DEFAULT_PATH)
|
||||
|
||||
if(OpenCOLLADAFramework)
|
||||
message(STATUS "OpenCOLLADA library files found")
|
||||
else()
|
||||
message(FATAL_ERROR "COLLADA_SUPPORT enabled, but unable to find OpenCOLLADA libraries. "
|
||||
"Disable COLLADA_SUPPORT or fix OpenCOLLADA paths to proceed.")
|
||||
endif()
|
||||
|
||||
set(OPENCOLLADA_LIBRARY_DIR "${OPENCOLLADA_LIBRARY_DIR}" CACHE FILEPATH "OpenCOLLADA library files")
|
||||
|
||||
set(OPENCOLLADA_INCLUDE_DIRS "${OPENCOLLADA_INCLUDE_DIR}/COLLADABaseUtils" "${OPENCOLLADA_INCLUDE_DIR}/COLLADAStreamWriter")
|
||||
|
||||
find_file(COLLADASWStreamWriter_h "COLLADASWStreamWriter.h" ${OPENCOLLADA_INCLUDE_DIRS})
|
||||
|
||||
if(COLLADASWStreamWriter_h)
|
||||
message(STATUS "OpenCOLLADA header files found")
|
||||
set(OPENCOLLADA_FOUND TRUE)
|
||||
|
||||
set(OPENCOLLADA_LIBRARY_NAMES
|
||||
GeneratedSaxParser MathMLSolver OpenCOLLADABaseUtils OpenCOLLADAFramework OpenCOLLADASaxFrameworkLoader
|
||||
OpenCOLLADAStreamWriter UTF buffer ftoa
|
||||
)
|
||||
|
||||
# Use the found OpenCOLLADAFramework as a template for all other OpenCOLLADA libraries
|
||||
foreach(lib ${OPENCOLLADA_LIBRARY_NAMES})
|
||||
# Make sure we'll handle the Windows/MSVC debug postfix convention too.
|
||||
string(REPLACE OpenCOLLADAFrameworkd "${lib}" lib_path "${OpenCOLLADAFramework}")
|
||||
string(REPLACE OpenCOLLADAFramework "${lib}" lib_path "${lib_path}")
|
||||
list(APPEND OPENCOLLADA_LIBRARIES "${lib_path}")
|
||||
endforeach()
|
||||
|
||||
if("${PCRE_LIBRARY_DIR}" STREQUAL "")
|
||||
if(WIN32)
|
||||
find_library(pcre_library NAMES pcre pcred PATHS ${OPENCOLLADA_LIBRARY_DIR} NO_DEFAULT_PATH)
|
||||
else()
|
||||
find_library(pcre_library NAMES pcre PATHS ${OPENCOLLADA_LIBRARY_DIR})
|
||||
endif()
|
||||
|
||||
get_filename_component(PCRE_LIBRARY_DIR ${pcre_library} PATH)
|
||||
else()
|
||||
find_library(pcre_library NAMES pcre pcred PATHS ${PCRE_LIBRARY_DIR} NO_DEFAULT_PATH)
|
||||
endif()
|
||||
|
||||
if(pcre_library)
|
||||
set(OPENCOLLADA_LIBRARY_DIR ${OPENCOLLADA_LIBRARY_DIR} ${PCRE_LIBRARY_DIR})
|
||||
|
||||
if(MSVC)
|
||||
# Add release lib regardless whether release or debug found. Debug version will be appended below.
|
||||
list(APPEND OPENCOLLADA_LIBRARIES "${PCRE_LIBRARY_DIR}/pcre.lib")
|
||||
else()
|
||||
list(APPEND OPENCOLLADA_LIBRARIES "${pcre_library}")
|
||||
endif()
|
||||
else()
|
||||
message(FATAL_ERROR "COLLADA_SUPPORT enabled, but unable to find PCRE. "
|
||||
"Disable COLLADA_SUPPORT or fix PCRE_LIBRARY_DIR path to proceed.")
|
||||
endif()
|
||||
|
||||
if(MSVC)
|
||||
add_debug_variants(OPENCOLLADA_LIBRARIES "${OPENCOLLADA_LIBRARIES}" d)
|
||||
endif()
|
||||
else()
|
||||
message(FATAL_ERROR "COLLADA_SUPPORT enabled, but unable to find OpenCOLLADA headers. "
|
||||
"Disable COLLADA_SUPPORT or fix OpenCOLLADA paths to proceed.")
|
||||
endif()
|
||||
endif(NOT OpenCOLLADA_DIR)
|
||||
|
||||
if(OPENCOLLADA_FOUND)
|
||||
add_definitions(-DWITH_OPENCOLLADA)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_OPENCOLLADA)
|
||||
endif()
|
||||
@@ -0,0 +1,54 @@
|
||||
#
|
||||
# Input variables:
|
||||
# - `USD_INCLUDE_DIR`
|
||||
# - `USD_LIBRARY_DIR`
|
||||
# Input variables could also be provided as environment variables.
|
||||
# TODO: Try to find USD config if varibales are not provided.
|
||||
# TODO: does usd have a config file?
|
||||
#
|
||||
# Output variables:
|
||||
# - `USD_LIBRARIES`
|
||||
|
||||
UNIFY_ENVVARS_AND_CACHE(USD_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(USD_LIBRARY_DIR)
|
||||
|
||||
if("${USD_INCLUDE_DIR}" STREQUAL "")
|
||||
find_path(USD_INCLUDE_DIR pxr.h
|
||||
PATHS
|
||||
/usr/include/pxr
|
||||
/usr/local/include/pxr
|
||||
REQUIRED
|
||||
)
|
||||
if(USD_INCLUDE_DIR)
|
||||
message(STATUS "Found USD include files in: ${USD_INCLUDE_DIR}")
|
||||
else()
|
||||
message(FATAL_ERROR "Unable to find USD include directory, specify USD_INCLUDE_DIR manually.")
|
||||
endif()
|
||||
else()
|
||||
set(USD_INCLUDE_DIR ${USD_INCLUDE_DIR} CACHE FILEPATH "USD header files")
|
||||
message(STATUS "Looking for USD include files in: ${USD_INCLUDE_DIR}")
|
||||
endif()
|
||||
|
||||
set(USD_LIBRARIES
|
||||
usd_usd
|
||||
usd_usdGeom
|
||||
usd_usdShade
|
||||
usd_usdLux
|
||||
usd_vt
|
||||
usd_sdf
|
||||
usd_tf
|
||||
usd_gf
|
||||
)
|
||||
|
||||
find_library(USD_LIBRARY
|
||||
NAMES ${USD_LIBRARIES}
|
||||
PATHS ${USD_LIBRARY_DIR})
|
||||
if(USD_LIBRARY)
|
||||
message(STATUS "USD libraries ${USD_LIBRARIES} found in: ${USD_LIBRARY_DIR}")
|
||||
link_directories(${USD_LIBRARY_DIR})
|
||||
else()
|
||||
message(FATAL_ERROR "Unable to find USD libraries in: ${USD_LIBRARY_DIR}")
|
||||
endif()
|
||||
|
||||
add_definitions(-DWITH_USD)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_USD)
|
||||
@@ -42,7 +42,6 @@ cmake -G "Ninja" ^
|
||||
-D Boost_LIBRARY_DIR:FILEPATH="%LIBRARY_PREFIX%\lib" ^
|
||||
-D Boost_INCLUDE_DIR:FILEPATH="%LIBRARY_PREFIX%\include" ^
|
||||
-D Boost_USE_STATIC_LIBS:BOOL=OFF ^
|
||||
-D CITYJSON_SUPPORT:BOOL=OFF ^
|
||||
../cmake
|
||||
|
||||
if errorlevel 1 exit 1
|
||||
|
||||
@@ -42,7 +42,6 @@ cmake ${CMAKE_ARGS} -G Ninja \
|
||||
-DBUILD_IFCGEOM:BOOL=ON \
|
||||
-DBUILD_GEOMSERVER:BOOL=OFF \
|
||||
-DBOOST_USE_STATIC_LIBS:BOOL=OFF \
|
||||
-DCITYJSON_SUPPORT:BOOL=OFF \
|
||||
./cmake
|
||||
|
||||
ninja
|
||||
|
||||
+444
-161
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index 34300d41ad..09b2e0d45f 100644
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -721,6 +721,8 @@ else()
|
||||
OCCT_CHECK_AND_UNSET ("3RDPARTY_DOT_EXECUTABLE")
|
||||
endif()
|
||||
|
||||
+list (REMOVE_ITEM BUILD_TOOLKITS ExpToCasExe)
|
||||
+
|
||||
# bison
|
||||
if (BUILD_YACCLEX)
|
||||
list (APPEND OCCT_3RDPARTY_CMAKE_LIST "adm/cmake/bison")
|
||||
@@ -0,0 +1,13 @@
|
||||
diff --git a/OpenCOLLADAConfig.cmake.in b/OpenCOLLADAConfig.cmake.in
|
||||
index d765b108..6800c2a7 100644
|
||||
--- a/OpenCOLLADAConfig.cmake.in
|
||||
+++ b/OpenCOLLADAConfig.cmake.in
|
||||
@@ -31,7 +31,7 @@ set(OPENCOLLADA_INCLUDE_DIRS
|
||||
include("@PACKAGE_OPENCOLLADA_INST_CMAKECONFIG@/OpenCOLLADATargets.cmake")
|
||||
|
||||
# Set the library variable
|
||||
-if(UNIX)
|
||||
+if(0)
|
||||
set(OPENCOLLADA_LIBRARIES
|
||||
ftoa_shared
|
||||
buffer_shared
|
||||
@@ -0,0 +1,32 @@
|
||||
|
||||
There are two ways to build pyodide ifcopenshell Python wrapper wheel.
|
||||
|
||||
1. Using pyodide build system (`build_pyodide.yml` does it):
|
||||
|
||||
- install prebuilt pyodide build and emscripten environment (see `build_pyodide.sh`)
|
||||
- clone IfcOpenShell to `IfcOpenShell` folder
|
||||
- create `packages/ifcopenshell` folder that will be used by pyodide build system
|
||||
- from `IfcOpenShell` move building recipe `pyodide/meta.yaml` to `packages/ifcopenshell`
|
||||
- run `pyodide build-recipes ifcopenshell --install`, it will
|
||||
- execute `meta.yaml` recipe - it will:
|
||||
- copy IfcOpenShell source to build folder `packages/ifcopenhell/build/ifcopenshell-0.8.0`
|
||||
- build ifcopenshell and its dependencies
|
||||
- note that rerunning `pyodide build-recipes` will remove previous build folder and rebuild all dependencies.
|
||||
The way to avoid it, if build fails, is to use `pyodide build-recipes-no-deps ifcopenshell --continue` instead.
|
||||
- run `setup.py` in `IfcOpenShell` root, producing a wheel in `IfcOpenShell/dist`
|
||||
- copy that wheel to `packages/ifcopenshell/dist`
|
||||
- `--install` it to current build envrionment
|
||||
- copy the wheel next to `dist` folder (in root directory, next to `packages`)
|
||||
- add wheel to `dist/pyodide-lock.json`
|
||||
|
||||
2. Build it outside of pyodide build system.
|
||||
|
||||
Building inside pyodide build system should be preferred, option to build it outside is useful for debugging purposes,
|
||||
since it's pure cmake without any additional moving parts.
|
||||
|
||||
- setup pyodide environment, see above
|
||||
- clone IfcOpenShell repo next to it to `IfcOpenShell` folder
|
||||
- run `python nix/build-all.py -wasm -py-313` in `IfcOpenShell`
|
||||
- it will produce Python package in `IfcOpenShell/ifcopenshell`
|
||||
- run `pyodide build`
|
||||
- it will produce a wheel in `IfcOpenShell/dist`
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/bash
|
||||
set -ex
|
||||
|
||||
# Install uv.
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
uv venv --python 3.13
|
||||
source .venv/bin/activate
|
||||
|
||||
# Install pyodide cross build environment.
|
||||
# Instructions: https://pyodide.org/en/stable/development/building-packages.html
|
||||
uv pip install pyodide-build
|
||||
# `uv run` is required, so xbuildenv would skip using `pip`.
|
||||
uv run pyodide xbuildenv install
|
||||
|
||||
# Emscripten doesn't come with xbuildenv.
|
||||
git clone https://github.com/emscripten-core/emsdk
|
||||
pushd emsdk
|
||||
PYODIDE_EMSCRIPTEN_VERSION=$(pyodide config get emscripten_version)
|
||||
./emsdk install ${PYODIDE_EMSCRIPTEN_VERSION}
|
||||
./emsdk activate ${PYODIDE_EMSCRIPTEN_VERSION}
|
||||
source emsdk_env.sh
|
||||
which emcc
|
||||
popd
|
||||
|
||||
mkdir -p packages/ifcopenshell
|
||||
VERSION=`cat IfcOpenShell/VERSION`
|
||||
cp IfcOpenShell/pyodide/meta.yaml packages/ifcopenshell
|
||||
sed -i s/0.8.0/$VERSION/g packages/ifcopenshell/meta.yaml
|
||||
|
||||
# Use custom build ifcopenshell directory in build-all to make caching simpler
|
||||
# Otherwise pyodide build path typically includes package version, so cached cmake configs might break.
|
||||
export BUILD_DIR=`readlink -f ifcopenshell_build`
|
||||
|
||||
# Use build-recipes-no-deps first, so logs would be printed to stdout.
|
||||
pyodide build-recipes-no-deps ifcopenshell
|
||||
pyodide build-recipes ifcopenshell --install
|
||||
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
Cache built dependencies for builds.
|
||||
|
||||
This script is finding common install directory and either
|
||||
packs each folder into a tar.gz archive, if it wasn't packed before,
|
||||
or unpacks existing archives.
|
||||
|
||||
Usage: python cache_dependencies.py [pack|unpack]
|
||||
"""
|
||||
|
||||
import tarfile
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
|
||||
CACHE_PREFIX = "cache-"
|
||||
|
||||
|
||||
def get_install_dir() -> Path:
|
||||
for data in Path.cwd().glob("*/*/install"):
|
||||
return data
|
||||
raise Exception("No install dir found")
|
||||
|
||||
|
||||
def pack_dependencies(install_dir: Path) -> None:
|
||||
# Process each install_dir
|
||||
for dependency_path in install_dir.iterdir():
|
||||
if not dependency_path.is_dir():
|
||||
continue
|
||||
dependency_name = dependency_path.name
|
||||
tar_path = install_dir / f"{CACHE_PREFIX}{dependency_name}.tar.gz"
|
||||
if tar_path.exists():
|
||||
print(f"Skipping existing cache: '{tar_path}'")
|
||||
else:
|
||||
with tarfile.open(tar_path, "w:gz") as tar:
|
||||
tar.add(dependency_path, arcname=dependency_path.name)
|
||||
print(f"Created cache: '{tar_path}'")
|
||||
|
||||
|
||||
def unpack_dependencies(install_dir: Path) -> None:
|
||||
# `filter` argument was fully introduced in 3.12
|
||||
# and results in deprecation warnings in 3.12-3.13, if not provided.
|
||||
tar_filter: dict[Literal["filter"], Literal["data"]] = (
|
||||
{"filter": "data"} if bool(sys.version_info >= (3, 12)) else {}
|
||||
)
|
||||
for tar_path in install_dir.glob(f"{CACHE_PREFIX}*.tar.gz"):
|
||||
with tarfile.open(tar_path, "r:gz") as tar:
|
||||
tar.extractall(path=install_dir, **tar_filter)
|
||||
print(f"Extracted cache: '{tar_path.name}'.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2 or (action := sys.argv[1].lower()) not in ("pack", "unpack"):
|
||||
print(__doc__)
|
||||
sys.exit(1)
|
||||
|
||||
install_dir = get_install_dir()
|
||||
print(f"Found install dir: '{install_dir}'")
|
||||
|
||||
if action == "pack":
|
||||
pack_dependencies(install_dir)
|
||||
else:
|
||||
unpack_dependencies(install_dir)
|
||||
+2
-3
@@ -3,13 +3,12 @@ package:
|
||||
version: 0.8.0
|
||||
|
||||
source:
|
||||
# meta.yaml is placed as `packages/ifcopenshell/meta.yaml`.
|
||||
path: ../../IfcOpenShell
|
||||
|
||||
build:
|
||||
script: |
|
||||
BUILD_CFG=Release python nix/build-all.py --without-rocksdb --without-hdf5 --without-opencollada --without-swig --without-pcre -v --wasm --py313 IfcOpenShell-Python
|
||||
mv package/ifcopenshell .
|
||||
cp pyodide/setup.py .
|
||||
BUILD_CFG=Release python nix/build-all.py -v --wasm --py313
|
||||
|
||||
about:
|
||||
home: http://ifcopenshell.org
|
||||
|
||||
+45
-9
@@ -1,11 +1,47 @@
|
||||
from setuptools import setup, find_packages
|
||||
# setup.py is getting deprecated, but we still use it,
|
||||
# because `tool.setuptools.ext-modules` is still experimental in pyproject.toml
|
||||
# and we need it to get the wheel suffix right.
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
setup(name='ifcopenshell',
|
||||
version='0.8.0',
|
||||
description='IfcOpenShell is an open source (LGPL) software library for working with the Industry Foundation Classes (IFC) file format.',
|
||||
author='Thomas Krijnen',
|
||||
author_email='thomas@aecgeeks.com',
|
||||
url='http://ifcopenshell.org',
|
||||
packages=find_packages(),
|
||||
package_data={'': ['*.so', '*.json']},
|
||||
import tomllib
|
||||
from setuptools import Extension, find_packages, setup
|
||||
|
||||
REPO_FOLDER = Path(__file__).parent
|
||||
|
||||
|
||||
def get_version() -> str:
|
||||
if "PKG_VERSION" in os.environ:
|
||||
# Inside pyodide build environment.
|
||||
return os.environ["PKG_VERSION"]
|
||||
return (REPO_FOLDER / "VERSION").read_text().strip()
|
||||
|
||||
|
||||
# Read dependencies from pyproject.toml
|
||||
def get_dependencies() -> list[str]:
|
||||
pyproject_toml = REPO_FOLDER / "src" / "ifcopenshell-python" / "pyproject.toml"
|
||||
pyproject_data = tomllib.loads(pyproject_toml.read_text())
|
||||
dependencies = pyproject_data["project"]["dependencies"]
|
||||
return dependencies
|
||||
|
||||
|
||||
setup(
|
||||
name="ifcopenshell",
|
||||
version=get_version(),
|
||||
description=(
|
||||
"IfcOpenShell is an open source (LGPL) software library "
|
||||
"for working with the Industry Foundation Classes (IFC) file format."
|
||||
),
|
||||
author="Thomas Krijnen",
|
||||
author_email="thomas@aecgeeks.com",
|
||||
url="https://ifcopenshell.org",
|
||||
install_requires=get_dependencies(),
|
||||
packages=find_packages(include=["ifcopenshell", "ifcopenshell.*"]),
|
||||
package_data={
|
||||
# "*.so" is needed to include prebuilt binary extension. Otherwise it would try to build it and fail.
|
||||
"ifcopenshell": ["util/schema/*.json", "util/schema/*.ifc", "*.so"],
|
||||
"": ["*.json", "*.ifc"],
|
||||
},
|
||||
# Has to provide extension to get the correct wheel suffix.
|
||||
ext_modules=[Extension("ifcopenshell._ifcopenshell_wrapper", sources=[])],
|
||||
)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
WHEEL_FILENAME = next(
|
||||
p.name for p in (Path.cwd() / "pyodide").iterdir() if p.name.startswith("ifcopenshell-") and p.suffix == ".whl"
|
||||
)
|
||||
|
||||
|
||||
def test_ifcopenshell_import(selenium):
|
||||
selenium.load_package("micropip")
|
||||
# Important to test it with `micropip.install`
|
||||
# without any dependencies loaded to ensure micropip will load them automatically.
|
||||
selenium.run_async(
|
||||
f"""
|
||||
import micropip
|
||||
await micropip.install(f"./{WHEEL_FILENAME}")
|
||||
import ifcopenshell
|
||||
ifc_file = ifcopenshell.file()
|
||||
wall = ifc_file.create_entity("IfcWall")
|
||||
wall1 = ifc_file.by_type("IfcWall")[0]
|
||||
print(wall, wall1)
|
||||
assert wall == wall1, "Wall entity doesn't match"
|
||||
wall.Name = "Test"
|
||||
assert wall.Name == "Test", f"Entity name wasn't changed: {{wall}}"
|
||||
print(wall)
|
||||
"""
|
||||
)
|
||||
+13
-7
@@ -9,10 +9,8 @@ extend-exclude = '''
|
||||
|src/ifcopenshell-python/ifcopenshell/mvd/*
|
||||
|src/ifcopenshell-python/ifcopenshell/simple_spf/*
|
||||
|src/ifc2ca/templates/*
|
||||
|src/ifcconvert/cityjson/*
|
||||
|src/svgfill
|
||||
|src/exterior-shell-extractor
|
||||
|src/pyodide
|
||||
'''
|
||||
|
||||
[tool.pyright]
|
||||
@@ -21,12 +19,11 @@ disableBytesTypePromotions = true
|
||||
reportUnnecessaryTypeIgnoreComment = true
|
||||
|
||||
# Define here general ruff settings,
|
||||
# then they will be inherited projects .toml files.
|
||||
# then they will be inherited by projects' .toml files.
|
||||
# This allows using assuming different Python version for different projects.
|
||||
[tool.ruff]
|
||||
exclude = [
|
||||
# Submodules.
|
||||
"src/ifcconvert/cityjson",
|
||||
"src/ifcopenshell-python/ifcopenshell/express",
|
||||
"src/ifcopenshell-python/ifcopenshell/mvd",
|
||||
"src/ifcopenshell-python/ifcopenshell/simple_spf",
|
||||
@@ -38,6 +35,7 @@ exclude = [
|
||||
"src/ifc2ca/templates",
|
||||
]
|
||||
[tool.ruff.lint]
|
||||
preview = true
|
||||
select = [
|
||||
# Default Ruff rules.
|
||||
# "E4", # imports
|
||||
@@ -60,7 +58,15 @@ ignore = [
|
||||
"UP030", # implicit references for positional format fields
|
||||
"UP031", # Replace % with .format
|
||||
"UP032", # Replace .format with f-string
|
||||
|
||||
# Deprecated Ruff rules.
|
||||
"UP038", # Use X | Y in isinstance
|
||||
]
|
||||
|
||||
[tool.poe.tasks]
|
||||
|
||||
ruff-main = "ruff check --extend-exclude nix/build-all.py"
|
||||
# It's actually Python 3.6, but ruff only supports 3.7+, but it should do.
|
||||
ruff-old = "ruff check nix/build-all.py --target-version py37"
|
||||
ruff.sequence = ["ruff-main", "ruff-old"]
|
||||
|
||||
black = "black ."
|
||||
|
||||
format.sequence = ["black", "ruff-main", "ruff-old"]
|
||||
|
||||
+3
-12
@@ -6,7 +6,7 @@ from numpy.typing import NDArray
|
||||
|
||||
def camera_vectors_from_element_placement(
|
||||
elem_placement: NDArray[np.float64],
|
||||
) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
|
||||
) -> tuple[list[float], list[float], list[float]]:
|
||||
"""
|
||||
Calculate the vectors of a camera pointing to an element.
|
||||
|
||||
@@ -22,7 +22,7 @@ def camera_vectors_from_element_placement(
|
||||
|
||||
def camera_vectors_from_target_position(
|
||||
target_position: NDArray[np.float64], offset: Optional[NDArray[np.float64]] = None
|
||||
) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
|
||||
) -> tuple[list[float], list[float], list[float]]:
|
||||
"""
|
||||
Calculate the vectors of a camera pointing to a target point.
|
||||
|
||||
@@ -38,16 +38,7 @@ def camera_vectors_from_target_position(
|
||||
camera_direction = unit_vector(-camera_offset) # pylint: disable=invalid-unary-operand-type
|
||||
camera_right = unit_vector(np.cross(np.array([0.0, 0.0, 1.0]), camera_direction))
|
||||
camera_up = unit_vector(np.cross(camera_direction, camera_right))
|
||||
return camera_position, camera_direction, camera_up
|
||||
# rotation_transform = np.eye(4)
|
||||
# rotation_transform[0, :3] = camera_right
|
||||
# rotation_transform[1, :3] = camera_up
|
||||
# rotation_transform[2, :3] = camera_direction
|
||||
# translation_transform = np.eye(4)
|
||||
# translation_transform[:3, -1] = -camera_position
|
||||
# look_at_transform = np.matmul(rotation_transform, translation_transform)
|
||||
# mat = np.linalg.inv(look_at_transform)
|
||||
# return camera_position, -mat[:3, 2], mat[:3, 1]
|
||||
return camera_position.tolist(), camera_direction.tolist(), camera_up.tolist()
|
||||
|
||||
|
||||
def unit_vector(v: NDArray[np.float64]) -> NDArray[np.float64]:
|
||||
|
||||
@@ -192,6 +192,8 @@ class TopicHandler:
|
||||
bcf_zip: The BCF zip file to save to.
|
||||
"""
|
||||
topic_dir = self.guid
|
||||
# simulating directory creation (ZipFile in python < 3.11 doesn't have mkdir)
|
||||
destination_zip.writestr(f"{topic_dir}/", "")
|
||||
self._save_xml(destination_zip, self._markup, "markup.bcf")
|
||||
self._save_viewpoints(destination_zip, topic_dir)
|
||||
self._save_bim_snippet(destination_zip)
|
||||
|
||||
@@ -319,9 +319,7 @@ def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo:
|
||||
ifc_file = element.wrapped_data.file
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
elem_placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
|
||||
elem_placement[0][3] *= unit_scale
|
||||
elem_placement[1][3] *= unit_scale
|
||||
elem_placement[2][3] *= unit_scale
|
||||
elem_placement[:3, 3] *= unit_scale
|
||||
|
||||
return mdl.VisualizationInfo(
|
||||
guid=str(uuid.uuid4()),
|
||||
@@ -381,7 +379,7 @@ def build_camera(elem_placement: NDArray[np.float64]) -> mdl.PerspectiveCamera:
|
||||
|
||||
|
||||
def build_camera_from_vectors(
|
||||
camera_position: NDArray[np.float64], camera_dir: NDArray[np.float64], camera_up: NDArray[np.float64]
|
||||
camera_position: list[float], camera_dir: list[float], camera_up: list[float]
|
||||
) -> mdl.PerspectiveCamera:
|
||||
"""
|
||||
Return a BCF camera for an IFC element placement matrix.
|
||||
|
||||
@@ -179,6 +179,8 @@ class TopicHandler:
|
||||
bcf_zip: The BCF zip file to save to.
|
||||
"""
|
||||
topic_dir = self.guid
|
||||
# simulating directory creation (ZipFile in python < 3.11 doesn't have mkdir)
|
||||
destination_zip.writestr(f"{topic_dir}/", "")
|
||||
self._save_xml(destination_zip, self._markup, "markup.bcf")
|
||||
self._save_viewpoints(destination_zip, topic_dir)
|
||||
self._save_bim_snippet(destination_zip)
|
||||
|
||||
@@ -319,9 +319,7 @@ def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo:
|
||||
ifc_file = element.wrapped_data.file
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
elem_placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
|
||||
elem_placement[0][3] *= unit_scale
|
||||
elem_placement[1][3] *= unit_scale
|
||||
elem_placement[2][3] *= unit_scale
|
||||
elem_placement[:3, 3] *= unit_scale
|
||||
|
||||
return mdl.VisualizationInfo(
|
||||
guid=str(uuid.uuid4()),
|
||||
@@ -381,7 +379,7 @@ def build_camera(elem_placement: NDArray[np.float64]) -> mdl.PerspectiveCamera:
|
||||
|
||||
|
||||
def build_camera_from_vectors(
|
||||
camera_position: NDArray[np.float64], camera_dir: NDArray[np.float64], camera_up: NDArray[np.float64]
|
||||
camera_position: list[float], camera_dir: list[float], camera_up: list[float]
|
||||
) -> mdl.PerspectiveCamera:
|
||||
"""
|
||||
Return a BCF camera for an IFC element placement matrix.
|
||||
|
||||
+5
-2
@@ -87,7 +87,7 @@ BLENDER_PLATFORM:=windows-x64
|
||||
endif
|
||||
|
||||
# Current build commit hash.
|
||||
OLD:=e795cec
|
||||
OLD:=e8eb5e4
|
||||
.PHONY: bump
|
||||
bump:
|
||||
ifndef NEW
|
||||
@@ -127,7 +127,7 @@ endif
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download GitPython --dest=./wheels
|
||||
# Provides audio playback for costing
|
||||
# This is a REALLY IMPORTANT feature
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) wheel git+https://github.com/Andrej730/aud.git --wheel-dir=./wheels
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) wheel git+https://github.com/zdhoward/aud --wheel-dir=./wheels
|
||||
# IfcOpenShell dependency - support for new typing features
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download typing_extensions --dest=./wheels
|
||||
# Required by IfcCSV
|
||||
@@ -155,6 +155,9 @@ endif
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download six --dest=./wheels
|
||||
# Required by drawing module
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download lxml $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
|
||||
# Required by drawing module for markdown hyperlinks in annotations
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download markdown-it-py --dest=./wheels
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download mdurl --dest=./wheels
|
||||
# Required by qto and drawing module
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download shapely $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
|
||||
# Required by the BIM type manager thumbnail generator
|
||||
|
||||
@@ -21,7 +21,7 @@ import bpy
|
||||
import bpy.utils.previews
|
||||
import importlib
|
||||
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
||||
from . import handler, ui, prop, operator
|
||||
from . import handler, ui, prop, operator, gizmo
|
||||
from typing import Union
|
||||
from collections.abc import Callable
|
||||
|
||||
@@ -148,7 +148,9 @@ classes = [
|
||||
ui.BIM_UL_clipping_plane,
|
||||
ui.BIM_UL_generic,
|
||||
ui.DocPreferences,
|
||||
ui.BIM_ADDON_preferences,
|
||||
ui.GizmoPreferencesDoor, # Register before GizmoPreferences
|
||||
ui.GizmoPreferences,
|
||||
# ui.DefaultParameters and ui.BIM_ADDON_preferences are registered separately after modules (see late_classes below)
|
||||
# Tabs panel
|
||||
ui.BIM_PT_tabs,
|
||||
# Project overview
|
||||
@@ -202,6 +204,14 @@ classes = [
|
||||
ui.BIM_PT_section_with_cappings,
|
||||
ui.BIM_PT_decorators_overlay,
|
||||
ui.BIM_PT_snappping,
|
||||
# Gizmos
|
||||
gizmo.GizmoArrow,
|
||||
gizmo.GizmoCone,
|
||||
gizmo.GizmoLock,
|
||||
gizmo.GizmoArc,
|
||||
gizmo.GizmoPen,
|
||||
gizmo.GizmoValidate,
|
||||
gizmo.GizmoCancel,
|
||||
]
|
||||
|
||||
for mod in modules.values():
|
||||
@@ -225,14 +235,33 @@ def on_register(scene):
|
||||
is_registering = False
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
# Classes that need to be registered after modules (due to cross-module dependencies)
|
||||
late_classes = (
|
||||
ui.DefaultParameters, # Register before BIM_ADDON_preferences
|
||||
ui.BIM_ADDON_preferences,
|
||||
)
|
||||
|
||||
|
||||
def register_classes(classes_to_register):
|
||||
for cls in classes_to_register:
|
||||
# Prevent crashes in Blender 4.4.0, see #6420.
|
||||
if issubclass(cls, (ImportHelper, ExportHelper)):
|
||||
assert getattr(cls, "bl_description", "") or cls.__doc__, cls
|
||||
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
|
||||
def unregister_classes(classes_to_unregister):
|
||||
for cls in reversed(classes_to_unregister):
|
||||
if getattr(cls, "is_registered", None) is None:
|
||||
bpy.utils.unregister_class(cls)
|
||||
elif cls.is_registered:
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
|
||||
def register():
|
||||
register_classes(classes)
|
||||
|
||||
bpy.app.handlers.depsgraph_update_post.append(on_register)
|
||||
bpy.app.handlers.undo_post.append(handler.undo_post)
|
||||
bpy.app.handlers.redo_post.append(handler.redo_post)
|
||||
@@ -256,6 +285,9 @@ def register():
|
||||
for mod in modules.values():
|
||||
mod.register()
|
||||
|
||||
# Delay registering classes that depend on module classes
|
||||
register_classes(late_classes)
|
||||
|
||||
wm = bpy.context.window_manager
|
||||
if wm.keyconfigs.addon:
|
||||
km = wm.keyconfigs.addon.keymaps.new(name="Window", space_type="EMPTY")
|
||||
@@ -287,11 +319,7 @@ def unregister():
|
||||
|
||||
bpy.utils.previews.remove(icons)
|
||||
|
||||
for cls in reversed(classes):
|
||||
if getattr(cls, "is_registered", None) is None:
|
||||
bpy.utils.unregister_class(cls)
|
||||
elif cls.is_registered:
|
||||
bpy.utils.unregister_class(cls)
|
||||
unregister_classes(classes)
|
||||
|
||||
bpy.app.handlers.load_post.remove(handler.load_post)
|
||||
bpy.app.handlers.load_post.remove(handler.loadIfcStore)
|
||||
@@ -306,6 +334,9 @@ def unregister():
|
||||
bpy.types.UI_MT_button_context_menu.remove(ui.draw_custom_context_menu)
|
||||
bpy.types.STATUSBAR_HT_header.remove(ui.draw_statusbar)
|
||||
|
||||
# Unregister late classes before modules
|
||||
unregister_classes(late_classes)
|
||||
|
||||
for mod in reversed(list(modules.values())):
|
||||
mod.unregister()
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 70 KiB |
@@ -7,12 +7,6 @@
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"scene.render.bake_bias": 0.0010000000474974513,
|
||||
"scene.render.bake_margin": 16,
|
||||
"scene.render.bake_margin_type": "ADJACENT_FACES",
|
||||
"scene.render.bake_samples": 256,
|
||||
"scene.render.bake_type": "NORMALS",
|
||||
"scene.render.bake_user_scale": 0.0,
|
||||
"scene.render.border_max_x": 1.0,
|
||||
"scene.render.border_max_y": 1.0,
|
||||
"scene.render.border_min_x": 0.0,
|
||||
@@ -54,11 +48,6 @@
|
||||
"scene.render.stamp_note_text": "",
|
||||
"scene.render.threads": 20,
|
||||
"scene.render.threads_mode": "AUTO",
|
||||
"scene.render.use_bake_clear": true,
|
||||
"scene.render.use_bake_lores_mesh": false,
|
||||
"scene.render.use_bake_multires": false,
|
||||
"scene.render.use_bake_selected_to_active": false,
|
||||
"scene.render.use_bake_user_scale": false,
|
||||
"scene.render.use_border": false,
|
||||
"scene.render.use_compositing": true,
|
||||
"scene.render.use_crop_to_border": false,
|
||||
@@ -253,12 +242,6 @@
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"scene.render.bake_bias": 0.0010000000474974513,
|
||||
"scene.render.bake_margin": 16,
|
||||
"scene.render.bake_margin_type": "ADJACENT_FACES",
|
||||
"scene.render.bake_samples": 256,
|
||||
"scene.render.bake_type": "NORMALS",
|
||||
"scene.render.bake_user_scale": 0.0,
|
||||
"scene.render.border_max_x": 1.0,
|
||||
"scene.render.border_max_y": 1.0,
|
||||
"scene.render.border_min_x": 0.0,
|
||||
@@ -300,11 +283,6 @@
|
||||
"scene.render.stamp_note_text": "",
|
||||
"scene.render.threads": 20,
|
||||
"scene.render.threads_mode": "AUTO",
|
||||
"scene.render.use_bake_clear": true,
|
||||
"scene.render.use_bake_lores_mesh": false,
|
||||
"scene.render.use_bake_multires": false,
|
||||
"scene.render.use_bake_selected_to_active": false,
|
||||
"scene.render.use_bake_user_scale": false,
|
||||
"scene.render.use_border": false,
|
||||
"scene.render.use_compositing": true,
|
||||
"scene.render.use_crop_to_border": false,
|
||||
@@ -377,7 +355,7 @@
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"scene.display.shading.studio_light": "Default",
|
||||
"scene.display.shading.studio_light": "forest.exr",
|
||||
"scene.display.shading.studiolight_background_alpha": 0.0,
|
||||
"scene.display.shading.studiolight_background_blur": 0.0,
|
||||
"scene.display.shading.studiolight_intensity": 0.0,
|
||||
@@ -503,18 +481,12 @@
|
||||
"scene.eevee.shadow_ray_count": 1,
|
||||
"scene.eevee.shadow_step_count": 6,
|
||||
"scene.eevee.shadow_resolution_scale": 1.0,
|
||||
"scene.render.bake_bias": 0.0010000000474974513,
|
||||
"scene.render.bake_margin": 16,
|
||||
"scene.render.bake_margin_type": "ADJACENT_FACES",
|
||||
"scene.render.bake_samples": 256,
|
||||
"scene.render.bake_type": "NORMALS",
|
||||
"scene.render.bake_user_scale": 0.0,
|
||||
"scene.render.border_max_x": 1.0,
|
||||
"scene.render.border_max_y": 1.0,
|
||||
"scene.render.border_min_x": 0.0,
|
||||
"scene.render.border_min_y": 0.0,
|
||||
"scene.render.dither_intensity": 1.0,
|
||||
"scene.render.engine": "BLENDER_EEVEE_NEXT",
|
||||
"scene.render.engine": "BLENDER_EEVEE",
|
||||
"scene.render.film_transparent": false,
|
||||
"scene.render.filter_size": 1.5,
|
||||
"scene.render.fps": 24,
|
||||
@@ -550,11 +522,6 @@
|
||||
"scene.render.stamp_note_text": "",
|
||||
"scene.render.threads": 20,
|
||||
"scene.render.threads_mode": "AUTO",
|
||||
"scene.render.use_bake_clear": true,
|
||||
"scene.render.use_bake_lores_mesh": false,
|
||||
"scene.render.use_bake_multires": false,
|
||||
"scene.render.use_bake_selected_to_active": false,
|
||||
"scene.render.use_bake_user_scale": false,
|
||||
"scene.render.use_border": false,
|
||||
"scene.render.use_compositing": true,
|
||||
"scene.render.use_crop_to_border": false,
|
||||
@@ -627,7 +594,7 @@
|
||||
0.800000011920929,
|
||||
0.800000011920929
|
||||
],
|
||||
"scene.display.shading.studio_light": "Default",
|
||||
"scene.display.shading.studio_light": "forest.exr",
|
||||
"scene.display.shading.studiolight_background_alpha": 0.0,
|
||||
"scene.display.shading.studiolight_background_blur": 0.0,
|
||||
"scene.display.shading.studiolight_intensity": 0.0,
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 215 B |
Binary file not shown.
|
After Width: | Height: | Size: 217 B |
@@ -13,10 +13,10 @@ DATA;
|
||||
#6=IFCSIMPLEPROPERTYTEMPLATE('0AK5C2UpL4$eaac2LszAx$',$,'HasUnderlay','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#7=IFCSIMPLEPROPERTYTEMPLATE('2j2ZEZR8X5tONm7kli5hM6',$,'HasLinework','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#8=IFCSIMPLEPROPERTYTEMPLATE('1ttChRysH9UuEX2FeMj5Hu',$,'HasAnnotation','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#9=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#9=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','Whether or not this drawing can be referenced in other drawings.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#10=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','Comma separated list of selector expressions to evaluate for each drawing elementand add results to their ''class'' attribute.\X2\000A\X0\E.g. ''Name, id'' would add to ''class'' value similar to ''Name-Wall id-1220''.\X2\000A\X0\Then it can be used to applied css styles based on the resulting class.\X2\000A\X0\If attribute is not present on the element, then it won''t be added to it''s ''class''.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#11=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','Selector expression to include ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#12=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','Selector expression to exclude ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#11=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','Selector expression to include elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#12=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','Selector expression to exclude elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#13=IFCSIMPLEPROPERTYTEMPLATE('0c1$8NpYDEaBiJrj16jHIo',$,'Stylesheet','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#14=IFCSIMPLEPROPERTYTEMPLATE('3mRF52q81FQB$h4oTh7M45',$,'Markers','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#15=IFCSIMPLEPROPERTYTEMPLATE('1rhr_0N3LDtuORcEJP0KXM',$,'Symbols','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
|
||||
@@ -5,10 +5,10 @@ FILE_NAME('Psets_BBIM_Annotation.ifc','2020-01-01T00:00:00',$,$,'Psets_BBIM_Anno
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation,IfcTypeProduct',(#2,#3,#4,#29));
|
||||
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation,IfcTypeProduct',(#4,#33,#29,#32,#3,#2));
|
||||
#2=IFCSIMPLEPROPERTYTEMPLATE('2P7JN79n96Q9pElZ83LKe4',$,'ZIndex','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
|
||||
#3=IFCSIMPLEPROPERTYTEMPLATE('1Wpx_r2xj1_9w5JpI0QRJy',$,'Symbol','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#4=IFCSIMPLEPROPERTYTEMPLATE('3q0oxMUKP47vZ4jnyG$dDb',$,'Classes','Classes separarated by spaces that end up in classes for this element in svg. Can be used to specify the text font size: small - 1.8mm; regular - 2.5mm; large - 3.5mm; header - 5mm; title - 7mm. By default regular size is used.',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#4=IFCSIMPLEPROPERTYTEMPLATE('3q0oxMUKP47vZ4jnyG$dDb',$,'Classes','Classes separated by spaces that end up in classes for this element in svg. Can be used to specify the text font size: small - 1.8mm; regular - 2.5mm; large - 3.5mm; header - 5mm; title - 7mm. By default regular size is used.',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#5=IFCPROPERTYSETTEMPLATE('0iKwujnQL9IevVQato8f7Z',$,'BBIM_Batting','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/BATTING,IfcTypeProduct',(#6,#7));
|
||||
#6=IFCSIMPLEPROPERTYTEMPLATE('0t2LEesGT1QRQtrIZUAR8L',$,'Thickness','Batting thickness',.P_SINGLEVALUE.,'IfcPositiveLengthMeasure',$,$,$,$,$,.READWRITE.);
|
||||
#7=IFCSIMPLEPROPERTYTEMPLATE('082PndS6v2kBOiJoSboMnh',$,'Reverse pattern direction','Reverse batting pattern (swap starting and ending points)',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
@@ -36,5 +36,7 @@ DATA;
|
||||
#29=IFCSIMPLEPROPERTYTEMPLATE('2pJmUDpB50VBdCOib1zcJJ',$,'Newline_At','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
|
||||
#30=IFCSIMPLEPROPERTYTEMPLATE('2TJn72t_v2cvBUG916Dpev',$,'CustomUnit','Dimension''s custom unit',.P_ENUMERATEDVALUE.,'IfcText',$,#31,$,$,$,.READWRITE.);
|
||||
#31=IFCPROPERTYENUMERATION('CustomUnit',(IFCTEXT('Feet and Inches - Fractional'),IFCTEXT('Feet - Decimal'),IFCTEXT('Inches - Fractional'),IFCTEXT('Inches - Decimal'),IFCTEXT('Meters'),IFCTEXT('Decimeters'),IFCTEXT('Centimeters'),IFCTEXT('Millimeters')),$);
|
||||
#32=IFCSIMPLEPROPERTYTEMPLATE('0gjJzDYBX8P85qn1xcAOOo',$,'Reverse_List','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#33=IFCSIMPLEPROPERTYTEMPLATE('22TrcxF8jFNB4buSmzjGEF',$,'List_Separator','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
|
||||
@@ -25,6 +25,7 @@ import zipfile
|
||||
import tempfile
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.unit
|
||||
import bonsai.tool as tool
|
||||
@@ -36,6 +37,7 @@ from bonsai.bim.ifc import IfcStore
|
||||
from mathutils import Vector
|
||||
from typing import Union
|
||||
from logging import Logger
|
||||
from math import radians
|
||||
|
||||
|
||||
class IfcExporter:
|
||||
@@ -104,7 +106,16 @@ class IfcExporter:
|
||||
|
||||
def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
|
||||
element = self.file.by_id(tool.Blender.get_object_bim_props(obj).ifc_definition_id)
|
||||
if tool.Geometry.is_scaled(obj):
|
||||
# Handle camera scales specially
|
||||
if obj.type == "CAMERA":
|
||||
# Check if this is a reflected ceiling plan camera
|
||||
camera = tool.Ifc.get_entity(obj)
|
||||
if ifcopenshell.util.element.get_pset(camera, "EPset_Drawing", "TargetView") == "REFLECTED_PLAN_VIEW":
|
||||
# Ensure reflected ceiling cameras have the correct scale
|
||||
if obj.scale != (-1, -1, -1):
|
||||
obj.scale = (-1, -1, -1)
|
||||
# Skip all other scale handling for cameras
|
||||
elif tool.Geometry.is_scaled(obj):
|
||||
bpy.ops.bim.update_representation(obj=obj.name)
|
||||
# update_representation might not apply scale if the object has openings
|
||||
# reset it, so let user know that the scale wasn't saved.
|
||||
|
||||
@@ -0,0 +1,805 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai 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.
|
||||
#
|
||||
# Bonsai 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 Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Shared gizmo components for reuse across modules.
|
||||
"""
|
||||
|
||||
import bpy
|
||||
import math
|
||||
from typing import Optional
|
||||
from mathutils import Vector
|
||||
from mathutils.bvhtree import BVHTree
|
||||
from mathutils.geometry import intersect_line_line
|
||||
from bpy_extras.view3d_utils import region_2d_to_vector_3d, region_2d_to_origin_3d
|
||||
import gpu
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
|
||||
SNAP_CIRCLE_SEGMENTS = 16
|
||||
SNAP_CIRCLE_RADIUS = 0.1
|
||||
SNAP_CIRCLE_COLOR = (1.0, 0.5, 0.0, 1.0)
|
||||
SNAP_LINE_WIDTH = 3.0
|
||||
SNAP_MAX_RADIUS = 10.0
|
||||
|
||||
ARROW_SHAFT_LENGTH = 0.8
|
||||
ARROW_HEAD_LENGTH = 0.2
|
||||
ARROW_WIDTH = 0.015
|
||||
ARROW_HEAD_WIDTH_MULTIPLIER = 10
|
||||
ARROW_CIRCLE_SEGMENTS = 8
|
||||
|
||||
CONE_LENGTH = 1.0
|
||||
CONE_RADIUS = 0.35
|
||||
CONE_SEGMENTS = 16
|
||||
|
||||
ARC_SEGMENTS = 24
|
||||
ARC_LINE_WIDTH = 0.015
|
||||
|
||||
PRECISION_MODE_MULTIPLIER = 0.1
|
||||
|
||||
|
||||
class SnapManager:
|
||||
"""Manages snap point visualization and mesh snapping."""
|
||||
|
||||
def __init__(self):
|
||||
self._snap_point: Optional[tuple[float, float, float]] = None
|
||||
self._draw_handler = None
|
||||
self._shader = None
|
||||
|
||||
def set_snap_point(self, point: Optional[tuple[float, float, float]]) -> None:
|
||||
"""Set snap point and register draw handler if needed."""
|
||||
self._snap_point = point
|
||||
if self._draw_handler is None and point is not None:
|
||||
self._draw_handler = bpy.types.SpaceView3D.draw_handler_add(self._draw, (), "WINDOW", "POST_VIEW")
|
||||
self._redraw_viewport()
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear snap point and unregister handler."""
|
||||
self._snap_point = None
|
||||
if self._draw_handler is not None:
|
||||
bpy.types.SpaceView3D.draw_handler_remove(self._draw_handler, "WINDOW")
|
||||
self._draw_handler = None
|
||||
self._redraw_viewport()
|
||||
|
||||
def _draw(self) -> None:
|
||||
"""Draw snap point circles."""
|
||||
if self._snap_point is None:
|
||||
return
|
||||
|
||||
if self._shader is None:
|
||||
self._shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
|
||||
self._shader.bind()
|
||||
self._shader.uniform_float("color", SNAP_CIRCLE_COLOR)
|
||||
gpu.state.line_width_set(SNAP_LINE_WIDTH)
|
||||
|
||||
for plane in ("XY", "XZ", "YZ"):
|
||||
vertices = generate_circle_vertices(self._snap_point, SNAP_CIRCLE_RADIUS, SNAP_CIRCLE_SEGMENTS, plane)
|
||||
batch = batch_for_shader(self._shader, "LINE_STRIP", {"pos": vertices})
|
||||
batch.draw(self._shader)
|
||||
|
||||
gpu.state.line_width_set(1.0)
|
||||
|
||||
@staticmethod
|
||||
def _redraw_viewport() -> None:
|
||||
"""Force 3D viewport redraw."""
|
||||
for area in bpy.context.screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
area.tag_redraw()
|
||||
|
||||
@staticmethod
|
||||
def snap_to_mesh(
|
||||
location: Vector, context: bpy.types.Context, axis_vector: Vector, active_obj: bpy.types.Object
|
||||
) -> Vector:
|
||||
"""Snap a location to the nearest mesh element if snapping is enabled.
|
||||
|
||||
Args:
|
||||
location: The world space location to snap
|
||||
context: The Blender context
|
||||
axis_vector: The axis direction of the gizmo
|
||||
active_obj: The active object to exclude from snapping
|
||||
|
||||
Returns:
|
||||
The snapped location or original location if snapping is disabled
|
||||
"""
|
||||
tool_settings = context.scene.tool_settings
|
||||
|
||||
if not tool_settings.use_snap:
|
||||
return location
|
||||
|
||||
snap_elements = tool_settings.snap_elements_base
|
||||
|
||||
mesh_objects = [
|
||||
obj for obj in context.visible_objects if obj.type == "MESH" and obj != active_obj and obj.visible_get()
|
||||
]
|
||||
|
||||
if not mesh_objects:
|
||||
return location
|
||||
|
||||
nearby_objects = []
|
||||
for obj in mesh_objects:
|
||||
bbox_corners = [obj.matrix_world @ Vector(corner) for corner in obj.bound_box]
|
||||
|
||||
for corner in bbox_corners:
|
||||
if (corner - location).length <= SNAP_MAX_RADIUS:
|
||||
nearby_objects.append(obj)
|
||||
break
|
||||
|
||||
if not nearby_objects:
|
||||
return location
|
||||
|
||||
closest_point = None
|
||||
closest_distance = float("inf")
|
||||
|
||||
for obj in nearby_objects:
|
||||
if not obj.data.vertices:
|
||||
continue
|
||||
|
||||
depsgraph = context.evaluated_depsgraph_get()
|
||||
obj_eval = obj.evaluated_get(depsgraph)
|
||||
mesh = obj_eval.to_mesh()
|
||||
|
||||
if not mesh.vertices:
|
||||
obj_eval.to_mesh_clear()
|
||||
continue
|
||||
|
||||
world_vertices = [obj.matrix_world @ v.co for v in mesh.vertices]
|
||||
|
||||
if "VERTEX" in snap_elements:
|
||||
for v_co in world_vertices:
|
||||
dist = (v_co - location).length
|
||||
if dist < closest_distance:
|
||||
closest_distance = dist
|
||||
closest_point = v_co
|
||||
|
||||
if "EDGE" in snap_elements:
|
||||
for edge in mesh.edges:
|
||||
v1 = world_vertices[edge.vertices[0]]
|
||||
v2 = world_vertices[edge.vertices[1]]
|
||||
|
||||
edge_vec = v2 - v1
|
||||
edge_len_sq = edge_vec.length_squared
|
||||
|
||||
if edge_len_sq > 0:
|
||||
t = max(0, min(1, (location - v1).dot(edge_vec) / edge_len_sq))
|
||||
closest_on_edge = v1 + t * edge_vec
|
||||
dist = (closest_on_edge - location).length
|
||||
|
||||
if dist < closest_distance:
|
||||
closest_distance = dist
|
||||
closest_point = closest_on_edge
|
||||
|
||||
if "FACE" in snap_elements:
|
||||
bvh = BVHTree.FromPolygons(world_vertices, [p.vertices for p in mesh.polygons])
|
||||
|
||||
nearest_loc, normal, index, dist = bvh.find_nearest(location)
|
||||
|
||||
if nearest_loc and dist < closest_distance:
|
||||
closest_distance = dist
|
||||
closest_point = nearest_loc
|
||||
|
||||
obj_eval.to_mesh_clear()
|
||||
|
||||
if closest_point and closest_distance < SNAP_MAX_RADIUS:
|
||||
return closest_point
|
||||
|
||||
return location
|
||||
|
||||
|
||||
_snap_manager = SnapManager()
|
||||
|
||||
|
||||
def set_snap_point(point: Optional[tuple[float, float, float]]) -> None:
|
||||
_snap_manager.set_snap_point(point)
|
||||
|
||||
|
||||
def clear_snap_point() -> None:
|
||||
_snap_manager.clear()
|
||||
|
||||
|
||||
def snap_to_mesh(
|
||||
location: Vector, context: bpy.types.Context, axis_vector: Vector, active_obj: bpy.types.Object
|
||||
) -> Vector:
|
||||
return _snap_manager.snap_to_mesh(location, context, axis_vector, active_obj)
|
||||
|
||||
|
||||
def generate_circle_vertices(
|
||||
center: tuple[float, float, float], radius: float, segments: int, plane: str = "XY"
|
||||
) -> list[tuple[float, float, float]]:
|
||||
"""Generate circle vertices in specified plane.
|
||||
|
||||
Args:
|
||||
center: (x, y, z) tuple for circle center
|
||||
radius: Circle radius
|
||||
segments: Number of segments
|
||||
plane: 'XY', 'XZ', or 'YZ'
|
||||
|
||||
Returns:
|
||||
List of (x, y, z) vertices
|
||||
"""
|
||||
vertices = []
|
||||
for i in range(segments + 1):
|
||||
angle = (2 * math.pi * i) / segments
|
||||
cos_a = radius * math.cos(angle)
|
||||
sin_a = radius * math.sin(angle)
|
||||
|
||||
if plane == "XY":
|
||||
vertices.append((center[0] + cos_a, center[1] + sin_a, center[2]))
|
||||
elif plane == "XZ":
|
||||
vertices.append((center[0] + cos_a, center[1], center[2] + sin_a))
|
||||
else:
|
||||
vertices.append((center[0], center[1] + cos_a, center[2] + sin_a))
|
||||
|
||||
return vertices
|
||||
|
||||
|
||||
def create_quarter_circle_arc(
|
||||
radius: float = 1.0, segments: int = ARC_SEGMENTS, direction: str = "LEFT", line_width: float = ARC_LINE_WIDTH
|
||||
) -> tuple[tuple[float, float, float], ...]:
|
||||
"""Create a quarter circle arc with cross-section thickness for visibility from all angles.
|
||||
|
||||
Args:
|
||||
radius: Radius of the arc
|
||||
segments: Number of segments for smoothness
|
||||
direction: 'LEFT' for counterclockwise (0 to 90°), 'RIGHT' for clockwise (0 to -90°)
|
||||
line_width: Width of the arc line in both perpendicular directions
|
||||
|
||||
Returns:
|
||||
Tuple of arc triangles for drawing geometry visible from all angles
|
||||
"""
|
||||
half_width = line_width / 2
|
||||
|
||||
arc_points = []
|
||||
if direction == "LEFT":
|
||||
for i in range(segments + 1):
|
||||
angle = (math.pi / 2) * (i / segments)
|
||||
x = radius * math.cos(angle)
|
||||
y = radius * math.sin(angle)
|
||||
arc_points.append((x, y))
|
||||
else:
|
||||
for i in range(segments + 1):
|
||||
angle = (math.pi / 2) * (i / segments)
|
||||
x = -radius * math.cos(angle)
|
||||
y = radius * math.sin(angle)
|
||||
arc_points.append((x, y))
|
||||
|
||||
arc_triangles = []
|
||||
for i in range(len(arc_points) - 1):
|
||||
x1, y1 = arc_points[i]
|
||||
x2, y2 = arc_points[i + 1]
|
||||
|
||||
dx, dy = x2 - x1, y2 - y1
|
||||
length = (dx**2 + dy**2) ** 0.5
|
||||
if length > 0:
|
||||
px, py = -dy / length * half_width, dx / length * half_width
|
||||
|
||||
arc_triangles.extend(
|
||||
[
|
||||
(x1 + px, y1 + py, 0.0),
|
||||
(x1 - px, y1 - py, 0.0),
|
||||
(x2 + px, y2 + py, 0.0),
|
||||
]
|
||||
)
|
||||
arc_triangles.extend(
|
||||
[
|
||||
(x2 + px, y2 + py, 0.0),
|
||||
(x1 - px, y1 - py, 0.0),
|
||||
(x2 - px, y2 - py, 0.0),
|
||||
]
|
||||
)
|
||||
|
||||
arc_triangles.extend(
|
||||
[
|
||||
(x1, y1, -half_width),
|
||||
(x2, y2, -half_width),
|
||||
(x1, y1, +half_width),
|
||||
]
|
||||
)
|
||||
arc_triangles.extend(
|
||||
[
|
||||
(x1, y1, +half_width),
|
||||
(x2, y2, -half_width),
|
||||
(x2, y2, +half_width),
|
||||
]
|
||||
)
|
||||
|
||||
return tuple(arc_triangles)
|
||||
|
||||
|
||||
class GizmoMovable(bpy.types.Gizmo):
|
||||
"""Base class for movable gizmos with snapping and precision controls.
|
||||
|
||||
This class provides common functionality for gizmos that can be dragged
|
||||
to modify values, including:
|
||||
- Snap to mesh support (toggle with Ctrl)
|
||||
- Precision mode (hold Shift for 10x slower movement)
|
||||
- Header text with value and modifier hints
|
||||
- Consistent invoke/exit/modal behavior
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"custom_shape",
|
||||
"init_value",
|
||||
"move_get_cb",
|
||||
"move_set_cb",
|
||||
"axis",
|
||||
"local_axis",
|
||||
"start_location",
|
||||
"active_obj",
|
||||
"initial_snap_state",
|
||||
)
|
||||
|
||||
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set:
|
||||
self.init_value = self.move_get_cb() if self.move_get_cb else 0.0
|
||||
self.start_location = self.matrix_basis.translation.copy()
|
||||
self.active_obj = context.active_object
|
||||
self.initial_snap_state = context.scene.tool_settings.use_snap
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def exit(self, context: bpy.types.Context, cancel: bool) -> None:
|
||||
context.area.header_text_set(None)
|
||||
if cancel and self.move_set_cb:
|
||||
self.move_set_cb(self.init_value)
|
||||
if hasattr(self, "initial_snap_state"):
|
||||
context.scene.tool_settings.use_snap = self.initial_snap_state
|
||||
clear_snap_point()
|
||||
|
||||
def get_axis_direction(self) -> Vector:
|
||||
return self.axis
|
||||
|
||||
def modal(self, context: bpy.types.Context, event: bpy.types.Event, tweak) -> set:
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
tool_settings = context.scene.tool_settings
|
||||
|
||||
tool_settings.use_snap = not self.initial_snap_state if event.ctrl else self.initial_snap_state
|
||||
|
||||
current_coord = (event.mouse_region_x, event.mouse_region_y)
|
||||
view_origin = region_2d_to_origin_3d(region, rv3d, current_coord)
|
||||
view_direction = region_2d_to_vector_3d(region, rv3d, current_coord)
|
||||
|
||||
axis_direction = self.get_axis_direction()
|
||||
|
||||
result = intersect_line_line(
|
||||
view_origin,
|
||||
view_origin + view_direction * 1000,
|
||||
self.start_location,
|
||||
self.start_location + axis_direction * 1000,
|
||||
)
|
||||
current_3d = result[1] if result else self.start_location
|
||||
|
||||
delta = (current_3d - self.start_location).dot(axis_direction)
|
||||
|
||||
if tool_settings.use_snap and self.active_obj:
|
||||
snapped_pos = snap_to_mesh(current_3d, context, axis_direction, self.active_obj)
|
||||
if snapped_pos != current_3d:
|
||||
delta = (snapped_pos - self.start_location).dot(axis_direction)
|
||||
set_snap_point(snapped_pos)
|
||||
else:
|
||||
clear_snap_point()
|
||||
else:
|
||||
clear_snap_point()
|
||||
|
||||
if event.shift:
|
||||
delta *= PRECISION_MODE_MULTIPLIER
|
||||
|
||||
if self.move_set_cb:
|
||||
self.move_set_cb(self.init_value + delta)
|
||||
|
||||
self._update_header(context, self.init_value + delta, tool_settings.use_snap, event.shift)
|
||||
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def _update_header(self, context: bpy.types.Context, value: float, is_snapping: bool, is_precision: bool) -> None:
|
||||
header_text = f"Value: {value:.3f}m"
|
||||
hints = []
|
||||
if is_snapping:
|
||||
hints.append("Snapping: ON")
|
||||
if is_precision:
|
||||
hints.append("Precision Mode (0.1x)")
|
||||
hints.extend(["Ctrl: Toggle Snap", "Shift: Precision"])
|
||||
|
||||
if hints:
|
||||
header_text += " | " + " | ".join(hints)
|
||||
context.area.header_text_set(header_text)
|
||||
|
||||
|
||||
class GizmoLock(bpy.types.Gizmo):
|
||||
"""Reusable lock icon gizmo that switches between closed and open states."""
|
||||
|
||||
bl_idname = "VIEW3D_GT_lock"
|
||||
|
||||
__slots__ = (
|
||||
"custom_shape_closed",
|
||||
"custom_shape_open",
|
||||
"prop_path",
|
||||
)
|
||||
|
||||
tris_closed = (
|
||||
(0.16803650558, 0.18791499734, 0.0),
|
||||
(-0.07805634290, 0.18791499734, 0.0),
|
||||
(0.16803650558, 0.44701099396, 0.0),
|
||||
(0.16803650558, 0.44701099396, 0.0),
|
||||
(-0.07805634290, 0.18791499734, 0.0),
|
||||
(-0.07805634290, 0.44701099396, 0.0),
|
||||
(0.20165449381, 0.13603900373, 0.0),
|
||||
(-0.11167433113, 0.13603900373, 0.0),
|
||||
(0.20165449381, -0.44701099396, 0.0),
|
||||
(0.20165449381, -0.44701099396, 0.0),
|
||||
(-0.11167433113, 0.13603900373, 0.0),
|
||||
(-0.11167433113, -0.44701099396, 0.0),
|
||||
(-0.07805634290, 0.18791499734, 0.0),
|
||||
(-0.39353451133, 0.18791499734, 0.0),
|
||||
(-0.07805634290, 0.44701099396, 0.0),
|
||||
(-0.07805634290, 0.44701099396, 0.0),
|
||||
(-0.39353451133, 0.18791499734, 0.0),
|
||||
(-0.39353451133, 0.30746498704, 0.0),
|
||||
(-0.44701099396, 0.18791499734, 0.0),
|
||||
(-0.44701099396, -0.04477182776, 0.0),
|
||||
(-0.39353451133, 0.18791499734, 0.0),
|
||||
(-0.39353451133, 0.18791499734, 0.0),
|
||||
(-0.44701099396, -0.04477182776, 0.0),
|
||||
(-0.39353451133, -0.04477182776, 0.0),
|
||||
)
|
||||
|
||||
tris_open = (
|
||||
(0.16803650558, 0.18791499734, 0.0),
|
||||
(-0.07805634290, 0.18791499734, 0.0),
|
||||
(0.16803650558, 0.44701099396, 0.0),
|
||||
(0.16803650558, 0.44701099396, 0.0),
|
||||
(-0.07805634290, 0.18791499734, 0.0),
|
||||
(-0.07805634290, 0.44701099396, 0.0),
|
||||
(0.20165449381, 0.13603900373, 0.0),
|
||||
(-0.11167433113, 0.13603900373, 0.0),
|
||||
(0.20165449381, -0.44701099396, 0.0),
|
||||
(0.20165449381, -0.44701099396, 0.0),
|
||||
(-0.11167433113, 0.13603900373, 0.0),
|
||||
(-0.11167433113, -0.44701099396, 0.0),
|
||||
(0.16803650558, 0.44701099396, 0.0),
|
||||
(-0.07805634290, 0.44701099396, 0.0),
|
||||
(0.16803650558, 0.70610702038, 0.0),
|
||||
(0.16803650558, 0.70610702038, 0.0),
|
||||
(-0.07805634290, 0.44701099396, 0.0),
|
||||
(-0.07805634290, 0.58656096458, 0.0),
|
||||
(-0.11167433113, 0.44701099396, 0.0),
|
||||
(-0.11167433113, 0.73432201147, 0.0),
|
||||
(-0.07805634290, 0.44701099396, 0.0),
|
||||
(-0.07805634290, 0.44701099396, 0.0),
|
||||
(-0.11167433113, 0.73432201147, 0.0),
|
||||
(-0.07805634290, 0.73432201147, 0.0),
|
||||
)
|
||||
|
||||
def get_custom_shape(self, context: bpy.types.Context):
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return self.custom_shape_closed
|
||||
|
||||
try:
|
||||
is_open = obj.path_resolve(self.prop_path)
|
||||
return self.custom_shape_open if is_open else self.custom_shape_closed
|
||||
except:
|
||||
return self.custom_shape_closed
|
||||
|
||||
def setup(self) -> None:
|
||||
self.custom_shape_closed = self.new_custom_shape("TRIS", self.tris_closed)
|
||||
self.custom_shape_open = self.new_custom_shape("TRIS", self.tris_open)
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
self.draw_custom_shape(self.get_custom_shape(context))
|
||||
|
||||
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
|
||||
self.draw_custom_shape(self.get_custom_shape(context), select_id=select_id)
|
||||
|
||||
|
||||
class GizmoArc(bpy.types.Gizmo):
|
||||
"""Reusable quarter circle arc gizmo."""
|
||||
|
||||
bl_idname = "VIEW3D_GT_arc"
|
||||
|
||||
__slots__ = (
|
||||
"custom_shape_left",
|
||||
"custom_shape_right",
|
||||
"prop_path",
|
||||
)
|
||||
|
||||
def setup(self) -> None:
|
||||
"""Create quarter circle shapes for both LEFT and RIGHT directions."""
|
||||
arc_left = create_quarter_circle_arc(radius=1.0, direction="LEFT")
|
||||
arc_right = create_quarter_circle_arc(radius=1.0, direction="RIGHT")
|
||||
|
||||
self.custom_shape_left = self.new_custom_shape(type="TRIS", verts=arc_left)
|
||||
self.custom_shape_right = self.new_custom_shape(type="TRIS", verts=arc_right)
|
||||
|
||||
def _get_shape_for_direction(self, context: bpy.types.Context):
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return self.custom_shape_left
|
||||
|
||||
try:
|
||||
direction_value = obj.path_resolve(self.prop_path)
|
||||
if "RIGHT" in str(direction_value):
|
||||
return self.custom_shape_right
|
||||
except:
|
||||
pass
|
||||
|
||||
return self.custom_shape_left
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
self.draw_custom_shape(self._get_shape_for_direction(context))
|
||||
|
||||
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
|
||||
self.draw_custom_shape(self._get_shape_for_direction(context), select_id=select_id)
|
||||
|
||||
|
||||
class GizmoPen(bpy.types.Gizmo):
|
||||
"""Reusable pen/edit icon gizmo for entering edit mode."""
|
||||
|
||||
bl_idname = "VIEW3D_GT_pen"
|
||||
|
||||
__slots__ = ("custom_shape",)
|
||||
|
||||
tris = (
|
||||
(-0.07595771551132202, -0.2948460578918457, 0.0),
|
||||
(0.16886109113693237, 0.23203276097774506, 0.0),
|
||||
(0.062240585684776306, 0.28157487511634827, 0.0),
|
||||
(0.07201281189918518, 0.30260589718818665, 0.0),
|
||||
(0.21042980253696442, 0.321493536233902, 0.0),
|
||||
(0.17863331735134125, 0.25306373834609985, 0.0),
|
||||
(0.062240585684776306, 0.28157487511634827, 0.0),
|
||||
(-0.1825782209634781, -0.2453039139509201, 0.0),
|
||||
(-0.07595771551132202, -0.2948460578918457, 0.0),
|
||||
(-0.1825782209634781, -0.2453039139509201, 0.0),
|
||||
(-0.19114767014980316, -0.4032624065876007, 0.0),
|
||||
(-0.07595771551132202, -0.2948460578918457, 0.0),
|
||||
(0.07201281189918518, 0.30260589718818665, 0.0),
|
||||
(0.10380929708480835, 0.371035635471344, 0.0),
|
||||
(0.21042980253696442, 0.321493536233902, 0.0),
|
||||
)
|
||||
|
||||
def setup(self) -> None:
|
||||
self.custom_shape = self.new_custom_shape("TRIS", self.tris)
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
self.draw_custom_shape(self.custom_shape)
|
||||
|
||||
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
|
||||
self.draw_custom_shape(self.custom_shape, select_id=select_id)
|
||||
|
||||
|
||||
class GizmoValidate(bpy.types.Gizmo):
|
||||
"""Reusable validate/checkmark icon gizmo for confirming edits."""
|
||||
|
||||
bl_idname = "VIEW3D_GT_validate"
|
||||
|
||||
__slots__ = ("custom_shape",)
|
||||
|
||||
tris = (
|
||||
(0.36775994300842285, 0.205583393573761, 0.0),
|
||||
(0.030080009251832962, -0.1881658434867859, 0.0),
|
||||
(0.030080009251832962, -0.3380376696586609, 0.0),
|
||||
(-0.22017201781272888, -0.16090886294841766, 0.0),
|
||||
(0.030080009251832962, -0.1881658434867859, 0.0),
|
||||
(-0.22017201781272888, -0.011037036776542664, 0.0),
|
||||
(0.36775994300842285, 0.205583393573761, 0.0),
|
||||
(0.36775994300842285, 0.355455219745636, 0.0),
|
||||
(0.030080009251832962, -0.1881658434867859, 0.0),
|
||||
(-0.22017201781272888, -0.16090886294841766, 0.0),
|
||||
(0.030080009251832962, -0.3380376696586609, 0.0),
|
||||
(0.030080009251832962, -0.1881658434867859, 0.0),
|
||||
)
|
||||
|
||||
def setup(self) -> None:
|
||||
self.custom_shape = self.new_custom_shape("TRIS", self.tris)
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
self.draw_custom_shape(self.custom_shape)
|
||||
|
||||
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
|
||||
self.draw_custom_shape(self.custom_shape, select_id=select_id)
|
||||
|
||||
|
||||
class GizmoCancel(bpy.types.Gizmo):
|
||||
"""Reusable cancel/X icon gizmo for canceling edits."""
|
||||
|
||||
bl_idname = "VIEW3D_GT_cancel"
|
||||
|
||||
__slots__ = ("custom_shape",)
|
||||
|
||||
tris = (
|
||||
(0.0, 0.048707593232393265, 0.0),
|
||||
(0.0, -0.048707593232393265, 0.0),
|
||||
(0.048707593232393265, 0.0, 0.0),
|
||||
(0.0, 0.048707593232393265, 0.0),
|
||||
(-0.21918421983718872, 0.2678918242454529, 0.0),
|
||||
(-0.048707593232393265, 0.0, 0.0),
|
||||
(-0.21918421983718872, 0.2678918242454529, 0.0),
|
||||
(-0.2678918242454529, 0.21918421983718872, 0.0),
|
||||
(-0.048707593232393265, 0.0, 0.0),
|
||||
(-0.048707593232393265, 0.0, 0.0),
|
||||
(-0.2678918242454529, -0.21918421983718872, 0.0),
|
||||
(-0.21918421983718872, -0.2678918242454529, 0.0),
|
||||
(0.2678918242454529, 0.21918421983718872, 0.0),
|
||||
(0.21918421983718872, 0.2678918242454529, 0.0),
|
||||
(0.0, 0.048707593232393265, 0.0),
|
||||
(0.21918421983718872, -0.2678918242454529, 0.0),
|
||||
(0.2678918242454529, -0.21918421983718872, 0.0),
|
||||
(0.048707593232393265, 0.0, 0.0),
|
||||
(0.048707593232393265, 0.0, 0.0),
|
||||
(0.2678918242454529, 0.21918421983718872, 0.0),
|
||||
(0.0, 0.048707593232393265, 0.0),
|
||||
(0.0, 0.048707593232393265, 0.0),
|
||||
(-0.048707593232393265, 0.0, 0.0),
|
||||
(0.0, -0.048707593232393265, 0.0),
|
||||
(-0.048707593232393265, 0.0, 0.0),
|
||||
(-0.21918421983718872, -0.2678918242454529, 0.0),
|
||||
(0.0, -0.048707593232393265, 0.0),
|
||||
(0.0, -0.048707593232393265, 0.0),
|
||||
(0.21918421983718872, -0.2678918242454529, 0.0),
|
||||
(0.048707593232393265, 0.0, 0.0),
|
||||
)
|
||||
|
||||
def setup(self) -> None:
|
||||
self.custom_shape = self.new_custom_shape("TRIS", self.tris)
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
self.draw_custom_shape(self.custom_shape)
|
||||
|
||||
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
|
||||
self.draw_custom_shape(self.custom_shape, select_id=select_id)
|
||||
|
||||
|
||||
class GizmoArrow(GizmoMovable):
|
||||
"""Arrow gizmo for directional value editing."""
|
||||
|
||||
bl_idname = "BIM_GT_gizmo_arrow"
|
||||
bl_target_properties = ({"id": "offset", "type": "FLOAT", "array_length": 1},)
|
||||
|
||||
def _get_arrow_triangles(self) -> tuple[tuple[float, float, float], ...]:
|
||||
"""Generate arrow geometry along +X axis."""
|
||||
triangles = []
|
||||
|
||||
triangles.extend(
|
||||
[
|
||||
(0, -ARROW_WIDTH, 0),
|
||||
(ARROW_SHAFT_LENGTH, -ARROW_WIDTH, 0),
|
||||
(0, +ARROW_WIDTH, 0),
|
||||
]
|
||||
)
|
||||
triangles.extend(
|
||||
[
|
||||
(0, +ARROW_WIDTH, 0),
|
||||
(ARROW_SHAFT_LENGTH, -ARROW_WIDTH, 0),
|
||||
(ARROW_SHAFT_LENGTH, +ARROW_WIDTH, 0),
|
||||
]
|
||||
)
|
||||
|
||||
triangles.extend(
|
||||
[
|
||||
(0, 0, -ARROW_WIDTH),
|
||||
(ARROW_SHAFT_LENGTH, 0, -ARROW_WIDTH),
|
||||
(0, 0, +ARROW_WIDTH),
|
||||
]
|
||||
)
|
||||
triangles.extend(
|
||||
[
|
||||
(0, 0, +ARROW_WIDTH),
|
||||
(ARROW_SHAFT_LENGTH, 0, -ARROW_WIDTH),
|
||||
(ARROW_SHAFT_LENGTH, 0, +ARROW_WIDTH),
|
||||
]
|
||||
)
|
||||
|
||||
head_width = ARROW_WIDTH * ARROW_HEAD_WIDTH_MULTIPLIER
|
||||
triangles.extend(
|
||||
[
|
||||
(ARROW_SHAFT_LENGTH, -head_width, 0),
|
||||
(ARROW_SHAFT_LENGTH + ARROW_HEAD_LENGTH, 0, 0),
|
||||
(ARROW_SHAFT_LENGTH, +head_width, 0),
|
||||
]
|
||||
)
|
||||
|
||||
triangles.extend(
|
||||
[
|
||||
(ARROW_SHAFT_LENGTH, 0, -head_width),
|
||||
(ARROW_SHAFT_LENGTH + ARROW_HEAD_LENGTH, 0, 0),
|
||||
(ARROW_SHAFT_LENGTH, 0, +head_width),
|
||||
]
|
||||
)
|
||||
|
||||
for i in range(ARROW_CIRCLE_SEGMENTS):
|
||||
angle1 = (2 * math.pi * i) / ARROW_CIRCLE_SEGMENTS
|
||||
angle2 = (2 * math.pi * (i + 1)) / ARROW_CIRCLE_SEGMENTS
|
||||
|
||||
y1 = head_width * math.cos(angle1)
|
||||
z1 = head_width * math.sin(angle1)
|
||||
y2 = head_width * math.cos(angle2)
|
||||
z2 = head_width * math.sin(angle2)
|
||||
|
||||
triangles.extend(
|
||||
[
|
||||
(ARROW_SHAFT_LENGTH, 0, 0),
|
||||
(ARROW_SHAFT_LENGTH, y1, z1),
|
||||
(ARROW_SHAFT_LENGTH, y2, z2),
|
||||
]
|
||||
)
|
||||
|
||||
return tuple(triangles)
|
||||
|
||||
def setup(self) -> None:
|
||||
self.custom_shape = self.new_custom_shape("TRIS", self._get_arrow_triangles())
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
self.draw_custom_shape(self.custom_shape)
|
||||
|
||||
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
|
||||
self.draw_custom_shape(self.custom_shape, select_id=select_id)
|
||||
|
||||
|
||||
class GizmoCone(GizmoMovable):
|
||||
"""Cone gizmo for directional value editing with local axis support."""
|
||||
|
||||
bl_idname = "BIM_GT_gizmo_cone"
|
||||
bl_target_properties = ({"id": "offset", "type": "FLOAT", "array_length": 1},)
|
||||
|
||||
def get_axis_direction(self) -> Vector:
|
||||
if hasattr(self, "local_axis") and self.active_obj:
|
||||
obj_rotation = self.active_obj.matrix_world.to_3x3()
|
||||
axis_direction = obj_rotation @ self.local_axis
|
||||
axis_direction.normalize()
|
||||
return axis_direction
|
||||
return self.axis
|
||||
|
||||
def _get_cone_triangles(self) -> tuple[tuple[float, float, float], ...]:
|
||||
"""Generate cone geometry along +X axis."""
|
||||
triangles = []
|
||||
cone_tip_x = CONE_LENGTH
|
||||
|
||||
for i in range(CONE_SEGMENTS):
|
||||
angle1 = (2 * math.pi * i) / CONE_SEGMENTS
|
||||
angle2 = (2 * math.pi * (i + 1)) / CONE_SEGMENTS
|
||||
|
||||
y1 = CONE_RADIUS * math.cos(angle1)
|
||||
z1 = CONE_RADIUS * math.sin(angle1)
|
||||
y2 = CONE_RADIUS * math.cos(angle2)
|
||||
z2 = CONE_RADIUS * math.sin(angle2)
|
||||
|
||||
triangles.extend(
|
||||
[
|
||||
(cone_tip_x, 0, 0),
|
||||
(0, y1, z1),
|
||||
(0, y2, z2),
|
||||
]
|
||||
)
|
||||
|
||||
triangles.extend(
|
||||
[
|
||||
(0, 0, 0),
|
||||
(0, y2, z2),
|
||||
(0, y1, z1),
|
||||
]
|
||||
)
|
||||
|
||||
return tuple(triangles)
|
||||
|
||||
def setup(self) -> None:
|
||||
self.custom_shape = self.new_custom_shape("TRIS", self._get_cone_triangles())
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
self.draw_custom_shape(self.custom_shape)
|
||||
|
||||
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
|
||||
self.draw_custom_shape(self.custom_shape, select_id=select_id)
|
||||
@@ -222,8 +222,6 @@ def refresh_ui_data():
|
||||
if isinstance(ifc_file := tool.Ifc.get(), ifcopenshell.sqlite):
|
||||
ifc_file.clear_cache()
|
||||
|
||||
props = tool.Drawing.get_document_props()
|
||||
props.should_draw_decorations = props.should_draw_decorations
|
||||
if tool.Web.get_web_props().is_connected:
|
||||
tool.Web.send_webui_data()
|
||||
|
||||
|
||||
@@ -556,6 +556,7 @@ def draw_filter(
|
||||
elif ifc_filter.type == "query":
|
||||
row = box.row(align=True)
|
||||
row.prop(ifc_filter, "name", text="", icon="POINTCLOUD_DATA")
|
||||
row.prop(ifc_filter, "comparison", text="")
|
||||
row.prop(ifc_filter, "value", text="")
|
||||
elif ifc_filter.type == "instance":
|
||||
row = box.row(align=True)
|
||||
|
||||
@@ -28,6 +28,7 @@ import numpy as np
|
||||
import numpy.typing as npt
|
||||
import multiprocessing
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.ifcopenshell_wrapper as W
|
||||
import ifcopenshell.util.unit
|
||||
@@ -283,6 +284,7 @@ class IfcImporter:
|
||||
self.place_objects_in_collections()
|
||||
self.profile_code("Place objects in collections")
|
||||
self.setup_arrays()
|
||||
self.update_linked_aggregates()
|
||||
self.profile_code("Setup arrays")
|
||||
tool.Project.load_linked_models_from_ifc()
|
||||
self.profile_code("Load linked models")
|
||||
@@ -624,18 +626,13 @@ class IfcImporter:
|
||||
print("Done creating geometry")
|
||||
|
||||
def create_spatial_elements(self) -> None:
|
||||
if tool.Blender.get_addon_preferences().spatial_elements_unselectable:
|
||||
self.create_generic_elements(self.spatial_elements, unselectable=True)
|
||||
else:
|
||||
self.create_generic_elements(self.spatial_elements, unselectable=False)
|
||||
self.create_generic_elements(self.spatial_elements)
|
||||
|
||||
def create_elements(self) -> None:
|
||||
self.create_generic_elements(self.elements)
|
||||
self.create_generic_elements(self.gross_elements, is_gross=True)
|
||||
|
||||
def create_generic_elements(
|
||||
self, elements: set[ifcopenshell.entity_instance], unselectable=False, is_gross=False
|
||||
) -> None:
|
||||
def create_generic_elements(self, elements: set[ifcopenshell.entity_instance], is_gross=False) -> None:
|
||||
if isinstance(self.file, ifcopenshell.sqlite):
|
||||
return self.create_generic_sqlite_elements(elements)
|
||||
|
||||
@@ -659,10 +656,6 @@ class IfcImporter:
|
||||
print("{} / {} elements processed ...".format(i, total))
|
||||
objects.add(self.create_product(element))
|
||||
|
||||
if unselectable:
|
||||
for obj in objects:
|
||||
obj.hide_select = True
|
||||
|
||||
def create_generic_sqlite_elements(self, elements: set[ifcopenshell.entity_instance]) -> None:
|
||||
assert isinstance(self.file, ifcopenshell.sql.sqlite)
|
||||
self.geometry_cache = self.file.get_geometry([e.id() for e in elements])
|
||||
@@ -960,6 +953,9 @@ class IfcImporter:
|
||||
if not props.ifc_file:
|
||||
props.ifc_file = self.ifc_import_settings.input_file
|
||||
self.file = tool.Ifc.get()
|
||||
# IFC4 Reference View shall have no booleans https://github.com/BuildingSMART/IFC4-CV/issues/14
|
||||
if self.file.schema == "IFC4" and "ReferenceView" in str(self.file.header.file_description.description):
|
||||
self.ifc_import_settings.void_limit = 0
|
||||
|
||||
def calculate_unit_scale(self):
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
||||
@@ -1017,6 +1013,7 @@ class IfcImporter:
|
||||
)
|
||||
obj = self.create_product(self.project["ifc"])
|
||||
obj.hide_select = True
|
||||
obj.hide_viewport = True
|
||||
self.project["blender"].objects.link(obj)
|
||||
self.project["blender"].BIMCollectionProperties.obj = obj
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
@@ -1218,6 +1215,35 @@ class IfcImporter:
|
||||
tool.Blender.Modifier.Array.set_children_lock_state(element, i, True)
|
||||
tool.Blender.Modifier.Array.constrain_children_to_parent(element)
|
||||
|
||||
def update_linked_aggregates(self):
|
||||
# TODO Remove this after a while. See commit 17d6b8a
|
||||
# https://github.com/IfcOpenShell/IfcOpenShell/commit/17d6b8af61c8dd7cd82f6e72b2fb3831d851a33d#commitcomment-163151609
|
||||
|
||||
groups = self.file.by_type("IfcGroup")
|
||||
target_groups = [group for group in groups if group.Name == "BBIM_Linked_Aggregate"]
|
||||
if not target_groups:
|
||||
return
|
||||
|
||||
for i, group in enumerate(target_groups):
|
||||
name = "Default"
|
||||
elements = ifcopenshell.util.element.get_grouped_by(self.file.by_id(group.id()), is_recursive=True)
|
||||
for j, element in enumerate(elements):
|
||||
split = element.Name.rsplit("_")
|
||||
name = split[0]
|
||||
try:
|
||||
aggregate_index = int(split[1])
|
||||
except:
|
||||
aggregate_index = 0
|
||||
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Linked_Aggregate", should_inherit=False)
|
||||
if not pset:
|
||||
return
|
||||
if "Aggregate_Index" not in pset.keys():
|
||||
ifcopenshell.api.pset.edit_pset(
|
||||
self.file,
|
||||
pset=self.file.by_id(pset["id"]),
|
||||
properties={"Aggregate_Index": aggregate_index, "Name": name},
|
||||
)
|
||||
|
||||
|
||||
class IfcImportSettings:
|
||||
"""
|
||||
|
||||
@@ -21,6 +21,7 @@ import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.group
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.api.root
|
||||
import ifcopenshell.util.element
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.aggregate as core
|
||||
@@ -94,6 +95,9 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
aggregates_to_check = set()
|
||||
|
||||
# First pass: unassign all parts and track their aggregates
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
@@ -101,6 +105,10 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
if not aggregate:
|
||||
continue
|
||||
|
||||
# Track this aggregate for later checking
|
||||
aggregates_to_check.add(aggregate)
|
||||
|
||||
core.unassign_object(
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
@@ -116,6 +124,33 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
|
||||
|
||||
# Second pass: delete aggregates that now have no parts
|
||||
deleted_aggregates = []
|
||||
for aggregate in aggregates_to_check:
|
||||
# Skip spatial elements - they should not be deleted even if empty
|
||||
if aggregate.is_a("IfcSpatialElement"):
|
||||
continue
|
||||
|
||||
related_objects = ifcopenshell.util.element.get_parts(aggregate)
|
||||
if len(related_objects) == 0:
|
||||
aggregate_name = aggregate.Name or f"{aggregate.is_a()} #{aggregate.id()}"
|
||||
deleted_aggregates.append(aggregate_name)
|
||||
|
||||
aggregate_obj = tool.Ifc.get_object(aggregate)
|
||||
if aggregate_obj:
|
||||
ifcopenshell.api.root.remove_product(tool.Ifc.get(), product=aggregate)
|
||||
bpy.data.objects.remove(aggregate_obj, do_unlink=True)
|
||||
|
||||
# Show info message if aggregates were deleted
|
||||
if deleted_aggregates:
|
||||
if len(deleted_aggregates) == 1:
|
||||
self.report(
|
||||
{"INFO"}, f"Aggregate '{deleted_aggregates[0]}' was deleted because it had no remaining parts"
|
||||
)
|
||||
else:
|
||||
aggregate_list = ", ".join(f"'{name}'" for name in deleted_aggregates)
|
||||
self.report({"INFO"}, f"Aggregates {aggregate_list} were deleted because they had no remaining parts")
|
||||
|
||||
|
||||
class BIM_OT_enable_editing_aggregate(bpy.types.Operator):
|
||||
"""Enable editing aggregation relationship"""
|
||||
@@ -291,6 +326,21 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
|
||||
aggregate_obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = aggregate_obj
|
||||
|
||||
# copy selection query to clipboard
|
||||
result = ""
|
||||
for aggregate in all_parts:
|
||||
aggregate_class = aggregate.is_a()
|
||||
aggregate_name = aggregate.Name or ""
|
||||
query = f'parent = "{aggregate_name}"'
|
||||
if not result:
|
||||
result = query
|
||||
else:
|
||||
result += f" + {query}"
|
||||
|
||||
if result:
|
||||
bpy.context.window_manager.clipboard = result
|
||||
self.report({"INFO"}, f"({result}) was copied to the clipboard.")
|
||||
|
||||
self.one_level_deep = False # <-- forcibly reset
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -1215,7 +1215,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
|
||||
assert blender_topic
|
||||
topic = bcfxml.topics[blender_topic.name]
|
||||
if self.viewpoint_guid:
|
||||
viewpoint_guid = self.viewpoint_guid
|
||||
viewpoint_guid = self.viewpoint_guid + ".bcfv"
|
||||
if viewpoint_guid not in topic.viewpoints:
|
||||
self.report({"ERROR"}, f"No such viewpoint in the active topic: '{viewpoint_guid}'.")
|
||||
return {"CANCELLED"}
|
||||
@@ -1313,7 +1313,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
|
||||
x_axis = y_axis.cross(z_axis).normalized()
|
||||
rotation = Matrix((x_axis, y_axis, z_axis))
|
||||
rotation.invert()
|
||||
matrix = np.matrix(
|
||||
matrix = np.array(
|
||||
(
|
||||
[x_axis[0], y_axis[0], z_axis[0], camera.camera_view_point.x],
|
||||
[x_axis[1], y_axis[1], z_axis[1], camera.camera_view_point.y],
|
||||
|
||||
@@ -282,7 +282,11 @@ class BIM_PT_classification_references(Panel, ReferenceUI):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return bool((obj := context.active_object) and tool.Ifc.get_entity(obj))
|
||||
return (
|
||||
(obj := tool.Blender.get_active_object())
|
||||
and (element := tool.Ifc.get_entity(obj))
|
||||
and element.is_a("IfcObjectDefinition")
|
||||
)
|
||||
|
||||
def get_object_name(self, context: bpy.types.Context) -> str:
|
||||
assert (obj := context.active_object)
|
||||
|
||||
@@ -28,6 +28,7 @@ import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.ifcopenshell_wrapper as W
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.unit
|
||||
@@ -253,13 +254,17 @@ class ProfileImportIFC(bpy.types.Operator):
|
||||
class CreateAllShapes(bpy.types.Operator):
|
||||
bl_idname = "bim.create_all_shapes"
|
||||
bl_label = "Test All Shapes"
|
||||
bl_description = "Look for errors in all the shapes contained in the file"
|
||||
bl_description = (
|
||||
"Look for errors in all the shapes contained in the file.\n\nSee system console for the detailed results."
|
||||
)
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
geometry_library: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Geometry Library",
|
||||
description="Geometry library to use for testing shape creation.",
|
||||
items=[(i, i, "") for i in get_args(ifcopenshell.geom.GEOMETRY_LIBRARY)],
|
||||
default="opencascade",
|
||||
# By default use the same library as used for importing ifc project.
|
||||
default="hybrid-cgal-simple-opencascade",
|
||||
)
|
||||
custom_geometry_library: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Custom Geometry Library",
|
||||
@@ -272,19 +277,23 @@ class CreateAllShapes(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Ifc.get()
|
||||
if not tool.Ifc.get():
|
||||
cls.poll_message_set("No IFC file is loaded.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
self.file = tool.Ifc.get()
|
||||
geometry_library = self.custom_geometry_library or self.geometry_library
|
||||
elements = self.file.by_type("IfcElement") + self.file.by_type("IfcSpace")
|
||||
print(f"Testing geometry library '{geometry_library}'.")
|
||||
|
||||
total = len(elements)
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("keep-bounding-boxes", True)
|
||||
settings_2d = ifcopenshell.geom.settings()
|
||||
settings_2d.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
failures = []
|
||||
failures: list[ifcopenshell.entity_instance] = []
|
||||
excludes = () # For the developer to debug with
|
||||
for i, element in enumerate(elements, 1):
|
||||
if element.GlobalId in excludes:
|
||||
@@ -303,12 +312,11 @@ class CreateAllShapes(bpy.types.Operator):
|
||||
failures.append(element)
|
||||
print("***** FAILURE *****")
|
||||
if shape:
|
||||
assert isinstance(shape, W.TriangulationElement)
|
||||
geom = shape.geometry
|
||||
print(
|
||||
"Success",
|
||||
time.time() - start,
|
||||
len(shape.geometry.verts),
|
||||
len(shape.geometry.edges),
|
||||
len(shape.geometry.faces),
|
||||
f"Success {time.time() - start:.3f}s "
|
||||
f"V:{(len(geom.verts)//3)} E:{(len(geom.edges)//2)} F:{(len(geom.faces)//3)}"
|
||||
)
|
||||
self.report({"INFO"}, f"Failed shapes: {len(failures)}, check the system console for details.")
|
||||
for failure in failures:
|
||||
@@ -810,7 +818,12 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.merge_identical_objects"
|
||||
bl_label = "Merge Identical Objects"
|
||||
bl_description = "For materials currently only IfcMaterials are supported"
|
||||
bl_description = (
|
||||
"Merge identical IFC objects (that match all attributes).\n"
|
||||
"\n"
|
||||
"SHIFT + CLICK to merge by name/identification attribute only.\n"
|
||||
"Merges names with number suffix, as well (ex: foo, foo.001, foo.002)\n"
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
@@ -818,18 +831,36 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
|
||||
items=((s, s.capitalize(), "") for s in get_args(tool.Debug.PurgeMergeObjectType)),
|
||||
)
|
||||
|
||||
by_name_or_identification_only: bpy.props.BoolProperty(
|
||||
name="By Name/Identification Only",
|
||||
description="Merge based only on Name or Identification attribute, ignoring other properties",
|
||||
default=False,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
object_type: tool.Debug.PurgeMergeObjectType
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Check if shift key is pressed
|
||||
if event.shift:
|
||||
self.by_name_or_identification_only = True
|
||||
else:
|
||||
self.by_name_or_identification_only = False
|
||||
|
||||
return self.execute(context)
|
||||
|
||||
def _execute(self, context):
|
||||
object_type: str = self.object_type
|
||||
if object_type in ("PROFILE", "TYPE"):
|
||||
self.report({"ERROR"}, f"Unsupported object type {object_type}.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
merged_data = tool.Debug.merge_identical_objects(object_type)
|
||||
merged_data = tool.Debug.merge_identical_objects(
|
||||
object_type, by_name_or_identification_only=self.by_name_or_identification_only
|
||||
)
|
||||
plural_object_type = f"{object_type.lower().replace('_', ' ')}s"
|
||||
if merged_data:
|
||||
merge_mode = " by name/identification" if self.by_name_or_identification_only else ""
|
||||
for element_type, element_names in merged_data.items():
|
||||
print(f"- {element_type}:")
|
||||
for name in element_names:
|
||||
@@ -838,7 +869,8 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
|
||||
merged = sum(len(v) for v in merged_data.values())
|
||||
|
||||
msg = " See system console for details." if merged else ""
|
||||
self.report({"INFO"}, f"{merged} identical {plural_object_type} were merged.{msg}")
|
||||
merge_mode = " (by name/identification)" if self.by_name_or_identification_only else ""
|
||||
self.report({"INFO"}, f"{merged} identical {plural_object_type} were merged{merge_mode}.{msg}")
|
||||
|
||||
if merged == 0:
|
||||
return
|
||||
|
||||
@@ -72,7 +72,10 @@ class BIM_PT_debug(Panel):
|
||||
row = layout.row()
|
||||
row.operator("bim.convert_to_blender")
|
||||
|
||||
row = layout.row()
|
||||
row = layout.row(align=True)
|
||||
op_props = wm.operator_properties_last(bpy.ops.bim.create_all_shapes.idname())
|
||||
row.prop(op_props, "geometry_library", text="")
|
||||
row.prop(op_props, "custom_geometry_library", text="", placeholder="Custom Geometry Library")
|
||||
row.operator("bim.create_all_shapes")
|
||||
|
||||
row = layout.row()
|
||||
|
||||
@@ -61,6 +61,7 @@ classes = (
|
||||
operator.EnableEditingAssignedProduct,
|
||||
operator.EnableEditingElementFilter,
|
||||
operator.EnableEditingText,
|
||||
operator.ExcludeAnnotation,
|
||||
operator.ExpandSheet,
|
||||
operator.LoadDrawings,
|
||||
operator.LoadReferences,
|
||||
|
||||
@@ -235,12 +235,18 @@ class DecoratorData:
|
||||
cut_cache = {}
|
||||
slice_cache = {}
|
||||
fill_cache = {}
|
||||
camera_location_checksum = ""
|
||||
camera_rotation_checksum = ""
|
||||
|
||||
@classmethod
|
||||
def clear_cache(cls):
|
||||
cls.cut_cache = {}
|
||||
cls.layerset_cache = {}
|
||||
cls.fill_cache = {}
|
||||
|
||||
@classmethod
|
||||
def load(cls, handler):
|
||||
cls.is_loaded = True
|
||||
cls.cut_cache = {}
|
||||
cls.layerset_cache = {}
|
||||
|
||||
text = {}
|
||||
dimension = {}
|
||||
@@ -346,12 +352,10 @@ class DecoratorData:
|
||||
(font_size_type for font_size_type in FONT_SIZES if font_size_type in classes_split), "regular"
|
||||
)
|
||||
font_size = FONT_SIZES[font_size_type]
|
||||
|
||||
# get symbol
|
||||
symbol = tool.Drawing.get_annotation_symbol(element)
|
||||
|
||||
# get newline_at
|
||||
newline_at = pset_data.get("Newline_At", 0)
|
||||
reverse_list = pset_data.get("Reverse_List", False)
|
||||
list_separator = pset_data.get("List_Separator") or ", "
|
||||
|
||||
# other attributes
|
||||
literals = tool.Drawing.get_text_literal(obj, return_list=True)
|
||||
@@ -363,11 +367,20 @@ class DecoratorData:
|
||||
literal_data = {
|
||||
"Literal": literal_value,
|
||||
"BoxAlignment": literal.BoxAlignment,
|
||||
"CurrentValue": tool.Drawing.replace_text_literal_variables(literal_value, product),
|
||||
"CurrentValue": tool.Drawing.replace_text_literal_variables(
|
||||
literal_value, product, reverse_list, list_separator
|
||||
),
|
||||
}
|
||||
literals_data.append(literal_data)
|
||||
|
||||
return {"Literals": literals_data, "FontSize": font_size, "Symbol": symbol, "Newline_At": newline_at}
|
||||
return {
|
||||
"Literals": literals_data,
|
||||
"FontSize": font_size,
|
||||
"Symbol": symbol,
|
||||
"Newline_At": newline_at,
|
||||
"Reverse_List": reverse_list,
|
||||
"List_Separator": list_separator,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_dimension_data(cls, obj: bpy.types.Object) -> dict[str, Any]:
|
||||
|
||||
@@ -19,9 +19,11 @@
|
||||
import gpu
|
||||
import bpy
|
||||
import blf
|
||||
import os
|
||||
import math
|
||||
import bmesh
|
||||
import shapely
|
||||
import numpy as np
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.representation
|
||||
@@ -40,7 +42,7 @@ from bonsai.bim.module.drawing.helper import format_distance
|
||||
from timeit import default_timer as timer
|
||||
from functools import cache
|
||||
from typing import Optional, Union
|
||||
from collections.abc import Iterator
|
||||
from collections.abc import Generator, Iterator
|
||||
|
||||
UNSPECIAL_ELEMENT_COLOR = (0.2, 0.2, 0.2, 1) # GREY
|
||||
|
||||
@@ -171,15 +173,11 @@ class BaseDecorator:
|
||||
def get_camera_width_mm(self):
|
||||
# Horrific prototype code to ensure bgl draws at drawing scales
|
||||
# https://blender.stackexchange.com/questions/16493/is-there-a-way-to-fit-the-viewport-to-the-current-field-of-view
|
||||
def is_landscape(render):
|
||||
return render.resolution_x > render.resolution_y
|
||||
|
||||
camera = bpy.context.scene.camera
|
||||
render = bpy.context.scene.render
|
||||
if is_landscape(render):
|
||||
camera_width_model = camera.data.ortho_scale
|
||||
else:
|
||||
camera_width_model = camera.data.ortho_scale / render.resolution_y * render.resolution_x
|
||||
|
||||
# Always use the camera's ortho_scale as the width in model space
|
||||
camera_width_model = camera.data.ortho_scale
|
||||
|
||||
scale = tool.Drawing.get_scale_ratio(tool.Drawing.get_diagram_scale(camera)["Scale"])
|
||||
camera_width_mm = scale * camera_width_model
|
||||
@@ -188,29 +186,26 @@ class BaseDecorator:
|
||||
def camera_zoom_to_factor(self, zoom):
|
||||
return math.pow(((zoom / 50) + math.sqrt(2)) / 2, 2)
|
||||
|
||||
def get_splines(self, obj):
|
||||
def get_splines(self, obj: bpy.types.Object) -> Generator[list[Vector]]:
|
||||
"""Iterates through splines
|
||||
Args:
|
||||
obj: Blender object with Curve data
|
||||
|
||||
Yields:
|
||||
verts: points of each spline, world coords
|
||||
:param obj: Blender object with Curve data
|
||||
:yield: points of each spline, world coords
|
||||
"""
|
||||
assert type(obj.data) is bpy.types.Curve
|
||||
for spline in obj.data.splines:
|
||||
spline_points = spline.bezier_points if spline.bezier_points else spline.points
|
||||
if len(spline_points) < 2:
|
||||
continue
|
||||
yield [obj.matrix_world @ p.co for p in spline_points]
|
||||
|
||||
def get_path_geom(self, obj, topo=True):
|
||||
def get_path_geom(self, obj: bpy.types.Object, topo: bool = True):
|
||||
"""Parses path geometry into line segments
|
||||
|
||||
Args:
|
||||
obj: Blender object with data of type Curve
|
||||
topo: bool; if types of vertices are needed
|
||||
:param obj: Blender object with data of type Curve
|
||||
:param topo: if types of vertices are needed
|
||||
|
||||
Returns:
|
||||
vertices: 3-tuples of coords
|
||||
:return: vertices: 3-tuples of coords
|
||||
indices: 2-tuples of each segment verices' indices
|
||||
topology: types of vertices
|
||||
0: internal
|
||||
@@ -241,11 +236,9 @@ class BaseDecorator:
|
||||
def get_mesh_geom(self, obj, check_mode=True):
|
||||
"""Parses mesh geometry into line segments
|
||||
|
||||
Args:
|
||||
obj: Blender object with data of type Mesh
|
||||
:param obj: Blender object with data of type Mesh
|
||||
|
||||
Returns:
|
||||
vertices: 3-tuples of coords
|
||||
:return: vertices: 3-tuples of coords
|
||||
indices: 2-tuples of each segment verices' indices
|
||||
"""
|
||||
if check_mode and obj.data.is_editmode:
|
||||
@@ -376,11 +369,9 @@ class BaseDecorator:
|
||||
):
|
||||
"""Draw text label
|
||||
|
||||
Args:
|
||||
pos: bottom-center
|
||||
multiline: \n characters will be interpreted as line breaks
|
||||
|
||||
aligned and centered at segment middle
|
||||
:param pos: bottom-center
|
||||
:param multiline: ``\n`` characters will be interpreted as line breaks
|
||||
aligned and centered at segment middle
|
||||
|
||||
NOTE: `blf.draw` seems to ignore the \n character, so we have to split the text ourselves
|
||||
and use the `line_no` argument of `draw_label`
|
||||
@@ -498,7 +489,7 @@ class BaseDecorator:
|
||||
self.draw_label(context, text=text, line_no=line_number_start, multiline=True, **draw_label_kwargs)
|
||||
|
||||
@cache
|
||||
def format_value(self, context, value, custom_unit=None):
|
||||
def format_value(self, context, value, suppress_zero_inches=False, custom_unit=None, in_unit_length=False):
|
||||
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
|
||||
precision = drawing_pset_data.get("MetricPrecision", None)
|
||||
if not precision:
|
||||
@@ -509,8 +500,9 @@ class BaseDecorator:
|
||||
value,
|
||||
precision=precision,
|
||||
decimal_places=decimal_places,
|
||||
suppress_zero_inches=True,
|
||||
suppress_zero_inches=suppress_zero_inches,
|
||||
custom_unit=custom_unit,
|
||||
in_unit_length=in_unit_length,
|
||||
)
|
||||
|
||||
def draw_asterisk(self, context: bpy.types.Context, pos: Vector, rotation: float = 0.0, scale: float = 1.0) -> None:
|
||||
@@ -752,7 +744,12 @@ class DimensionDecorator(BaseDecorator):
|
||||
|
||||
if not show_description_only:
|
||||
length = (v1 - v0).length
|
||||
text = self.format_value(context, length, custom_unit=dimension_data["custom_unit"])
|
||||
text = self.format_value(
|
||||
context,
|
||||
length,
|
||||
suppress_zero_inches=dimension_data["suppress_zero_inches"],
|
||||
custom_unit=dimension_data["custom_unit"],
|
||||
)
|
||||
if isinstance(self, DiameterDecorator):
|
||||
text = "D" + text
|
||||
text = text_prefix + text + text_suffix
|
||||
@@ -1205,19 +1202,13 @@ class PlanLevelDecorator(BaseDecorator):
|
||||
|
||||
def get_text():
|
||||
abs_z = verts[0].z
|
||||
z = abs_z
|
||||
z = helper.get_relative_z(obj, element, abs_z)
|
||||
|
||||
if element:
|
||||
spatial_container = ifcopenshell.util.element.get_container(element)
|
||||
# Z is in meters (Blender world space), convert to project units (feet)
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
z = z / unit_scale # Convert meters to feet
|
||||
|
||||
if spatial_container:
|
||||
container_obj = tool.Ifc.get_object(spatial_container)
|
||||
|
||||
if container_obj:
|
||||
spatial_container_z = container_obj.matrix_world.translation.z
|
||||
z = abs_z - spatial_container_z
|
||||
|
||||
rl = self.format_value(context, z)
|
||||
rl = self.format_value(context, z, in_unit_length=True)
|
||||
text = "{}{}".format("" if z < 0 else "+", rl)
|
||||
return text
|
||||
|
||||
@@ -1290,7 +1281,11 @@ class SectionLevelDecorator(BaseDecorator):
|
||||
|
||||
def get_text():
|
||||
z = verts[0].z
|
||||
rl = self.format_value(context, z)
|
||||
# Z is in meters (Blender world space), convert to project units (feet)
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
z = z / unit_scale # Convert meters to feet
|
||||
|
||||
rl = self.format_value(context, z, in_unit_length=True)
|
||||
text = "RL {}{}".format("" if z < 0 else "+", rl)
|
||||
return text
|
||||
|
||||
@@ -1635,6 +1630,7 @@ class CutDecorator:
|
||||
if cls.installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
handler.cache_camera_matrix()
|
||||
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
|
||||
|
||||
@classmethod
|
||||
@@ -1649,6 +1645,13 @@ class CutDecorator:
|
||||
if not context.scene.camera:
|
||||
return
|
||||
|
||||
# Check if any viewport is in local view - skip decorations if so
|
||||
for area in context.screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
space = area.spaces.active
|
||||
if isinstance(space, bpy.types.SpaceView3D) and space.local_view:
|
||||
return # Don't draw decorations in local view
|
||||
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
selected_elements_color = self.addon_prefs.decorator_color_selected
|
||||
self.fallback_colour = (0.3, 0.3, 0.3, 1)
|
||||
@@ -1737,6 +1740,47 @@ class CutDecorator:
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def cache_camera_matrix(self):
|
||||
obj = bpy.context.scene.camera
|
||||
# Explicit `dtype` for Blender <5.0 compatibility.
|
||||
DecoratorData.camera_location_checksum = repr(
|
||||
np.array(obj.matrix_world.translation, dtype=np.float32).tobytes()
|
||||
)
|
||||
DecoratorData.camera_rotation_checksum = repr(np.array(obj.matrix_world.to_3x3(), dtype=np.float32).tobytes())
|
||||
|
||||
def is_camera_moved(self):
|
||||
if not DecoratorData.camera_location_checksum:
|
||||
self.cache_camera_matrix()
|
||||
return True # Let's be conservative
|
||||
obj = bpy.context.scene.camera
|
||||
|
||||
# Handle both old float64 and new float32 checksums for version compatibility
|
||||
loc_checksum_bytes = eval(DecoratorData.camera_location_checksum)
|
||||
if len(loc_checksum_bytes) == 24: # Old format: 3 * 8 bytes (float64)
|
||||
loc_check = np.frombuffer(loc_checksum_bytes, dtype=np.float64).astype(np.float32)
|
||||
else: # New format: 3 * 4 bytes (float32)
|
||||
loc_check = np.frombuffer(loc_checksum_bytes, dtype=np.float32)
|
||||
|
||||
loc_real = np.array(obj.matrix_world.translation, dtype=np.float32).flatten()
|
||||
if not np.allclose(loc_check, loc_real, atol=1e-4): # 0.1 mm
|
||||
self.cache_camera_matrix()
|
||||
return True
|
||||
|
||||
# Handle both old float64 and new float32 checksums for version compatibility
|
||||
rot_checksum_bytes = eval(DecoratorData.camera_rotation_checksum)
|
||||
if len(rot_checksum_bytes) == 72: # Old format: 9 * 8 bytes (float64)
|
||||
rot_check = np.frombuffer(rot_checksum_bytes, dtype=np.float64).astype(np.float32).reshape(3, 3)
|
||||
else: # New format: 9 * 4 bytes (float32)
|
||||
rot_check = np.frombuffer(rot_checksum_bytes, dtype=np.float32).reshape(3, 3)
|
||||
|
||||
rot_real = np.array(obj.matrix_world.to_3x3(), dtype=np.float32)
|
||||
rot_dot = np.dot(rot_check, rot_real.T)
|
||||
angle_rad = np.arccos(np.clip((np.trace(rot_dot) - 1) / 2, -1, 1))
|
||||
if angle_rad > 0.0017453292519943296: # 0.1 degrees
|
||||
self.cache_camera_matrix()
|
||||
return True
|
||||
return False
|
||||
|
||||
def decorate(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||
has_cut_cache = element.id() in DecoratorData.cut_cache
|
||||
has_fill_cache = element.id() in DecoratorData.fill_cache
|
||||
@@ -1744,9 +1788,9 @@ class CutDecorator:
|
||||
# Currently selected objects must be recalculated as they may be being moved / edited.
|
||||
# If the camera is selected, we also recalculate as the user may be moving the camera.
|
||||
|
||||
if not has_cut_cache or obj.select_get() or context.scene.camera.select_get():
|
||||
if not has_cut_cache or obj.select_get() or self.is_camera_moved():
|
||||
self.recalculate_cut(context, obj, element)
|
||||
if not has_fill_cache or obj.select_get() or context.scene.camera.select_get():
|
||||
if not has_fill_cache or obj.select_get() or self.is_camera_moved():
|
||||
self.recalculate_fill(context, obj, element)
|
||||
|
||||
def recalculate_cut(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||
@@ -1943,8 +1987,7 @@ class DecorationsHandler:
|
||||
# NOTE: we USE POST_PIXEL here so that we can use both POLYLINE_UNIFORM_COLOR
|
||||
# and drawing text in the same handler. BUT this means that we supply coordinates in WINSPACE
|
||||
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_PIXEL")
|
||||
if not DecoratorData.is_loaded:
|
||||
DecoratorData.load(handler)
|
||||
DecoratorData.clear_cache()
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
@@ -1959,17 +2002,56 @@ class DecorationsHandler:
|
||||
for object_type in ("SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"):
|
||||
self.decorators[object_type] = self.decorators["FALL"]
|
||||
self.decorators["MULTI_SYMBOL"] = self.decorators["SYMBOL"]
|
||||
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
|
||||
if drawing_font := prefs.doc.drawing_font:
|
||||
# Try Blender addon font folder
|
||||
drawing_font_path = tool.Blender.get_data_dir_path(Path("fonts") / drawing_font)
|
||||
if drawing_font_path.is_file():
|
||||
font_id = blf.load(drawing_font_path.__str__())
|
||||
for decorator in self.decorators.values():
|
||||
decorator.font_id = font_id
|
||||
|
||||
if not drawing_font_path.is_file():
|
||||
# 2 — Fallback search: Windows font directories
|
||||
# TODO - Linux
|
||||
win_font_dirs = [
|
||||
Path(os.environ.get("WINDIR", "C:\\Windows")) / "Fonts",
|
||||
# Add any custom enterprise paths here if needed
|
||||
]
|
||||
|
||||
found_font = None
|
||||
for font_dir in win_font_dirs:
|
||||
candidate = font_dir / drawing_font
|
||||
if candidate.is_file():
|
||||
found_font = candidate
|
||||
break
|
||||
|
||||
if found_font:
|
||||
drawing_font_path = found_font
|
||||
else:
|
||||
print(f"[BIM] Font '{drawing_font}' not found in addon or Windows fonts.")
|
||||
return # Bail out without assigning fonts
|
||||
|
||||
font_id = blf.load(str(drawing_font_path))
|
||||
|
||||
for decorator in self.decorators.values():
|
||||
decorator.font_id = font_id
|
||||
|
||||
def __call__(self, context):
|
||||
# Check if any viewport is in local view - skip decorations if so
|
||||
for area in context.screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
space = area.spaces.active
|
||||
if isinstance(space, bpy.types.SpaceView3D) and space.local_view:
|
||||
return # Don't draw decorations in local view
|
||||
|
||||
# disable decorations when not in camera view
|
||||
if context.region_data.view_perspective != "CAMERA":
|
||||
return
|
||||
|
||||
if not DrawingsData.is_loaded:
|
||||
DrawingsData.load()
|
||||
|
||||
if not DecoratorData.is_loaded:
|
||||
DecoratorData.load(self)
|
||||
|
||||
for obj, decorator in DecoratorData.data["object_decorators"]:
|
||||
decorator.decorate(context, obj)
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import bpy
|
||||
import math
|
||||
import ifcopenshell.util.element
|
||||
import mathutils.geometry
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.unit
|
||||
@@ -121,6 +122,45 @@ class BoundingBox:
|
||||
return retVal
|
||||
|
||||
|
||||
def find_best_precision(fractional_inches, max_precision=256, base_tolerance=0.0001):
|
||||
"""
|
||||
Find the simplest fraction denominator that accurately represents the value.
|
||||
Uses a sliding tolerance - smaller denominators get more tolerance.
|
||||
|
||||
:param fractional_inches: The fractional part of inches (0.0 to 1.0)
|
||||
:param max_precision: Maximum denominator to try (256 for imperial)
|
||||
:param base_tolerance: Base tolerance in inches (very strict)
|
||||
:return: Best denominator (power of 2: 2, 4, 8, 16, 32, 64, 128, 256)
|
||||
"""
|
||||
if abs(fractional_inches) < 0.00001:
|
||||
return 1 # No fraction needed
|
||||
|
||||
# Try denominators from coarsest to finest: 2, 4, 8, 16, 32, 64, 128, 256
|
||||
for exp in range(1, 9): # 2^1 to 2^8
|
||||
denom = 2**exp
|
||||
if denom > max_precision:
|
||||
break
|
||||
|
||||
# Find nearest numerator for this denominator
|
||||
numer = round(fractional_inches * denom)
|
||||
|
||||
# Skip if numerator is 0
|
||||
if numer == 0:
|
||||
continue
|
||||
|
||||
# Check if this fraction is close enough
|
||||
fraction_value = numer / denom
|
||||
error = abs(fraction_value - fractional_inches)
|
||||
|
||||
# Very strict tolerance - only simplify if it's a near-perfect match
|
||||
# This keeps 3/256 as 3/256, but allows floating point errors to be cleaned up
|
||||
if error < base_tolerance:
|
||||
return denom
|
||||
|
||||
# If no simpler fraction works, use max precision
|
||||
return max_precision
|
||||
|
||||
|
||||
# This function stolen from https://github.com/kevancress/MeasureIt_ARCH/blob/dcf607ce0896aa2284463c6b4ae9cd023fc54cbe/measureit_arch_baseclass.py
|
||||
# MeasureIt-ARCH is GPL-v3
|
||||
def format_distance(
|
||||
@@ -186,14 +226,18 @@ def format_distance(
|
||||
if unit_length == "INCHES":
|
||||
toInches = 1
|
||||
if unit_length == "FEET":
|
||||
toInches = 11.9999
|
||||
toInches = 12
|
||||
else:
|
||||
toInches = 39.3700787401574887
|
||||
inPerFoot = 11.9999
|
||||
inPerFoot = 12.0
|
||||
|
||||
if isArea:
|
||||
toInches = 1550
|
||||
inPerFoot = 143.999
|
||||
inPerFoot = 144
|
||||
|
||||
decInches = value * toInches
|
||||
decFeet = decInches / 12
|
||||
|
||||
if not precision:
|
||||
precision = 256
|
||||
elif precision == "1":
|
||||
@@ -202,8 +246,6 @@ def format_distance(
|
||||
precision = int(precision.split("/")[1])
|
||||
|
||||
base = int(precision)
|
||||
decInches = value * toInches
|
||||
decFeet = decInches / 12
|
||||
|
||||
# Separate ft and inches
|
||||
# Unless Inches are the specified Length Unit or unit_fraction is False
|
||||
@@ -219,20 +261,33 @@ def format_distance(
|
||||
# Separate Fractional Inches
|
||||
decInches = abs(decInches) # ignore the sign for inches
|
||||
inches = math.floor(decInches) # remove decimal
|
||||
if inches != 0:
|
||||
frac = round(base * (decInches - inches))
|
||||
|
||||
# Clean up floating point errors
|
||||
fractional_inches = decInches - inches
|
||||
tolerance = 0.01 # About 1/100 of an inch
|
||||
|
||||
# If the fractional part is very small, treat it as zero
|
||||
if fractional_inches < tolerance:
|
||||
frac = 0
|
||||
# If very close to the next whole inch, round up
|
||||
elif fractional_inches > (1.0 - tolerance):
|
||||
frac = 0
|
||||
inches += 1
|
||||
else:
|
||||
frac = round(base * (decInches))
|
||||
# Calculate fraction normally
|
||||
if inches != 0:
|
||||
frac = round(base * fractional_inches)
|
||||
else:
|
||||
frac = round(base * fractional_inches)
|
||||
|
||||
# Set proper numerator and denominator
|
||||
if frac != base:
|
||||
numcycles = int(math.log2(base))
|
||||
for i in range(numcycles):
|
||||
if frac % 2 == 0:
|
||||
frac = int(frac / 2)
|
||||
base = int(base / 2)
|
||||
else:
|
||||
break
|
||||
# Simplify using GCD
|
||||
from math import gcd
|
||||
|
||||
divisor = gcd(int(frac), int(base))
|
||||
frac = int(frac / divisor)
|
||||
base = int(base / divisor)
|
||||
else:
|
||||
frac = 0
|
||||
inches += 1
|
||||
@@ -251,31 +306,46 @@ def format_distance(
|
||||
frac = None
|
||||
if not isArea:
|
||||
add_inches = bool(inches) or not suppress_zero_inches or (inches == 0 and frac)
|
||||
|
||||
tx_dist = ""
|
||||
if feet:
|
||||
tx_dist += str(feet) + "'"
|
||||
if not feet and not add_inches:
|
||||
tx_dist += str(feet) + "'"
|
||||
if feet and add_inches:
|
||||
|
||||
# Add "0' - " when we have inches but no feet
|
||||
# But only add " - " separator if we actually have inches to show
|
||||
if not feet and add_inches:
|
||||
tx_dist += "0' - "
|
||||
elif feet and add_inches:
|
||||
tx_dist += " - "
|
||||
|
||||
if not feet and value < 0:
|
||||
tx_dist += "-"
|
||||
if add_inches:
|
||||
if feet == 0 and inches == 0:
|
||||
pass
|
||||
if feet == 0 and inches == 0 and not frac:
|
||||
# Special case: exactly zero, show "0"
|
||||
tx_dist += "0"
|
||||
elif feet == 0 and inches == 0:
|
||||
pass # Has fraction, will be added below
|
||||
else:
|
||||
tx_dist += str(inches)
|
||||
if add_inches and frac:
|
||||
if feet == 0 and inches == 0:
|
||||
pass
|
||||
pass # Has fraction, will be added below
|
||||
else:
|
||||
tx_dist += " "
|
||||
if frac:
|
||||
tx_dist += str(frac) + "/" + str(base)
|
||||
if add_inches or frac:
|
||||
tx_dist += '"'
|
||||
# Only add inch symbol if we actually added inch content
|
||||
if inches > 0 or frac > 0 or feet == 0:
|
||||
tx_dist += '"'
|
||||
|
||||
if precision == "12" and unit_system == "IMPERIAL":
|
||||
tx_dist = str(round(decFeet)) + "'"
|
||||
if tx_dist == '"':
|
||||
tx_dist = "0' - 0\""
|
||||
else:
|
||||
fmt = "%1.3f"
|
||||
sq_feet = round(value * toInches / inPerFoot, 4)
|
||||
@@ -421,8 +491,8 @@ def ortho_view_frame(
|
||||
|
||||
Similar to `bpy.types.Camera.view_frame`
|
||||
|
||||
:arg camera: camera of drawing
|
||||
:arg margin: margins, in scene units
|
||||
:param camera: camera of drawing
|
||||
:param margin: margins, in scene units
|
||||
:return: (xmin, xmax, ymin, ymax, zmin, zmax) in local camera coordinates
|
||||
"""
|
||||
props = tool.Drawing.get_camera_props(camera)
|
||||
@@ -443,8 +513,8 @@ def almost_zero(v):
|
||||
def clip_segment(bounds, segm):
|
||||
"""Clipping line segment to bounds
|
||||
|
||||
:arg bounds: (xmin, xmax, ymin, ymax)
|
||||
:arg segm: 2 vertices of the segment
|
||||
:param bounds: (xmin, xmax, ymin, ymax)
|
||||
:param segm: 2 vertices of the segment
|
||||
:return: 2 new vertices of segment or None if segment outside the bounding box
|
||||
"""
|
||||
# Liang–Barsky algorithm
|
||||
@@ -493,8 +563,8 @@ def clip_segment(bounds, segm):
|
||||
def elevate_segment(bounds, segm):
|
||||
"""Elevate line xy-perpendicular segment vertically
|
||||
|
||||
:arg bounds: (xmin, xmax, ymin, ymax)
|
||||
:arg segm: 2 vertices of the segment
|
||||
:param bounds: (xmin, xmax, ymin, ymax)
|
||||
:param segm: 2 vertices of the segment
|
||||
:return: 2 new vertices of segment or None if segment outside the bounding box
|
||||
"""
|
||||
_, _, ymin, ymax, zmin, _ = bounds
|
||||
@@ -539,3 +609,24 @@ def add_newline_between_words(text: str, newline_at: int) -> str:
|
||||
start = space_index + 1 # Skip the space itself
|
||||
|
||||
return "\n".join(result)
|
||||
|
||||
|
||||
def get_relative_z(obj: bpy.types.Object, element, abs_z: float) -> float:
|
||||
"""Return relative Z of an element, accounting for its spatial container.
|
||||
|
||||
:param obj: The Blender object representing the element.
|
||||
:param element: The IFC entity for the object.
|
||||
:param abs_z: The absolute Z value in world coordinates.
|
||||
|
||||
:return: Relative Z value if the element is inside a spatial container,
|
||||
otherwise the absolute Z.
|
||||
"""
|
||||
z = abs_z
|
||||
if element:
|
||||
spatial_container = ifcopenshell.util.element.get_container(element)
|
||||
if spatial_container:
|
||||
container_obj = tool.Ifc.get_object(spatial_container)
|
||||
if container_obj:
|
||||
spatial_container_z = container_obj.matrix_world.translation.z
|
||||
z = abs_z - spatial_container_z
|
||||
return z
|
||||
|
||||
@@ -106,14 +106,17 @@ class AddAnnotationType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
obj = bpy.data.objects.new(object_type, None)
|
||||
|
||||
obj.name = props.type_name
|
||||
ifc_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Annotation", "MODEL_VIEW")
|
||||
element = tool.Drawing.run_root_assign_class(
|
||||
obj=obj,
|
||||
ifc_class="IfcTypeProduct",
|
||||
predefined_type=object_type,
|
||||
should_add_representation=has_representation,
|
||||
context=ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Annotation", "MODEL_VIEW"),
|
||||
context=ifc_context,
|
||||
ifc_representation_class=tool.Drawing.get_ifc_representation_class(object_type),
|
||||
)
|
||||
if representation := tool.Drawing.get_representation(element, ifc_context):
|
||||
tool.Drawing.reload_representation(obj=obj, representation=representation)
|
||||
element.ApplicableOccurrence = f"IfcAnnotation/{object_type}"
|
||||
|
||||
if props.create_representation_for_type and object_type == "IMAGE":
|
||||
@@ -207,12 +210,6 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
should_duplicate_annotations=self.should_duplicate_annotations,
|
||||
)
|
||||
|
||||
# TODO: Why need to resync active drawing, if it wasn't changed.
|
||||
drawing = props.get_active_drawing()
|
||||
if drawing is None:
|
||||
return
|
||||
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing)
|
||||
|
||||
|
||||
class CreateDrawing(bpy.types.Operator):
|
||||
"""Creates/refreshes a .svg drawing
|
||||
@@ -435,21 +432,19 @@ class CreateDrawing(bpy.types.Operator):
|
||||
if os.path.isfile(svg_path) and self.props.should_use_underlay_cache:
|
||||
return svg_path
|
||||
|
||||
visible_object_names = {obj.name for obj in bpy.context.visible_objects}
|
||||
for obj in bpy.context.view_layer.objects:
|
||||
obj.hide_render = obj.name not in visible_object_names
|
||||
|
||||
assert context.scene and context.view_layer and context.screen
|
||||
context.scene.render.filepath = str(Path(svg_path).with_suffix(".png"))
|
||||
assert (drawing_style := self.cprops.get_active_drawing_style())
|
||||
|
||||
tool.Blender.sync_render_visibility()
|
||||
|
||||
if drawing_style.render_type == "DEFAULT":
|
||||
bpy.ops.render.render(write_still=True)
|
||||
else:
|
||||
previous_visibility: dict[str, bool] = {}
|
||||
collection = tool.Blender.get_object_bim_props(self.camera).collection
|
||||
assert collection
|
||||
# Hie annotations.
|
||||
# Hide annotations.
|
||||
for obj in collection.objects:
|
||||
if context.view_layer.objects.get(obj.name):
|
||||
previous_visibility[obj.name] = obj.hide_get()
|
||||
@@ -626,7 +621,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
path.attrib["d"] = d
|
||||
group.append(g)
|
||||
|
||||
def generate_wall_layers(self, context: bpy.types.Context, root) -> None:
|
||||
def generate_material_layers(self, context: bpy.types.Context, root) -> None:
|
||||
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
|
||||
if "projection" in el.get("class", "").split():
|
||||
continue
|
||||
@@ -795,8 +790,9 @@ class CreateDrawing(bpy.types.Operator):
|
||||
edge_bm.to_mesh(edge_mesh)
|
||||
edge_bm.free()
|
||||
|
||||
actual_path = svg_path[0:-4] + "0001.svg"
|
||||
freestyle_svg_exporter = tool.Blender.get_addon("freestyle_svg_exporter")
|
||||
context.scene.render.filepath = svg_path[0:-4]
|
||||
actual_path = freestyle_svg_exporter.create_path(bpy.context.scene)
|
||||
bpy.ops.render.render(write_still=False)
|
||||
|
||||
os.replace(actual_path, svg_path)
|
||||
@@ -840,6 +836,8 @@ class CreateDrawing(bpy.types.Operator):
|
||||
|
||||
if tool.Drawing.is_camera_orthographic():
|
||||
self.generate_bisect_linework(context, root)
|
||||
if self.cprops.generate_material_layers:
|
||||
self.generate_material_layers(context, root)
|
||||
self.merge_linework_and_add_metadata(root)
|
||||
self.move_elements_to_top(root)
|
||||
|
||||
@@ -934,15 +932,42 @@ class CreateDrawing(bpy.types.Operator):
|
||||
|
||||
return svg_path
|
||||
|
||||
# Add target_view and scale classes to the parent group from IFC data
|
||||
if group is not None:
|
||||
existing_classes = group.get("class", "").split()
|
||||
|
||||
# Add target_view class
|
||||
if hasattr(self, "cprops") and getattr(self.cprops, "target_view", None):
|
||||
target_view_class = tool.Drawing.canonicalise_class_name(str(self.cprops.target_view))
|
||||
target_view_full_class = f"target-view-{target_view_class}"
|
||||
if target_view_full_class not in existing_classes:
|
||||
existing_classes.append(target_view_full_class)
|
||||
|
||||
# Add scale class from EPset_Drawing.Scale
|
||||
drawing_pset = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing")
|
||||
if drawing_pset and drawing_pset.get("Scale"):
|
||||
scale_value = drawing_pset["Scale"]
|
||||
# Remove "1/" prefix if it exists
|
||||
if isinstance(scale_value, str) and scale_value.startswith("1/"):
|
||||
scale_value = scale_value[2:]
|
||||
scale_class = tool.Drawing.canonicalise_class_name(str(scale_value))
|
||||
scale_full_class = f"scale-{scale_class}"
|
||||
if scale_full_class not in existing_classes:
|
||||
existing_classes.append(scale_full_class)
|
||||
|
||||
group.set("class", " ".join(existing_classes))
|
||||
|
||||
if self.cprops.cut_mode == "BISECT":
|
||||
self.remove_cut_linework(root)
|
||||
self.generate_bisect_linework(context, root)
|
||||
self.generate_wall_layers(context, root)
|
||||
if self.cprops.generate_material_layers:
|
||||
self.generate_material_layers(context, root)
|
||||
self.merge_linework_and_add_metadata(root)
|
||||
self.move_elements_to_top(root)
|
||||
elif self.cprops.cut_mode == "OPENCASCADE":
|
||||
self.move_projection_to_bottom(root)
|
||||
self.generate_wall_layers(context, root)
|
||||
if self.cprops.generate_material_layers:
|
||||
self.generate_material_layers(context, root)
|
||||
self.merge_linework_and_add_metadata(root)
|
||||
self.move_elements_to_top(root)
|
||||
|
||||
@@ -1252,6 +1277,8 @@ class CreateDrawing(bpy.types.Operator):
|
||||
|
||||
def get_svg_classes(self, element, layer=None):
|
||||
classes = [element.is_a()]
|
||||
|
||||
# ─── Material ──────────────────────────────────────────────
|
||||
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
|
||||
material_name = ""
|
||||
if material:
|
||||
@@ -1264,18 +1291,27 @@ class CreateDrawing(bpy.types.Operator):
|
||||
else:
|
||||
classes.append("material-null")
|
||||
|
||||
# ─── Layer ─────────────────────────────────────────────────
|
||||
if layer:
|
||||
classes.append(layer.is_a())
|
||||
material_name = layer.Material.Name or "null"
|
||||
material_name = tool.Drawing.canonicalise_class_name(material_name)
|
||||
classes.append(f"layer-material-{material_name}")
|
||||
layer_material = layer.Material
|
||||
layer_material_name = layer_material.Name or "null"
|
||||
layer_material_name = tool.Drawing.canonicalise_class_name(layer_material_name)
|
||||
classes.append(f"layer-material-{layer_material_name}")
|
||||
|
||||
# Add material category if available
|
||||
if hasattr(layer_material, "Category") and layer_material.Category:
|
||||
layer_material_category = tool.Drawing.canonicalise_class_name(layer_material.Category)
|
||||
classes.append(f"layer-material-category-{layer_material_category}")
|
||||
|
||||
# ─── Metadata ──────────────────────────────────────────────
|
||||
for key in self.metadata:
|
||||
value = ifcopenshell.util.selector.get_element_value(element, key)
|
||||
if value:
|
||||
classes.append(
|
||||
tool.Drawing.canonicalise_class_name(key) + "-" + tool.Drawing.canonicalise_class_name(str(value))
|
||||
)
|
||||
|
||||
return classes
|
||||
|
||||
def is_manifold(self, obj) -> bool:
|
||||
@@ -1342,7 +1378,13 @@ class CreateDrawing(bpy.types.Operator):
|
||||
join_criteria = join_criteria.split(",")
|
||||
else:
|
||||
# Drawing convention states that same objects classes with the same material are merged when cut.
|
||||
join_criteria = ["class", "material.Name", "/Pset_.*Common/.Status", "EPset_Status.Status", "Material.Name"]
|
||||
join_criteria = [
|
||||
"class",
|
||||
"material.Name",
|
||||
"/Pset_.*Common/.Status",
|
||||
"EPset_Status.Status",
|
||||
"EPset_Status.UserDefinedStatus",
|
||||
]
|
||||
|
||||
group = root.find("{http://www.w3.org/2000/svg}g")
|
||||
joined_paths = {}
|
||||
@@ -1471,11 +1513,10 @@ class CreateDrawing(bpy.types.Operator):
|
||||
joined_paths.setdefault(hash_keys, []).append(el)
|
||||
|
||||
for key, els in joined_paths.items():
|
||||
polygons = []
|
||||
classes = set()
|
||||
queue = []
|
||||
|
||||
for el in els:
|
||||
classes.update(el.attrib["class"].split())
|
||||
classes = set(el.attrib["class"].split())
|
||||
classes.add(el.attrib["{http://www.ifcopenshell.org/ns}guid"])
|
||||
is_closed_polygon = False
|
||||
for path in el.findall("{http://www.w3.org/2000/svg}path"):
|
||||
@@ -1490,31 +1531,30 @@ class CreateDrawing(bpy.types.Operator):
|
||||
coords.append(coords[0])
|
||||
if len(coords) > 2 and coords[0] == coords[-1]:
|
||||
is_closed_polygon = True
|
||||
polygons.append(shapely.Polygon(coords))
|
||||
queue.append((shapely.Polygon(coords), classes))
|
||||
if is_closed_polygon:
|
||||
el.getparent().remove(el)
|
||||
|
||||
try:
|
||||
merged_polygons = shapely.ops.unary_union(polygons)
|
||||
except:
|
||||
print("Warning. Portions of the merge failed. Please report a bug!", polygons)
|
||||
merged_polygons = polygons
|
||||
while queue:
|
||||
polygon, polygon_classes = queue.pop()
|
||||
for polygon2, polygon2_classes in queue[:]:
|
||||
try:
|
||||
merged_polygon = shapely.union(polygon, polygon2)
|
||||
except:
|
||||
print("Warning. Portions of the merge failed. Please report a bug!", polygon, polygon2)
|
||||
continue
|
||||
if type(merged_polygon) == shapely.Polygon:
|
||||
polygon = merged_polygon
|
||||
polygon_classes.update(polygon2_classes)
|
||||
queue.remove((polygon2, polygon2_classes))
|
||||
|
||||
if type(merged_polygons) == shapely.MultiPolygon:
|
||||
merged_polygons = merged_polygons.geoms
|
||||
elif type(merged_polygons) == shapely.Polygon:
|
||||
merged_polygons = [merged_polygons]
|
||||
else:
|
||||
merged_polygons = []
|
||||
|
||||
for polygon in merged_polygons:
|
||||
g = etree.Element("g")
|
||||
path = etree.SubElement(g, "path")
|
||||
d = "M" + " L".join([",".join([str(o) for o in co]) for co in polygon.exterior.coords[0:-1]]) + " Z"
|
||||
for interior in polygon.interiors:
|
||||
d += " M" + " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]]) + " Z"
|
||||
path.attrib["d"] = d
|
||||
g.set("class", " ".join(list(classes)))
|
||||
g.set("class", " ".join(list(polygon_classes)))
|
||||
group.append(g)
|
||||
|
||||
def drawing_to_model_co(self, x: float, y: float) -> Vector:
|
||||
@@ -1856,13 +1896,15 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = tool.Drawing.get_document_props()
|
||||
# Won't be visible in UI anyway.
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
if not props.sheets or not prefs.data_dir:
|
||||
return False
|
||||
if not tool.Drawing.get_active_drawing_item():
|
||||
cls.poll_message_set("No drawing selected.")
|
||||
return False
|
||||
if not props.sheets:
|
||||
cls.poll_message_set("No sheets available.")
|
||||
return False
|
||||
if not tool.Blender.get_user_data_dir():
|
||||
cls.poll_message_set("BIM data directory not set.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -2165,27 +2207,10 @@ class ActivateModel(bpy.types.Operator):
|
||||
if model_props.show_cut_decorator:
|
||||
CutDecorator.uninstall()
|
||||
|
||||
# Preserve current visibility statuses for:
|
||||
# - non-ifc objects
|
||||
# - type product
|
||||
# - annotations (so we won't unhide other drawings)
|
||||
ifc_file = tool.Ifc.get()
|
||||
visibility_status: dict[bpy.types.Object, bool] = {}
|
||||
for obj in bpy.data.objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
hide = obj.hide_get()
|
||||
elif element.is_a("IfcAnnotation"):
|
||||
hide = True
|
||||
elif element.is_a("IfcTypeProduct"):
|
||||
hide = obj.hide_get()
|
||||
else:
|
||||
continue
|
||||
visibility_status[obj] = hide
|
||||
|
||||
if not bpy.app.background:
|
||||
with context.temp_override(**tool.Blender.get_viewport_context()):
|
||||
bpy.ops.object.hide_view_clear(select=False)
|
||||
bpy.ops.bim.activate_status_filters(only_if_enabled=True)
|
||||
|
||||
elements = {e for obj in context.visible_objects if (e := tool.Ifc.get_entity(obj))}
|
||||
@@ -2230,15 +2255,10 @@ class ActivateModel(bpy.types.Operator):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=model,
|
||||
should_reload=False,
|
||||
is_global=True,
|
||||
should_sync_changes_first=True,
|
||||
)
|
||||
|
||||
# restore visibility after hide_view_clear()
|
||||
for obj, hide_status in visibility_status.items():
|
||||
obj.hide_set(hide_status)
|
||||
|
||||
tool.Blender.reset_object_visibility()
|
||||
tool.Drawing.hide_all_drawing_collections()
|
||||
tool.Blender.update_viewport()
|
||||
bonsai.bim.handler.refresh_ui_data()
|
||||
|
||||
@@ -2335,9 +2355,8 @@ class ActivateDrawingBase(tool.Ifc.Operator):
|
||||
|
||||
dprops.active_drawing_id = self.drawing
|
||||
dprops.drawing_styles.clear()
|
||||
if ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "HasUnderlay"):
|
||||
bpy.ops.bim.reload_drawing_styles()
|
||||
bpy.ops.bim.activate_drawing_style()
|
||||
bpy.ops.bim.reload_drawing_styles()
|
||||
bpy.ops.bim.activate_drawing_style()
|
||||
|
||||
if tool.Drawing.is_camera_orthographic():
|
||||
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=tool.Ifc.get().by_id(self.drawing))
|
||||
@@ -2350,10 +2369,22 @@ class ActivateDrawingBase(tool.Ifc.Operator):
|
||||
camera = context.scene.camera
|
||||
assert camera
|
||||
camera_props = tool.Drawing.get_camera_props(camera)
|
||||
# Check if this is a reflected ceiling camera and preserve its scale
|
||||
camera_element = tool.Ifc.get_entity(camera)
|
||||
is_reflected = False
|
||||
if camera_element:
|
||||
is_reflected = (
|
||||
ifcopenshell.util.element.get_pset(camera_element, "EPset_Drawing", "TargetView")
|
||||
== "REFLECTED_PLAN_VIEW"
|
||||
)
|
||||
if is_reflected and camera.scale != (-1, -1, -1):
|
||||
camera.scale = (-1, -1, -1)
|
||||
|
||||
if camera_props.update_representation(camera.matrix_world):
|
||||
bpy.ops.bim.update_representation(obj=camera.name, ifc_representation_class="")
|
||||
# See 6452 and 6478.
|
||||
# bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT")
|
||||
# Restore the scale after update if needed
|
||||
if is_reflected:
|
||||
camera.scale = (-1, -1, -1)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -2690,21 +2721,59 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def set_raster_style(self, context: bpy.types.Context) -> None:
|
||||
scene = context.scene # Do not remove. It is used in exec later
|
||||
assert (space := tool.Blender.get_view3d_space()) # Do not remove. It is used in exec later
|
||||
style = json.loads(self.drawing_style.raster_style)
|
||||
style: dict[str, Any] = json.loads(self.drawing_style.raster_style)
|
||||
|
||||
VIEWPORT_SHADING_TYPE = "scene.display.shading.type"
|
||||
|
||||
def preprocess(path: str, value: Any) -> tuple[str, Any, bool, bool]:
|
||||
warning = False
|
||||
skip = False
|
||||
# @25.05.12
|
||||
if path == "scene.render.engine" and value == "BLENDER_EEVEE":
|
||||
value = "BLENDER_EEVEE_NEXT"
|
||||
|
||||
BLENDER_5_REMOVED = (
|
||||
"scene.render.bake_bias",
|
||||
"scene.render.bake_margin",
|
||||
"scene.render.bake_margin_type",
|
||||
"scene.render.bake_samples",
|
||||
"scene.render.bake_type",
|
||||
"scene.render.bake_user_scale",
|
||||
"scene.render.use_bake_clear",
|
||||
"scene.render.use_bake_lores_mesh",
|
||||
"scene.render.use_bake_multires",
|
||||
"scene.render.use_bake_selected_to_active",
|
||||
"scene.render.use_bake_user_scale",
|
||||
)
|
||||
|
||||
# 25.11.07, Blender 5+
|
||||
if path == "scene.render.engine" and value in ("BLENDER_EEVEE", "BLENDER_EEVEE_NEXT"):
|
||||
if value == "BLENDER_EEVEE_NEXT":
|
||||
print(
|
||||
f"Warning: Value 'BLENDER_EEVEE_NEXT' is outdated for property '{path}' "
|
||||
"since Blender 5.0 and should be replaced with 'BLENDER_EEVEE' in shading_styles.json."
|
||||
)
|
||||
warning = True
|
||||
value = tool.Blender.get_eevee_name()
|
||||
elif tool.Blender.BLENDER_5 and path in BLENDER_5_REMOVED:
|
||||
print(
|
||||
f"Warning: Value 'BLENDER_EEVEE' is outdated for property '{path}' "
|
||||
"since Blender 4.2 and should be replaced with 'BLENDER_EEVEE_NEXT' in shading_styles.json."
|
||||
f"Warning: Property '{path}' is removed "
|
||||
"since Blender 5.0 and should be also removed from shading_styles.json."
|
||||
)
|
||||
warning = True
|
||||
skip = True
|
||||
# @25.11.11
|
||||
elif (
|
||||
path == "scene.display.shading.studio_light"
|
||||
and value == "Default"
|
||||
and style[VIEWPORT_SHADING_TYPE] in ("RENDERED", "MATERIAL")
|
||||
):
|
||||
value = "forest.exr"
|
||||
print(
|
||||
f"Warning: Value 'Default' for property '{path}' and "
|
||||
f"'{VIEWPORT_SHADING_TYPE}' = '{style[VIEWPORT_SHADING_TYPE]}' is outdated "
|
||||
"and should be replaced with 'forest.exr' in shading_styles.json."
|
||||
)
|
||||
warning = True
|
||||
# @25.05.12
|
||||
if path == "scene.display.shading.wireframe_color_type" and value == "MATERIAL":
|
||||
elif path == "scene.display.shading.wireframe_color_type" and value == "MATERIAL":
|
||||
value = "THEME"
|
||||
print(
|
||||
f"Warning: Value 'MATERIAL' is outdated for property '{path}' "
|
||||
@@ -2738,7 +2807,16 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
return path, value, warning, skip
|
||||
|
||||
for path, value in style.items():
|
||||
paths = list(style.keys())
|
||||
PRIORITY_PATHS = (
|
||||
# `scene.display.shading.studio_light` values depend on `.type`
|
||||
# so we got to set it first.
|
||||
VIEWPORT_SHADING_TYPE,
|
||||
)
|
||||
paths.sort(key=lambda p: p not in PRIORITY_PATHS)
|
||||
|
||||
for path in paths:
|
||||
value = style[path]
|
||||
path, value, warning, skip = preprocess(path, value)
|
||||
self.has_warnings_during_activation |= warning
|
||||
|
||||
@@ -2829,8 +2907,13 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if not props.schedules:
|
||||
cls.poll_message_set("No schedule selected.")
|
||||
return False
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
return props.schedules and props.sheets and prefs.data_dir
|
||||
if not props.sheets:
|
||||
cls.poll_message_set("No sheets available.")
|
||||
return False
|
||||
if not tool.Blender.get_user_data_dir():
|
||||
cls.poll_message_set("BIM data directory not set.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
props = tool.Drawing.get_document_props()
|
||||
@@ -2897,8 +2980,13 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if not props.references:
|
||||
cls.poll_message_set("No reference selected.")
|
||||
return False
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
return props.references and props.sheets and bim_props.data_dir
|
||||
if not props.sheets:
|
||||
cls.poll_message_set("No sheets available.")
|
||||
return False
|
||||
if not tool.Blender.get_user_data_dir():
|
||||
cls.poll_message_set("BIM data directory not set.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
props = tool.Drawing.get_document_props()
|
||||
@@ -3393,6 +3481,15 @@ class LoadDrawings(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
# Operator is not accessible through UI if IFC project is not saved,
|
||||
# but adding poll check to avoid using Drawings UI in scripts with unsaved project.
|
||||
if tool.Ifc.get_path():
|
||||
return True
|
||||
cls.poll_message_set("IFC project is not saved.")
|
||||
return False
|
||||
|
||||
def _execute(self, context):
|
||||
core.load_drawings(tool.Drawing)
|
||||
|
||||
@@ -3747,3 +3844,19 @@ class OpenDocumentationWebUi(bpy.types.Operator):
|
||||
else:
|
||||
bpy.ops.bim.open_web_browser(page="documentation")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ExcludeAnnotation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.exclude_annotation"
|
||||
bl_label = "Exclude Annotation"
|
||||
bl_description = "Excludes the automatic annotation reference from the drawing"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
if not (obj := bpy.context.scene.camera) or not (drawing := tool.Ifc.get_entity(obj)):
|
||||
return
|
||||
for obj in tool.Blender.get_selected_objects(include_active=False):
|
||||
if (element := tool.Ifc.get_entity(obj)) and tool.Drawing.is_auto_annotation(element):
|
||||
if referenced_element := tool.Drawing.get_annotation_element(element):
|
||||
tool.Drawing.exclude_annotation_from_drawing(referenced_element, drawing)
|
||||
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing)
|
||||
|
||||
@@ -465,6 +465,12 @@ class DocProperties(PropertyGroup):
|
||||
return None
|
||||
return tool.Ifc.get().by_id(drawing_id)
|
||||
|
||||
def get_active_target_view(self) -> Union[str, None]:
|
||||
active_drawing = self.get_active_drawing()
|
||||
if not active_drawing:
|
||||
return None
|
||||
return tool.Drawing.get_drawing_target_view(active_drawing)
|
||||
|
||||
|
||||
def update_width_height(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
|
||||
self.update_camera_resolution()
|
||||
@@ -492,6 +498,9 @@ class BIMCameraProperties(PropertyGroup):
|
||||
name="Linework Mode",
|
||||
update=get_update_layer_callback("linework_mode", "LineworkMode"),
|
||||
)
|
||||
generate_material_layers: bpy.props.BoolProperty(
|
||||
name="Generate Material Layers", description="Generate material layer linework in drawings", default=True
|
||||
)
|
||||
fill_mode: EnumProperty(
|
||||
items=[
|
||||
("NONE", "None", "Disable filling areas seen in projection"),
|
||||
@@ -738,6 +747,11 @@ class BIMTextProperties(PropertyGroup):
|
||||
name="Font Size",
|
||||
)
|
||||
newline_at: IntProperty(name="Newline At")
|
||||
reverse_list: BoolProperty(name="Reverse List", description="Reverses the order of any list.", default=False)
|
||||
list_separator: StringProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="List Separator",
|
||||
description="Text used to separate lists. Uses a comma (, ) if empty.",
|
||||
)
|
||||
symbol: EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Symbol",
|
||||
description="Symbol from symbols.svg to use for this text.",
|
||||
@@ -754,6 +768,8 @@ class BIMTextProperties(PropertyGroup):
|
||||
literals: bpy.types.bpy_prop_collection_idprop[LiteralProps]
|
||||
font_size: str
|
||||
newline_at: int
|
||||
reverse_list: bool
|
||||
list_separator: str
|
||||
symbol: Union[str, Literal["NO SYMBOL", "CUSTOM SYMBOL"]]
|
||||
custom_symbol: str
|
||||
|
||||
@@ -790,6 +806,8 @@ class BIMTextProperties(PropertyGroup):
|
||||
"FontSize": float(self.font_size),
|
||||
"Newline_At": int(self.newline_at),
|
||||
"Symbol": self.get_symbol(),
|
||||
"Reverse_List": self.reverse_list,
|
||||
"List_Separator": self.list_separator or ", ",
|
||||
}
|
||||
return text_data
|
||||
|
||||
@@ -844,6 +862,19 @@ class BIMAnnotationProperties(PropertyGroup):
|
||||
)
|
||||
is_adding_type: bpy.props.BoolProperty(default=False)
|
||||
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
|
||||
tag_rotation_mode: bpy.props.EnumProperty(
|
||||
name="Tag Rotation Mode",
|
||||
description="How to orient the tag relative to the tagged object",
|
||||
items=[
|
||||
("NONE", "No Rotation", "Keep tag in default orientation"),
|
||||
("LOCAL_X", "Local X Axis", "Align tag with object's local X axis"),
|
||||
("LOCAL_Y", "Local Y Axis", "Align tag with object's local Y axis"),
|
||||
("LOCAL_Z", "Local Z Axis", "Align tag with object's local Z axis"),
|
||||
("CAMERA_Horizontal", "Camera Horizontal", "Align tag with camera X axis"),
|
||||
("CAMERA_Vertical", "Camera Vertical", "Align tag with camera Y axis"),
|
||||
],
|
||||
default="NONE",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
object_type: str
|
||||
|
||||
@@ -544,11 +544,10 @@ class Scheduler:
|
||||
"""
|
||||
Adds text to svg.
|
||||
|
||||
Args:
|
||||
p_tags: list of cell's P tags from odt file
|
||||
box_alignment: alignment of text in box
|
||||
wrap_text: if True, text will be wrapped to fit in cell
|
||||
cell_width: width of cell, used for wrapping text
|
||||
:param p_tags: list of cell's P tags from odt file
|
||||
:param box_alignment: alignment of text in box
|
||||
:param wrap_text: if True, text will be wrapped to fit in cell
|
||||
:param cell_width: width of cell, used for wrapping text
|
||||
"""
|
||||
text_lines = [str(p) for p in p_tags]
|
||||
box_alignment_params = SvgWriter.get_box_alignment_parameters(box_alignment)
|
||||
|
||||
@@ -341,17 +341,29 @@ class SheetBuilder:
|
||||
assert style_data is not None
|
||||
text = ""
|
||||
brackets_level = 0
|
||||
selector_buffer = "" # Buffer to accumulate selectors across lines
|
||||
|
||||
for l in style_data:
|
||||
if l == "{":
|
||||
if brackets_level == 0:
|
||||
cur_line = text.splitlines()[-1]
|
||||
text = text[: -len(cur_line)]
|
||||
# Get all accumulated selector text (may span multiple lines)
|
||||
# Find where the last rule ended (after last }) or start of text
|
||||
last_close = text.rfind("}")
|
||||
if last_close == -1:
|
||||
selector_text = text
|
||||
text = ""
|
||||
else:
|
||||
selector_text = text[last_close + 1 :]
|
||||
text = text[: last_close + 1]
|
||||
|
||||
# Process all selectors (split by comma)
|
||||
css_selectors = []
|
||||
# making sure cases like "text, tspan" will be
|
||||
# converted to "text.prefix, tspan.prefix"
|
||||
for css_selector in cur_line.split(","):
|
||||
css_selector = f"{css_selector.strip()}.{prefix}"
|
||||
css_selectors.append(css_selector)
|
||||
for css_selector in selector_text.split(","):
|
||||
css_selector = css_selector.strip()
|
||||
if css_selector: # Only process non-empty selectors
|
||||
css_selector = f"{css_selector}.{prefix}"
|
||||
css_selectors.append(css_selector)
|
||||
|
||||
text += ", ".join(css_selectors) + " "
|
||||
brackets_level += 1
|
||||
elif l == "}":
|
||||
|
||||
@@ -202,6 +202,15 @@ class SvgWriter:
|
||||
) -> Self:
|
||||
self.precision = precision
|
||||
self.decimal_places = decimal_places
|
||||
|
||||
# Sort annotations to ensure FILLAREA types are drawn first (at the bottom)
|
||||
def sort_key(element):
|
||||
predefined_type = ifcopenshell.util.element.get_predefined_type(element)
|
||||
# Return 0 for FILLAREA to draw them first, 1 for everything else
|
||||
return 0 if predefined_type == "FILL_AREA" else 1
|
||||
|
||||
annotations = sorted(annotations, key=sort_key)
|
||||
|
||||
for element in annotations:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if (
|
||||
@@ -839,6 +848,12 @@ class SvgWriter:
|
||||
|
||||
symbol = tool.Drawing.get_annotation_symbol(element)
|
||||
newline_at = tool.Drawing.get_newline_at(element)
|
||||
|
||||
# Get reverse_list and list_separator from EPset_Annotation
|
||||
pset_data = ifcopenshell.util.element.get_pset(element, "EPset_Annotation") or {}
|
||||
reverse_list = pset_data.get("Reverse_List", False)
|
||||
list_separator = pset_data.get("List_Separator") or ", "
|
||||
|
||||
template_text_fields = []
|
||||
if symbol:
|
||||
symbol_transform = self.get_symbol_transform(text_position_svg_str, angle, text_obj)
|
||||
@@ -855,7 +870,7 @@ class SvgWriter:
|
||||
# NOTE: zip makes sure that we iterate over the shortest list
|
||||
for field, text_literal in zip(template_text_fields, text_literals):
|
||||
field.text = tool.Drawing.replace_text_literal_variables(
|
||||
text_literal.Literal, product or element
|
||||
text_literal.Literal, product or element, reverse_list, list_separator
|
||||
)
|
||||
field.attrib["class"] = classes_str
|
||||
|
||||
@@ -875,7 +890,9 @@ class SvgWriter:
|
||||
line_number = 0
|
||||
|
||||
for text_literal in text_literals:
|
||||
text = tool.Drawing.replace_text_literal_variables(text_literal.Literal, product or element)
|
||||
text = tool.Drawing.replace_text_literal_variables(
|
||||
text_literal.Literal, product or element, reverse_list, list_separator
|
||||
)
|
||||
text_tags = self.create_text_tag(
|
||||
text,
|
||||
text_position_svg,
|
||||
@@ -1012,7 +1029,9 @@ class SvgWriter:
|
||||
|
||||
# TODO: allow metric to be configurable
|
||||
def get_text():
|
||||
z = (matrix_world @ points[0].co.xyz).z
|
||||
abs_z = (matrix_world @ points[0].co.xyz).z
|
||||
z = helper.get_relative_z(obj, element, abs_z)
|
||||
|
||||
rl = helper.format_distance(
|
||||
z,
|
||||
precision=self.precision,
|
||||
|
||||
@@ -99,12 +99,13 @@ class BIM_PT_camera(Panel):
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(props, "linework_mode")
|
||||
row = self.layout.row()
|
||||
row.prop(props, "generate_material_layers")
|
||||
if props.linework_mode == "OPENCASCADE":
|
||||
row = self.layout.row()
|
||||
row.prop(props, "fill_mode")
|
||||
row = self.layout.row()
|
||||
row.prop(props, "cut_mode")
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(props, "width")
|
||||
row = self.layout.row()
|
||||
@@ -115,7 +116,7 @@ class BIM_PT_camera(Panel):
|
||||
# See #6686.
|
||||
if (
|
||||
props.has_underlay
|
||||
and str(render.engine) == "BLENDER_EEVEE_NEXT"
|
||||
and str(render.engine) == tool.Blender.get_eevee_name()
|
||||
and ((megapixels := (render.resolution_x * render.resolution_y / 10**6)) > MEGAPIXELS_WARNING_THRESHOLD)
|
||||
):
|
||||
box = self.layout.box()
|
||||
@@ -193,6 +194,7 @@ class BIM_PT_element_filters(Panel):
|
||||
text = "Exclude Filter" if ElementFiltersData.data["has_exclude_filter"] else "No Exclude Filter Found"
|
||||
icon = "GREASEPENCIL" if ElementFiltersData.data["has_exclude_filter"] else "ADD"
|
||||
row.label(text=text, icon="FILTER")
|
||||
row.operator("bim.exclude_annotation", icon="REMOVE", text="")
|
||||
row.operator("bim.enable_editing_element_filter", icon=icon, text="").filter_mode = "EXCLUDE"
|
||||
|
||||
|
||||
@@ -590,6 +592,10 @@ class BIM_PT_text(Panel):
|
||||
row.prop(props, "font_size")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "newline_at")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "reverse_list")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "list_separator")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "symbol")
|
||||
@@ -648,6 +654,12 @@ class BIM_PT_text(Panel):
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Newline_At")
|
||||
row.label(text=str(text_data["Newline_At"]))
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Reverse_List")
|
||||
row.label(text=str(text_data["Reverse_List"]))
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="List_Separator")
|
||||
row.label(text=str(text_data["List_Separator"]))
|
||||
|
||||
for literal_data in text_data["Literals"]:
|
||||
box = self.layout.box()
|
||||
|
||||
@@ -180,9 +180,6 @@ def create_annotation_occurrence(context):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
|
||||
if obj.data and not relating_type_rep:
|
||||
@@ -231,7 +228,6 @@ class AnnotationToolUI:
|
||||
def draw_type_selection_interface(cls):
|
||||
# shared by both sidebar and header
|
||||
object_type = cls.props.object_type
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="FILE_VOLUME")
|
||||
prop_with_search(row, cls.props, "object_type", text="")
|
||||
@@ -251,6 +247,9 @@ class AnnotationToolUI:
|
||||
add_layout_hotkey_operator(cls.layout, "Add", "S_A", "Create a new annotation")
|
||||
|
||||
if object_type in tool.Drawing.ANNOTATION_TYPES_SUPPORT_SETUP:
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="DRIVER_ROTATIONAL_DIFFERENCE")
|
||||
row.prop(cls.props, "tag_rotation_mode", text="")
|
||||
add_layout_hotkey_operator(
|
||||
cls.layout,
|
||||
"Bulk Tag",
|
||||
@@ -302,6 +301,8 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return
|
||||
|
||||
related_objects = bpy.context.selected_objects
|
||||
created_objects = []
|
||||
|
||||
for related_object in related_objects:
|
||||
obj = core.add_annotation(
|
||||
tool.Ifc,
|
||||
@@ -314,7 +315,15 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
),
|
||||
enable_editing=False,
|
||||
)
|
||||
tool.Drawing.setup_annotation_object(obj, object_type, related_object)
|
||||
tool.Drawing.setup_annotation_object(obj, object_type, related_object, props.tag_rotation_mode)
|
||||
created_objects.append(obj)
|
||||
|
||||
# Select the created annotation objects
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
for obj in created_objects:
|
||||
obj.select_set(True)
|
||||
if created_objects:
|
||||
bpy.context.view_layer.objects.active = created_objects[-1]
|
||||
|
||||
def hotkey_S_A(self):
|
||||
if bpy.ops.bim.add_annotation.poll():
|
||||
@@ -344,4 +353,5 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
continue
|
||||
related_object = tool.Ifc.get_object(related_product)
|
||||
|
||||
tool.Drawing.setup_annotation_object(obj, annotation_type, related_object)
|
||||
rotation_mode = tool.Drawing.get_annotation_props().tag_rotation_mode
|
||||
tool.Drawing.setup_annotation_object(obj, annotation_type, related_object, rotation_mode)
|
||||
|
||||
@@ -30,6 +30,7 @@ classes = (
|
||||
operator.AddSweptAreaSolidItem,
|
||||
operator.AssignRepresentationLayer,
|
||||
operator.CopyRepresentation,
|
||||
operator.DirectProfileEdit,
|
||||
operator.DisableEditingRepresentationItemShapeAspect,
|
||||
operator.DisableEditingRepresentationItemStyle,
|
||||
operator.DisableEditingRepresentationItems,
|
||||
@@ -109,8 +110,9 @@ def block_scale(scene: bpy.types.Scene) -> None:
|
||||
if obj.type == "CAMERA":
|
||||
camera = tool.Ifc.get_entity(obj)
|
||||
if ifcopenshell.util.element.get_pset(camera, "EPset_Drawing", "TargetView") == "REFLECTED_PLAN_VIEW":
|
||||
obj.scale = (-1, -1, -1)
|
||||
obj.rotation_euler = (0.0, 0.0, math.radians(180))
|
||||
# Only update if scale isn't already (-1, -1, -1)
|
||||
if obj.scale != (-1, -1, -1):
|
||||
obj.scale = (-1, -1, -1)
|
||||
else:
|
||||
if obj.scale != (1, 1, 1):
|
||||
obj.scale = (1, 1, 1)
|
||||
@@ -159,6 +161,8 @@ def register():
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("bim.override_mode_set_edit", "TAB", "PRESS")
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("bim.direct_profile_edit", "SPACE", "PRESS")
|
||||
addon_keymaps.append((km, kmi))
|
||||
# Deletion.
|
||||
kmi = km.keymap_items.new("bim.override_object_delete", "X", "PRESS")
|
||||
addon_keymaps.append((km, kmi))
|
||||
@@ -177,6 +181,8 @@ def register():
|
||||
kmi = km.keymap_items.new("bim.override_mode_set_object", "TAB", "PRESS")
|
||||
kmi.properties.should_save = True
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("bim.direct_profile_edit", "SPACE", "PRESS")
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("wm.call_menu", "P", "PRESS")
|
||||
kmi.properties.name = ui.BIM_MT_hotkey_separate.bl_idname
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
@@ -42,6 +42,7 @@ import bonsai.core.geometry
|
||||
import bonsai.core.geometry as core
|
||||
import bonsai.core.aggregate
|
||||
import bonsai.core.nest
|
||||
import bonsai.core.spatial
|
||||
import bonsai.core.style
|
||||
import bonsai.core.root
|
||||
import bonsai.core.drawing
|
||||
@@ -427,9 +428,7 @@ class SwitchRepresentation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
ifc_definition_id: bpy.props.IntProperty()
|
||||
should_reload: bpy.props.BoolProperty()
|
||||
disable_opening_subtractions: bpy.props.BoolProperty()
|
||||
should_switch_all_meshes: bpy.props.BoolProperty()
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -465,9 +464,6 @@ class SwitchRepresentation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=self.should_reload,
|
||||
is_global=self.should_switch_all_meshes,
|
||||
should_sync_changes_first=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -721,9 +717,6 @@ class UpdateParametricRepresentation(bpy.types.Operator):
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=tool.Ifc.get().by_id(props.ifc_definition_id),
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
if show_representation_parameters:
|
||||
core.get_representation_ifc_parameters(tool.Geometry, obj=obj)
|
||||
@@ -817,9 +810,17 @@ class OverrideDelete(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return len(context.selected_objects) > 0
|
||||
# Match `object.delete` poll for consistency.
|
||||
# `object.delete` poll just checks for OBJECT mode.
|
||||
poll = bpy.ops.object.delete.poll()
|
||||
if poll:
|
||||
return True
|
||||
cls.poll_message_set("Only available in OBJECT mode")
|
||||
return False
|
||||
|
||||
def execute(self, context):
|
||||
if not context.selected_objects:
|
||||
return {"FINISHED"}
|
||||
# Deep magick from the dawn of time
|
||||
if tool.Ifc.get() is None:
|
||||
bpy.ops.object.delete(use_global=self.use_global, confirm=self.confirm)
|
||||
@@ -831,6 +832,8 @@ class OverrideDelete(bpy.types.Operator):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def invoke(self, context, event):
|
||||
if not context.selected_objects:
|
||||
return {"FINISHED"}
|
||||
assert context.window_manager
|
||||
ifc_file = tool.Ifc.get()
|
||||
if ifc_file is None:
|
||||
@@ -866,6 +869,10 @@ class OverrideDelete(bpy.types.Operator):
|
||||
objects_to_remove = context.selected_objects
|
||||
|
||||
self.process_arrays(context)
|
||||
|
||||
# Track aggregates before deleting their parts
|
||||
aggregates_to_check = self.track_aggregates(objects_to_remove)
|
||||
|
||||
clear_active_object = True
|
||||
|
||||
for i, obj in enumerate(objects_to_remove, 1):
|
||||
@@ -890,13 +897,28 @@ class OverrideDelete(bpy.types.Operator):
|
||||
continue
|
||||
if ifcopenshell.util.element.get_pset(element, "BBIM_Array"):
|
||||
self.report({"INFO"}, "Elements that are part of an array cannot be deleted.")
|
||||
return {"FINISHED"}
|
||||
continue
|
||||
if element.is_a("IfcGridAxis"):
|
||||
# Deleting the last W axis is OK
|
||||
if ((grid := element.PartOfU) and len(grid[0].UAxes) == 1) or (
|
||||
(grid := element.PartOfV) and len(grid[0].VAxes) == 1
|
||||
):
|
||||
self.report(
|
||||
{"INFO"}, "The last grid axis of a grid cannot be deleted. Delete the grid instead."
|
||||
)
|
||||
continue
|
||||
if tool.Drawing.is_auto_annotation(element):
|
||||
self.report({"INFO"}, "References cannot be deleted. Exclude the referenced element instead.")
|
||||
continue
|
||||
tool.Geometry.delete_ifc_object(obj)
|
||||
elif tool.Geometry.is_representation_item(obj):
|
||||
tool.Geometry.delete_ifc_item(obj)
|
||||
else:
|
||||
bpy.data.objects.remove(obj)
|
||||
|
||||
# Delete empty aggregates after deleting their parts
|
||||
self.delete_empty_aggregates(aggregates_to_check)
|
||||
|
||||
for opening in tool.Model.get_model_props().openings:
|
||||
if opening.obj is not None and not tool.Ifc.get_entity(opening.obj):
|
||||
bpy.data.objects.remove(opening.obj)
|
||||
@@ -929,6 +951,53 @@ class OverrideDelete(bpy.types.Operator):
|
||||
data["old_file"].redo()
|
||||
tool.Ifc.set(data["new_file"])
|
||||
|
||||
def track_aggregates(self, objects_to_remove):
|
||||
"""Track aggregates that contain objects being deleted"""
|
||||
aggregates_to_check = set()
|
||||
for obj in objects_to_remove:
|
||||
if not tool.Blender.is_valid_data_block(obj):
|
||||
continue
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
if aggregate:
|
||||
aggregates_to_check.add(aggregate)
|
||||
return aggregates_to_check
|
||||
|
||||
def delete_empty_aggregates(self, aggregates_to_check):
|
||||
"""Delete aggregates that now have no parts"""
|
||||
deleted_aggregates = []
|
||||
for aggregate in aggregates_to_check:
|
||||
# Check if aggregate still exists (might have been deleted already)
|
||||
try:
|
||||
aggregate.id()
|
||||
except:
|
||||
continue
|
||||
|
||||
# Skip spatial elements - they should not be deleted even if empty
|
||||
if aggregate.is_a("IfcSpatialElement"):
|
||||
continue
|
||||
|
||||
related_objects = ifcopenshell.util.element.get_parts(aggregate)
|
||||
if len(related_objects) == 0:
|
||||
aggregate_name = aggregate.Name or f"{aggregate.is_a()} #{aggregate.id()}"
|
||||
deleted_aggregates.append(aggregate_name)
|
||||
|
||||
aggregate_obj = tool.Ifc.get_object(aggregate)
|
||||
if aggregate_obj and tool.Blender.is_valid_data_block(aggregate_obj):
|
||||
tool.Geometry.delete_ifc_object(aggregate_obj)
|
||||
|
||||
# Show info message if aggregates were deleted
|
||||
if deleted_aggregates:
|
||||
if len(deleted_aggregates) == 1:
|
||||
self.report(
|
||||
{"INFO"}, f"Aggregate '{deleted_aggregates[0]}' was deleted because it had no remaining parts"
|
||||
)
|
||||
else:
|
||||
aggregate_list = ", ".join(f"'{name}'" for name in deleted_aggregates)
|
||||
self.report({"INFO"}, f"Aggregates {aggregate_list} were deleted because they had no remaining parts")
|
||||
|
||||
def process_arrays(self, context: bpy.types.Context) -> None:
|
||||
ifc_file = tool.Ifc.get()
|
||||
selected_objects = set(context.selected_objects)
|
||||
@@ -976,9 +1045,18 @@ class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context) -> bool:
|
||||
return len(getattr(context, "selected_ids", [])) > 0
|
||||
# Match `outliner.delete` poll for consistency.
|
||||
# `outliner.delete` just checks `area.type` == `OUTLINER`.
|
||||
poll = bpy.ops.outliner.delete.poll()
|
||||
if poll:
|
||||
return True
|
||||
cls.poll_message_set("Only available from Outliner.")
|
||||
return False
|
||||
|
||||
def execute(self, context):
|
||||
if len(getattr(context, "selected_ids", [])) == 0:
|
||||
return {"FINISHED"}
|
||||
|
||||
# In this override, we don't check self.hierarchy. This effectively
|
||||
# makes Delete and Delete Hierarchy identical. This is on purpose, since
|
||||
# non-hierarchical deletion may imply a whole bunch of potentially
|
||||
@@ -997,6 +1075,9 @@ class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
if len(getattr(context, "selected_ids", [])) == 0:
|
||||
return {"FINISHED"}
|
||||
|
||||
assert context.window_manager
|
||||
ifc_file = tool.Ifc.get()
|
||||
if ifc_file:
|
||||
@@ -1067,10 +1148,18 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
is_interactive: bpy.props.BoolProperty(name="Is Interactive", default=True)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return len(context.selected_objects) > 0
|
||||
def poll(cls, context) -> bool:
|
||||
# Match `object.duplicate_move` poll for consistency.
|
||||
# `object.duplicate_move` poll checks for OBJECT mode.
|
||||
poll = bpy.ops.object.duplicate_move.poll()
|
||||
if poll:
|
||||
return True
|
||||
cls.poll_message_set("Only available in OBJECT mode")
|
||||
return False
|
||||
|
||||
def execute(self, context):
|
||||
if not context.selected_objects:
|
||||
return {"FINISHED"}
|
||||
return OverrideDuplicateMove.execute_duplicate_operator(self, context, linked=False)
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -1093,7 +1182,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
|
||||
@staticmethod
|
||||
def execute_ifc_duplicate_operator(operator: bpy.types.Operator, context: bpy.types.Context, linked: bool = False):
|
||||
objects_to_remove = set()
|
||||
objects_to_remove: set[bpy.types.Object] = set()
|
||||
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
@@ -1151,6 +1240,9 @@ class DuplicateMoveLinkedAggregateMacro(bpy.types.Macro):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
|
||||
OldToNewType = dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]
|
||||
|
||||
|
||||
class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
bl_idname = "bim.object_duplicate_move_linked_aggregate"
|
||||
bl_label = "IFC Duplicate and Move Linked Aggregate"
|
||||
@@ -1173,9 +1265,9 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
self.new_active_obj = None
|
||||
self.group_name = "BBIM_Linked_Aggregate"
|
||||
self.pset_name = "BBIM_Linked_Aggregate"
|
||||
old_to_new = {}
|
||||
all_old_to_new = {} # Track all duplicates created
|
||||
|
||||
def select_objects_and_add_data(element):
|
||||
def select_objects_and_add_data(element: ifcopenshell.entity_instance) -> None:
|
||||
add_linked_aggregate_group(element)
|
||||
obj = tool.Ifc.get_object(element)
|
||||
obj.select_set(True)
|
||||
@@ -1189,8 +1281,6 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
select_objects_and_add_data(part)
|
||||
else:
|
||||
index = add_linked_aggregate_pset(part, index)
|
||||
# index += 1
|
||||
|
||||
obj = tool.Ifc.get_object(part)
|
||||
obj.select_set(True)
|
||||
|
||||
@@ -1203,20 +1293,15 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
|
||||
if not pset:
|
||||
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name=self.pset_name)
|
||||
|
||||
ifcopenshell.api.pset.edit_pset(
|
||||
ifc_file,
|
||||
pset=pset,
|
||||
properties=properties,
|
||||
)
|
||||
|
||||
index += 1
|
||||
else:
|
||||
pass
|
||||
|
||||
return index
|
||||
|
||||
def add_linked_aggregate_group(element):
|
||||
def add_linked_aggregate_group(element: ifcopenshell.entity_instance) -> None:
|
||||
linked_aggregate_group = None
|
||||
product_groups_name = [
|
||||
r.RelatingGroup.Name
|
||||
@@ -1229,25 +1314,21 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
linked_aggregate_group = ifcopenshell.api.group.add_group(ifc_file, name=self.group_name)
|
||||
ifcopenshell.api.group.assign_group(ifc_file, products=[element], group=linked_aggregate_group)
|
||||
|
||||
def custom_incremental_naming_for_element_assembly(old_to_new):
|
||||
def custom_incremental_naming_for_element_assembly(old_to_new: OldToNewType) -> None:
|
||||
for new in old_to_new.values():
|
||||
if new[0].is_a("IfcElementAssembly"):
|
||||
group_elements: list[ifcopenshell.entity_instance] = next(
|
||||
r.RelatedObjects
|
||||
for r in getattr(new[0], "HasAssignments", []) or []
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
|
||||
)
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(new[0], "BBIM_Linked_Aggregate")
|
||||
new_obj = tool.Ifc.get_object(new[0])
|
||||
new_obj.name = pset["Name"] + "_" + str(pset["Aggregate_Index"])
|
||||
new_obj.name = f"{pset['Name']}_{pset['Aggregate_Index']:02d}"
|
||||
|
||||
def get_max_index(parts):
|
||||
psets = [ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate") for p in parts]
|
||||
index = [i["Index"] for i in psets if i]
|
||||
if len(index) > 0:
|
||||
index = max(index)
|
||||
psets = [
|
||||
ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate")
|
||||
for p in parts
|
||||
if ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate")
|
||||
]
|
||||
if psets:
|
||||
index = max([i["Index"] for i in psets if i])
|
||||
return index
|
||||
else:
|
||||
return 0
|
||||
@@ -1261,23 +1342,28 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
|
||||
]
|
||||
ifcopenshell.api.group.assign_group(ifc_file, group=linked_aggregate_group[0], products=new)
|
||||
|
||||
group_elements: list[ifcopenshell.entity_instance] = next(
|
||||
r.RelatedObjects
|
||||
for r in getattr(new[0], "HasAssignments", []) or []
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
|
||||
)
|
||||
if linked_aggregate_group:
|
||||
ifcopenshell.api.group.assign_group(ifc_file, group=linked_aggregate_group[0], products=new)
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(old, "BBIM_Linked_Aggregate")
|
||||
if pset:
|
||||
new_pset = ifcopenshell.api.pset.add_pset(ifc_file, product=new[0], name=self.pset_name)
|
||||
if pset["Index"] == 0:
|
||||
group_elements = []
|
||||
if new[0].is_a("IfcElementAssembly"):
|
||||
group_elements = next(
|
||||
(
|
||||
r.RelatedObjects
|
||||
for r in getattr(new[0], "HasAssignments", []) or []
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
and "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
|
||||
),
|
||||
[],
|
||||
)
|
||||
properties = {
|
||||
"Index": pset["Index"],
|
||||
"Name": pset["Name"],
|
||||
"Aggregate_Index": len(group_elements) - 1,
|
||||
"Aggregate_Index": len(group_elements) - 1 if group_elements else 0,
|
||||
}
|
||||
else:
|
||||
properties = {"Index": pset["Index"]}
|
||||
@@ -1297,43 +1383,73 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
location_diff = new_obj.location - base_obj_location
|
||||
new_obj.location = context.scene.cursor.location + location_diff
|
||||
|
||||
if len(context.selected_objects) != 1:
|
||||
# Process multiple selected aggregates
|
||||
selected_aggregates = []
|
||||
for obj in context.selected_objects:
|
||||
selected_element = tool.Ifc.get_entity(obj)
|
||||
if not selected_element:
|
||||
continue
|
||||
|
||||
# Find the aggregate element
|
||||
if selected_element.is_a("IfcElementAssembly"):
|
||||
selected_aggregates.append(selected_element)
|
||||
elif selected_element.Decomposes:
|
||||
if selected_element.Decomposes[0].RelatingObject.is_a("IfcElementAssembly"):
|
||||
selected_aggregates.append(selected_element.Decomposes[0].RelatingObject)
|
||||
|
||||
# Remove duplicates
|
||||
selected_aggregates = list(set(selected_aggregates))
|
||||
|
||||
if not selected_aggregates:
|
||||
self.report({"INFO"}, "No Linked Aggregates selected.")
|
||||
return {"FINISHED"}
|
||||
|
||||
selected_obj = context.selected_objects[0]
|
||||
selected_element = tool.Ifc.get_entity(selected_obj)
|
||||
assert selected_element
|
||||
|
||||
if selected_element.is_a("IfcElementAssembly"):
|
||||
pass
|
||||
elif selected_element.Decomposes:
|
||||
if selected_element.Decomposes[0].RelatingObject.is_a("IfcElementAssembly"):
|
||||
selected_element = selected_element.Decomposes[0].RelatingObject
|
||||
selected_obj = tool.Ifc.get_object(selected_element)
|
||||
else:
|
||||
self.report({"INFO"}, "Object is not part of a IfcElementAssembly.")
|
||||
return {"FINISHED"}
|
||||
|
||||
select_objects_and_add_data(selected_element)
|
||||
|
||||
old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
|
||||
|
||||
tool.Root.recreate_aggregate(old_to_new)
|
||||
|
||||
copy_linked_aggregate_data(old_to_new)
|
||||
|
||||
custom_incremental_naming_for_element_assembly(old_to_new)
|
||||
|
||||
if location_from_3d_cursor:
|
||||
get_location_from_3d_cursor(old_to_new, selected_element)
|
||||
|
||||
# Deselect all first
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
new_aggregate = old_to_new[selected_element][0]
|
||||
tool.Blender.set_active_object(tool.Ifc.get_object(new_aggregate))
|
||||
|
||||
# Process each selected aggregate
|
||||
for aggregate in selected_aggregates:
|
||||
aggregate_obj = tool.Ifc.get_object(aggregate)
|
||||
|
||||
# Select and prepare the aggregate for duplication
|
||||
select_objects_and_add_data(aggregate)
|
||||
|
||||
# Duplicate the aggregate
|
||||
old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
|
||||
all_old_to_new.update(old_to_new) # Collect all duplicates
|
||||
|
||||
# Recreate aggregate structure
|
||||
tool.Root.recreate_aggregate(old_to_new)
|
||||
|
||||
# Copy linked aggregate data
|
||||
copy_linked_aggregate_data(old_to_new)
|
||||
|
||||
# Apply custom naming
|
||||
custom_incremental_naming_for_element_assembly(old_to_new)
|
||||
|
||||
# Apply 3D cursor location if requested
|
||||
if location_from_3d_cursor:
|
||||
get_location_from_3d_cursor(old_to_new, aggregate)
|
||||
|
||||
# Deselect for next iteration
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
|
||||
# Select all newly created aggregates
|
||||
for aggregate in selected_aggregates:
|
||||
if aggregate in all_old_to_new:
|
||||
new_aggregate = all_old_to_new[aggregate][0]
|
||||
new_aggregate_obj = tool.Ifc.get_object(new_aggregate)
|
||||
if new_aggregate_obj:
|
||||
new_aggregate_obj.select_set(True)
|
||||
|
||||
# Set active object to the first new aggregate
|
||||
if all_old_to_new:
|
||||
first_new_aggregate = next(iter(all_old_to_new.values()))[0]
|
||||
context.view_layer.objects.active = tool.Ifc.get_object(first_new_aggregate)
|
||||
|
||||
bonsai.bim.handler.refresh_ui_data()
|
||||
|
||||
return old_to_new
|
||||
return all_old_to_new
|
||||
|
||||
|
||||
class DuplicateLinkedAggregateTo3dCursor(bpy.types.Operator):
|
||||
@@ -1369,7 +1485,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
self.pset_name = "BBIM_Linked_Aggregate"
|
||||
refresh_start_time = time()
|
||||
old_to_new = {}
|
||||
original_data: dict[int, dict[int, str]] = {}
|
||||
original_data: dict[int, dict[int, dict[str, Any]]] = {}
|
||||
|
||||
def delete_objects(element: ifcopenshell.entity_instance) -> None:
|
||||
"""Remove IfcElementAssembly and it's parts."""
|
||||
@@ -1415,8 +1531,8 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
def get_original_data(element: ifcopenshell.entity_instance) -> dict[int, dict[int, str]]:
|
||||
group = next(
|
||||
def get_original_data(element: ifcopenshell.entity_instance) -> dict[int, dict[int, dict[str, Any]]]:
|
||||
group: int = next(
|
||||
r.RelatingGroup
|
||||
for r in getattr(element, "HasAssignments", []) or []
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
@@ -1424,15 +1540,22 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
).id()
|
||||
original_data[group] = {}
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
|
||||
index = pset["Index"]
|
||||
pset: dict[str, Any] = ifcopenshell.util.element.get_pset(element, self.pset_name)
|
||||
index: int = pset["Index"]
|
||||
annotations = get_assignments(element)
|
||||
original_data[group][index] = {"Name": str(pset["Aggregate_Index"]), "Assignment": annotations}
|
||||
container = ifcopenshell.util.element.get_container(element)
|
||||
original_data[group][index] = {
|
||||
"Name": pset["Name"],
|
||||
"Aggregate_Index": pset["Aggregate_Index"],
|
||||
"Assignment": annotations,
|
||||
"Container": container,
|
||||
}
|
||||
|
||||
parts = ifcopenshell.util.element.get_parts(element)
|
||||
if parts:
|
||||
for part in parts:
|
||||
if part.is_a("IfcElementAssembly"):
|
||||
# TODO: unused expression.
|
||||
original_data | get_original_data(part)
|
||||
else:
|
||||
try:
|
||||
@@ -1470,13 +1593,22 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
index = pset["Index"]
|
||||
|
||||
if index == 0:
|
||||
obj.name = pset["Name"]
|
||||
obj.name = pset["Name"] + "_" + str(original_data[group][index]["Aggregate_Index"])
|
||||
ifc_file = tool.Ifc.get()
|
||||
ifcopenshell.api.pset.edit_pset(
|
||||
ifc_file,
|
||||
ifc_file.by_id(pset["id"]),
|
||||
properties={"Aggregate_Index": int(original_data[group][index]["Name"])},
|
||||
properties={"Aggregate_Index": int(original_data[group][index]["Aggregate_Index"])},
|
||||
)
|
||||
bonsai.core.spatial.assign_container(
|
||||
tool.Ifc,
|
||||
tool.Collector,
|
||||
tool.Spatial,
|
||||
container=original_data[group][index]["Container"],
|
||||
element_obj=obj,
|
||||
)
|
||||
for part in ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(obj)):
|
||||
tool.Collector.assign(tool.Ifc.get_object(part))
|
||||
assignments = original_data[group][index]["Assignment"]
|
||||
if assignments:
|
||||
assign_to_annotations(obj, assignments)
|
||||
@@ -1889,9 +2021,6 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Geometry,
|
||||
obj=self.target,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
apply_openings=True,
|
||||
)
|
||||
|
||||
@@ -2420,6 +2549,264 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props.is_changing_mode = False
|
||||
|
||||
|
||||
class DirectProfileEdit(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.direct_profile_edit"
|
||||
bl_label = "IFC Direct Profile Edit"
|
||||
bl_description = "Directly enter profile/axis edit mode, or exit back to object mode"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
# Only allow in OBJECT or EDIT modes
|
||||
if context.mode not in ("OBJECT", "EDIT_MESH", "EDIT_CURVE"):
|
||||
return False
|
||||
|
||||
# Must have an active object
|
||||
if not context.active_object:
|
||||
return False
|
||||
|
||||
# In edit mode, we're good to go (we'll exit)
|
||||
if context.mode in ("EDIT_MESH", "EDIT_CURVE"):
|
||||
return True
|
||||
|
||||
# In object mode, check if it's an editable object type
|
||||
obj = context.active_object
|
||||
|
||||
# Only mesh and curve objects are editable
|
||||
if obj.type not in ("MESH", "CURVE"):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
# Ensure we're only working with the active object
|
||||
# Deselect all other objects if multiple are selected
|
||||
active_obj = context.active_object
|
||||
if active_obj and context.mode == "OBJECT":
|
||||
if len(context.selected_objects) > 1:
|
||||
self.report(
|
||||
{"WARNING"},
|
||||
"Currently Direct Profile Edit only works on one object at a time. Operating on active object only.",
|
||||
)
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
active_obj.select_set(True)
|
||||
context.view_layer.objects.active = active_obj
|
||||
|
||||
# If we're already in edit mode, exit to object mode and SAVE changes
|
||||
if context.mode in ("EDIT_MESH", "EDIT_CURVE"):
|
||||
return self.handle_exit_edit_mode(context)
|
||||
|
||||
# We're in object mode, try to enter profile/axis edit
|
||||
return self.handle_enter_edit_mode(context)
|
||||
|
||||
def handle_exit_edit_mode(self, context):
|
||||
"""Handle exiting from edit mode and saving changes."""
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Check if we're editing a representation item
|
||||
if tool.Geometry.is_representation_item(obj):
|
||||
return self.exit_item_edit_mode(context, obj)
|
||||
|
||||
# Check if we're editing an element-level profile or axis
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element and tool.Geometry.has_mesh_properties(obj.data):
|
||||
return self.exit_element_edit_mode(context, obj, element)
|
||||
|
||||
# For other edit modes, use standard operator
|
||||
return bpy.ops.bim.override_mode_set_object()
|
||||
|
||||
def exit_item_edit_mode(self, context, obj):
|
||||
"""Exit from SweptAreaSolid item editing."""
|
||||
try:
|
||||
item = tool.Geometry.get_active_representation(obj)
|
||||
if not item or not item.is_a("IfcSweptAreaSolid"):
|
||||
return bpy.ops.bim.override_mode_set_object()
|
||||
|
||||
# Use the standard save workflow for items
|
||||
from bonsai.bim.module.model.decorator import ProfileDecorator
|
||||
|
||||
ProfileDecorator.uninstall()
|
||||
tool.Blender.toggle_edit_mode(context)
|
||||
|
||||
profile = tool.Model.export_profile(obj)
|
||||
if not profile:
|
||||
|
||||
def msg(self, context):
|
||||
self.layout.label(text="INVALID PROFILE")
|
||||
|
||||
context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
|
||||
tool.Blender.toggle_edit_mode(context)
|
||||
ProfileDecorator.install(context)
|
||||
return {"CANCELLED"}
|
||||
|
||||
old_profile = item.SweptArea
|
||||
profile.ProfileName = old_profile.ProfileName
|
||||
|
||||
ifc_file = tool.Ifc.get()
|
||||
for inverse in ifc_file.get_inverse(old_profile):
|
||||
ifcopenshell.util.element.replace_attribute(inverse, old_profile, profile)
|
||||
|
||||
tool.Profile.replace_profile_in_profiles_ui(old_profile.id(), profile.id())
|
||||
ifcopenshell.util.element.remove_deep2(ifc_file, old_profile)
|
||||
|
||||
props = tool.Geometry.get_geometry_props()
|
||||
if props.representation_obj:
|
||||
tool.Geometry.reload_representation(props.representation_obj)
|
||||
tool.Geometry.disable_item_mode()
|
||||
|
||||
return {"FINISHED"}
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Failed to save item changes: {str(e)}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
def exit_element_edit_mode(self, context, obj, element):
|
||||
"""Exit from element-level profile or axis editing."""
|
||||
try:
|
||||
mesh_props = tool.Geometry.get_mesh_props(obj.data)
|
||||
|
||||
# Check if we're editing axis
|
||||
if mesh_props.subshape_type == "AXIS":
|
||||
return bpy.ops.bim.edit_extrusion_axis()
|
||||
|
||||
# Otherwise, we're editing a profile
|
||||
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if body:
|
||||
body = ifcopenshell.util.representation.resolve_representation(body)
|
||||
extrusion = tool.Model.get_extrusion(body)
|
||||
if extrusion:
|
||||
return bpy.ops.bim.edit_extrusion_profile()
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Failed to save profile/axis changes: {str(e)}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Fallback to standard operator
|
||||
return bpy.ops.bim.override_mode_set_object()
|
||||
|
||||
def handle_enter_edit_mode(self, context):
|
||||
"""Handle entering edit mode from object mode."""
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Check if object has mesh or curve data
|
||||
if not hasattr(obj, "data") or not obj.data:
|
||||
self.report({"INFO"}, "Object has no editable data")
|
||||
return {"CANCELLED"}
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
self.report({"INFO"}, "Active object is not an IFC element")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Check if this is a LAYER2 element (wall, railing, etc.)
|
||||
try:
|
||||
material_usage = tool.Model.get_usage_type(element)
|
||||
except:
|
||||
material_usage = None
|
||||
|
||||
if material_usage == "LAYER2":
|
||||
self.report({"ERROR"}, "LAYER2 elements (walls, railings, etc.) cannot use profile editing.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Try to get representation
|
||||
try:
|
||||
representation = tool.Geometry.get_active_representation(obj)
|
||||
except:
|
||||
representation = None
|
||||
|
||||
if not representation:
|
||||
self.report({"INFO"}, "Object has no active representation")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Check for PROFILE usage (beams, columns, members) - should edit axis
|
||||
if material_usage == "PROFILE":
|
||||
try:
|
||||
return bpy.ops.bim.enable_editing_extrusion_axis()
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Failed to enable axis editing: {str(e)}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Check if this is an element with an extrusion profile (LAYER3, etc)
|
||||
try:
|
||||
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if body:
|
||||
body = ifcopenshell.util.representation.resolve_representation(body)
|
||||
extrusion = tool.Model.get_extrusion(body)
|
||||
if extrusion and not tool.Geometry.is_representation_item(obj):
|
||||
return bpy.ops.bim.enable_editing_extrusion_profile()
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Failed to enable profile editing: {str(e)}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Otherwise, try to enter item mode and edit SweptAreaSolid
|
||||
return self.enter_item_edit_mode(context, obj)
|
||||
|
||||
def enter_item_edit_mode(self, context, obj):
|
||||
"""Enter item mode for SweptAreaSolid editing."""
|
||||
try:
|
||||
props = tool.Geometry.get_geometry_props()
|
||||
if not props.representation_obj:
|
||||
bpy.ops.bim.import_representation_items()
|
||||
props = tool.Geometry.get_geometry_props()
|
||||
|
||||
# Find the SweptAreaSolid item
|
||||
swept_solid_obj = None
|
||||
for item_obj_data in props.item_objs:
|
||||
item_obj = item_obj_data.obj
|
||||
if not item_obj:
|
||||
continue
|
||||
try:
|
||||
item = tool.Geometry.get_active_representation(item_obj)
|
||||
if item and item.is_a("IfcSweptAreaSolid"):
|
||||
swept_solid_obj = item_obj
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
if not swept_solid_obj:
|
||||
self.report({"INFO"}, "No editable SweptAreaSolid item found")
|
||||
tool.Geometry.disable_item_mode()
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Select and activate the swept solid object
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
swept_solid_obj.select_set(True)
|
||||
context.view_layer.objects.active = swept_solid_obj
|
||||
|
||||
# Import the profile to prepare for editing
|
||||
item = tool.Geometry.get_active_representation(swept_solid_obj)
|
||||
if item and item.is_a("IfcSweptAreaSolid"):
|
||||
profile = item.SweptArea
|
||||
res = tool.Model.import_profile(profile, obj=swept_solid_obj)
|
||||
if res is None:
|
||||
self.report({"ERROR"}, "Couldn't import profile for editing")
|
||||
tool.Geometry.disable_item_mode()
|
||||
return {"CANCELLED"}
|
||||
|
||||
tool.Ifc.link(item, swept_solid_obj.data)
|
||||
tool.Blender.toggle_edit_mode(context)
|
||||
|
||||
from bonsai.bim.module.model.decorator import ProfileDecorator
|
||||
|
||||
ProfileDecorator.install(context)
|
||||
|
||||
if not bpy.app.background:
|
||||
tool.Blender.set_viewport_tool("bim.cad_tool")
|
||||
|
||||
return {"FINISHED"}
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Failed to enter item edit mode: {str(e)}")
|
||||
try:
|
||||
tool.Geometry.disable_item_mode()
|
||||
except:
|
||||
pass
|
||||
return {"CANCELLED"}
|
||||
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
class FlipObject(bpy.types.Operator):
|
||||
bl_idname = "bim.flip_object"
|
||||
bl_label = "Flip Object"
|
||||
@@ -2969,6 +3356,7 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
|
||||
item_mesh = bpy.data.meshes.new("tmp")
|
||||
tool.Ifc.link(item, item_mesh)
|
||||
item_obj = bpy.data.objects.new("tmp", item_mesh)
|
||||
tool.Geometry.lock_scale(item_obj)
|
||||
tool.Geometry.name_item_object(item_obj, item)
|
||||
item_obj.matrix_world = obj.matrix_world
|
||||
bpy.context.collection.objects.link(item_obj)
|
||||
|
||||
@@ -157,8 +157,6 @@ class BIM_PT_representations(Panel):
|
||||
icon="FILE_REFRESH" if representation["is_active"] else "OUTLINER_DATA_MESH",
|
||||
text="",
|
||||
)
|
||||
op.should_switch_all_meshes = True
|
||||
op.should_reload = True
|
||||
op.ifc_definition_id = representation["id"]
|
||||
op.disable_opening_subtractions = False
|
||||
row.operator("bim.remove_representation", icon="X", text="").representation_id = representation["id"]
|
||||
|
||||
@@ -41,7 +41,7 @@ class SolarData:
|
||||
|
||||
@classmethod
|
||||
def sun_position(cls):
|
||||
return tool.Blender.get_sun_position_addon()
|
||||
return tool.Blender.get_addon("sun_position")
|
||||
|
||||
@classmethod
|
||||
def sites(cls):
|
||||
|
||||
@@ -202,7 +202,7 @@ class RadianceRender(bpy.types.Operator):
|
||||
print(f"Camera position: {camera_position}")
|
||||
print(f"Camera direction: {camera_direction}")
|
||||
|
||||
# sun_position = tool.Blender.get_sun_position_addon()
|
||||
# sun_position = tool.Blender.get_addon("sun_position")
|
||||
# azimuth, elevation = sun_position.sun_calc.get_sun_coordinates(
|
||||
# sun_pos_props.time,
|
||||
# sun_pos_props.latitude,
|
||||
|
||||
@@ -40,7 +40,7 @@ from bpy.types import PropertyGroup
|
||||
from bonsai.bim.module.light.data import SolarData
|
||||
from bonsai.bim.module.light.decorator import SolarDecorator
|
||||
|
||||
sun_position = tool.Blender.get_sun_position_addon()
|
||||
sun_position = tool.Blender.get_addon("sun_position")
|
||||
now = datetime.datetime.now()
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), "spectraldb.json"), "r") as f:
|
||||
@@ -96,11 +96,12 @@ def update_shadow_mode(self: "BIMSolarProperties", context: bpy.types.Context) -
|
||||
sun_props = tool.Blender.get_sun_props()
|
||||
assert sun_props
|
||||
if sun_props.sun_object is None:
|
||||
bpy.ops.object.light_add(type="SUN", radius=1, align="WORLD", location=(0, 0, 0), scale=(1, 1, 1))
|
||||
bpy.ops.object.move_to_collection(collection_index=0)
|
||||
sun_props.sun_object = bpy.context.active_object
|
||||
light = bpy.data.lights.new(name="Sun", type="SUN")
|
||||
sun = bpy.data.objects.new("Sun", light)
|
||||
context.scene.collection.objects.link(sun)
|
||||
sun_props.sun_object = sun
|
||||
update_sun_path(self)
|
||||
context.scene.render.engine = "BLENDER_EEVEE_NEXT"
|
||||
context.scene.render.engine = tool.Blender.get_eevee_name()
|
||||
assert context.scene.display
|
||||
assert context.scene.display.shading
|
||||
context.scene.display.shading.light = "FLAT"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user