Compare commits
574 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c79eb60959 | |||
| 538419a23f | |||
| eb19165ebc | |||
| 1138ffee7e | |||
| 0f3416774c | |||
| 1ba7977587 | |||
| 5b1b8e8f50 | |||
| 78811d331b | |||
| 4aa91cfa7f | |||
| c099a6cb75 | |||
| 029b402d11 | |||
| a243cdd6c6 | |||
| 1fae835de9 | |||
| f0c7ff1e57 | |||
| 460fd5ca87 | |||
| 054c0a9a27 | |||
| 5af1baa48b | |||
| 0e5d2cc0c9 | |||
| 48148f00a3 | |||
| 564c4b7644 | |||
| 33c53a2c9a | |||
| c049007ee8 | |||
| 05345e8ddf | |||
| 461b78665a | |||
| 6160d65d18 | |||
| d526314190 | |||
| 85bac37bed | |||
| e38184b152 | |||
| a0c8d87ad0 | |||
| 8686462eed | |||
| 6cd4a2d78b | |||
| 2c6cb2fbb2 | |||
| a60dfd8dd5 | |||
| 1b1fd1e6c5 | |||
| 1e60f23880 | |||
| eb7babb1b0 | |||
| 5797937825 | |||
| 28336140ca | |||
| ca03a88601 | |||
| 0303d217bf | |||
| 4184f2bf46 | |||
| 3b0429ace9 | |||
| baf75f552e | |||
| 4ea0c73081 | |||
| 33852be3f1 | |||
| 48d476c2b3 | |||
| 9db07883b7 | |||
| 7af53c543d | |||
| e7f6ed73ac | |||
| 8afa1a4f7d | |||
| 1dd85f6c48 | |||
| 8bd826fb20 | |||
| cbc6eefca7 | |||
| 4f86dd143a | |||
| c45c73c563 | |||
| 38f1c622ac | |||
| 38170db32a | |||
| 43d0c098a0 | |||
| 533111a1fb | |||
| 0bba31e5f9 | |||
| 9b9f0058f9 | |||
| 0c8273eb70 | |||
| c072554f21 | |||
| 18c9a57bea | |||
| eaa3301b8f | |||
| efb89acfa3 | |||
| 96256d365a | |||
| fa6ec25a39 | |||
| 44afa0df5d | |||
| c478e5bf1b | |||
| ea530858bd | |||
| 0b4cfea3a9 | |||
| e909f25432 | |||
| 2985bba157 | |||
| a1ba61636d | |||
| 1b5c20e14f | |||
| a43fae68b6 | |||
| fdd03d7294 | |||
| 2049bdd976 | |||
| d45a439f6c | |||
| 8fa5873e7e | |||
| 3fa8fce994 | |||
| b8aa90de00 | |||
| dcc1ed5607 | |||
| 2bfff12988 | |||
| 074a55b816 | |||
| a53fd97e5a | |||
| b39a0d1a7c | |||
| 44b786a21b | |||
| bddc64cad3 | |||
| 3713ec00f9 | |||
| b5a58998d5 | |||
| ca3b4d17ba | |||
| 983a1530b8 | |||
| 74e18a56ce | |||
| 05834d071f | |||
| 8f70f462cc | |||
| 456f45b346 | |||
| 53a9175802 | |||
| 379d6d9392 | |||
| 38b708ff1f | |||
| 233e73f9f6 | |||
| db85aa90d3 | |||
| 6fb1e34ab4 | |||
| 9cc683ea6e | |||
| e343494ab4 | |||
| bc9e403a58 | |||
| 8ffd9dc1f9 | |||
| 57f6c07240 | |||
| f518bbfb00 | |||
| 0f34420834 | |||
| 3ef6077a0d | |||
| 3c26cb672b | |||
| 673f757a51 | |||
| 578ab140cc | |||
| 96902bf38a | |||
| b5133c6874 | |||
| 5dfe85691b | |||
| 3b7bf8844a | |||
| 8038b77504 | |||
| b64601ee0a | |||
| 320bb7a086 | |||
| ad135f70f1 | |||
| 15b24d8ba4 | |||
| 3c2b25d923 | |||
| 3282d75aa5 | |||
| 72fc617196 | |||
| 47cad88c38 | |||
| e81dd9c7df | |||
| d16f92a445 | |||
| 8be79f9aee | |||
| ccc6f2c4c6 | |||
| deb0597000 | |||
| 348373d47e | |||
| 9e22e9486a | |||
| 3cf9017f27 | |||
| 21479482b4 | |||
| 97c660fec8 | |||
| 32af2025ec | |||
| ffc82dfb1e | |||
| 2a69af43f6 | |||
| 5c9984a12a | |||
| c938d6ae9b | |||
| 63d65f0c39 | |||
| 472a66b7ed | |||
| ddaed9e93d | |||
| 4d6eba0a06 | |||
| ed09b7ba31 | |||
| 2c2954b11a | |||
| 5317746a82 | |||
| 5af7e95bee | |||
| 618d902a57 | |||
| 68baa416fa | |||
| 7f212963d5 | |||
| d3a43c6e81 | |||
| a44a13f04c | |||
| 126ba8d88f | |||
| caf04200ce | |||
| 05f3cd41e2 | |||
| 80bc4cdb1b | |||
| dd7a47ab99 | |||
| 4eaf0e7a9c | |||
| 3db252bcc0 | |||
| d03685abfa | |||
| 3e4b1d95cb | |||
| b151ec2b0e | |||
| f909adc2dc | |||
| 913a428794 | |||
| fe6d9ae926 | |||
| 4a679812ca | |||
| 6be87c035f | |||
| 6ce8a3621a | |||
| 60ea856e51 | |||
| 419502ddb7 | |||
| 9f530d0578 | |||
| 524483e9cc | |||
| 7116a9f71a | |||
| 868ced2990 | |||
| 6a8292d24c | |||
| fca4d9dea0 | |||
| 89d5c44855 | |||
| 3e99393895 | |||
| 098ab2c237 | |||
| 94ff8349d3 | |||
| d333cb1b78 | |||
| 3f4c4a8e52 | |||
| b103e0dde5 | |||
| 883b8a523c | |||
| 8c0b06b36e | |||
| 72397f3aff | |||
| 77821caae4 | |||
| 10138e28c9 | |||
| f9ce0903f7 | |||
| a2c0045a40 | |||
| 3e10f52965 | |||
| fd8aa6d142 | |||
| ddb38b2ea0 | |||
| 74174b1e09 | |||
| 96179db7ad | |||
| 116252f857 | |||
| 538024e035 | |||
| e9670a6906 | |||
| c7428dd160 | |||
| bf9d46b506 | |||
| 7ee589cfc2 | |||
| 3083b66f61 | |||
| 074fe0d3a6 | |||
| 46e1a46dd2 | |||
| 677a9d715c | |||
| a210cf654e | |||
| 44e4ee70ae | |||
| 27e6d702e9 | |||
| fb76f985c7 | |||
| 9279aeccf6 | |||
| 92a1739d93 | |||
| dfba9827a6 | |||
| 19e007a67c | |||
| d3ef3f1501 | |||
| f2e9f76631 | |||
| 52739418d8 | |||
| 7f1fddf426 | |||
| ee4f48b566 | |||
| 1d47ad3cd4 | |||
| 36a7985f5a | |||
| e129f4afa9 | |||
| 8fd4351195 | |||
| 01f25528e8 | |||
| 3222c9ce99 | |||
| 28e6bb9e3d | |||
| 5884af0678 | |||
| f88a1038a8 | |||
| cafe8ce5f2 | |||
| 8569bcf350 | |||
| e38759c39d | |||
| 50ed8eccfd | |||
| d63d62ddd6 | |||
| 0bdb4b9fcf | |||
| 7e7c54a535 | |||
| 817fdead9b | |||
| 68a43b92ee | |||
| 595c72e08d | |||
| 5709efbdcb | |||
| 67952ed159 | |||
| 000e3acb2b | |||
| b337d5a174 | |||
| 6b0bdc564d | |||
| ab5db2fa23 | |||
| 810c71dfa2 | |||
| 5b281260f8 | |||
| 07264dafb1 | |||
| 9572fef222 | |||
| 1bbbc82b72 | |||
| aa6d03ade2 | |||
| 4c40fc1629 | |||
| 90e5b5576d | |||
| 531805fefb | |||
| 2852f53afa | |||
| 8b06aa9e0b | |||
| 52ea69bcec | |||
| 9ebe0aaa95 | |||
| 1a735c4bef | |||
| 482788a46f | |||
| a30a8cd920 | |||
| c2e7c27421 | |||
| 2f24343f8f | |||
| d259e0f490 | |||
| c09ed228c0 | |||
| d63be20c8b | |||
| f8c7f48674 | |||
| 258ca7fb8c | |||
| ec38fe2edc | |||
| 5a87ab8171 | |||
| d23d382854 | |||
| 30270db0c3 | |||
| 6d78ac4918 | |||
| 81016f3110 | |||
| 189e566740 | |||
| 5f7e4793c2 | |||
| be286c4d9d | |||
| 5ba542488a | |||
| 39960998de | |||
| 92642e6a6a | |||
| 78d33437ea | |||
| 65b3960ab3 | |||
| e81848dfba | |||
| fe9cda8ba1 | |||
| 49221739cf | |||
| c6966f9b39 | |||
| f08ed629b5 | |||
| ba675c0d28 | |||
| 3a80722621 | |||
| 9b77bb6e37 | |||
| 7a6df49348 | |||
| 666e484b2b | |||
| 256f40ed44 | |||
| 8eccb1a5ed | |||
| ce3490824d | |||
| 43c74010a5 | |||
| 6c991ed356 | |||
| c01c2a737a | |||
| c9c11a2905 | |||
| e5037dc41a | |||
| acd9250028 | |||
| a3dc477921 | |||
| 6246dda8d6 | |||
| 2683bcba20 | |||
| 97f2aa976e | |||
| 9b96073ac0 | |||
| 7efd9cea2a | |||
| c917881a8e | |||
| fa46eb140f | |||
| b8ea42540e | |||
| 671ab78c76 | |||
| a8550cea26 | |||
| 4f047274bf | |||
| eb04a7d579 | |||
| 84096529e1 | |||
| 3350f31312 | |||
| 0d79963ed8 | |||
| 43a8fa8bcb | |||
| ca8f3cf1e6 | |||
| 313a1c5705 | |||
| 53f6454784 | |||
| 2c0df1d4f5 | |||
| e384dbd548 | |||
| d22a0c7cb8 | |||
| 02a62a943c | |||
| 0625e4153c | |||
| 03f5feca88 | |||
| 85e3c6588f | |||
| 6e913a0ff9 | |||
| ff4e58a8ff | |||
| 1db028fa7b | |||
| 69a7efdab4 | |||
| 20dcb68009 | |||
| 6b0b64b869 | |||
| 09631e9565 | |||
| e9bb70f56b | |||
| bd008bdefe | |||
| 03d4782cb7 | |||
| 49c555c508 | |||
| c51c5651b2 | |||
| 364c2286d1 | |||
| 37d20eb360 | |||
| dd6912bdaa | |||
| a1fe68a17e | |||
| 3ec79cac3f | |||
| dbc6f521fe | |||
| c52f851d79 | |||
| 3380158f55 | |||
| 4e9a5f246f | |||
| 664c49a07d | |||
| 06b9cc6b38 | |||
| b9ea741e5e | |||
| 0098e38c77 | |||
| ece796fc2d | |||
| de4659570a | |||
| 62236c44ae | |||
| ce5db914db | |||
| 12699f29b7 | |||
| 54e326983f | |||
| 5fb40a390c | |||
| 0cb8716c6a | |||
| 099013fa05 | |||
| 4a8843b6bb | |||
| 33d15b42b7 | |||
| 44581c77ac | |||
| e848b249a6 | |||
| f85c3ae24c | |||
| 7c633fab31 | |||
| f716f8a357 | |||
| d156927e13 | |||
| 2ef1f4d812 | |||
| 78a36ab429 | |||
| 763a38bc9c | |||
| a23c88a53e | |||
| f6bf626349 | |||
| b66792f981 | |||
| 195af0c850 | |||
| ee56e06d86 | |||
| 146dd1e74d | |||
| 6d47cf3a51 | |||
| 9b6d53fe38 | |||
| 6902e2e2c6 | |||
| 82d136b9b3 | |||
| b8dd0450d1 | |||
| 85d76c084b | |||
| c658f7610e | |||
| 3fad28572b | |||
| 455fe80497 | |||
| 3f46d54ad9 | |||
| 89e3276c90 | |||
| bb9eba07c0 | |||
| 6d4573fb58 | |||
| 24890dd001 | |||
| 65d599bbd9 | |||
| ebe5460a9f | |||
| 01bc29c5b2 | |||
| 5ad6d455f9 | |||
| cef1c86f6d | |||
| d1caa3242f | |||
| 4f7bfec026 | |||
| df6ad70345 | |||
| a7a3ccb23a | |||
| b65f2ca062 | |||
| f991b053d3 | |||
| 47a25ca8b9 | |||
| a3f27830c5 | |||
| 1a5b5dd303 | |||
| f529bc37ee | |||
| 825ffe03b0 | |||
| 622ff05247 | |||
| d638405ff8 | |||
| c0f547c6e4 | |||
| 3accc1dbd6 | |||
| 6eab9fb593 | |||
| 2708133493 | |||
| c5e6c3e2c7 | |||
| d7b27c0620 | |||
| 75ffc4454d | |||
| a1817e8a61 | |||
| b4b334145f | |||
| 04535d27ec | |||
| 3c41f6b0f8 | |||
| a17c44c076 | |||
| eccf72ee8a | |||
| 9c9bec9c4e | |||
| 71c1cd6a9e | |||
| 0504556e7a | |||
| e9a18d030e | |||
| 6d725f60fa | |||
| 72206bc441 | |||
| c0eb9addfd | |||
| ff3a98234d | |||
| ebf6b9fc88 | |||
| c2e4b5f1fa | |||
| 9728c4d345 | |||
| f0d6500539 | |||
| 124ccf0404 | |||
| df03f7ad29 | |||
| ff8fc898a1 | |||
| 2d62d8181a | |||
| 6853735bb2 | |||
| 1c3980b6f0 | |||
| 5c224150ec | |||
| 18719c16df | |||
| ddb8abff0f | |||
| f9221717ff | |||
| da6945b352 | |||
| 62cdf76a5a | |||
| fae607f8ce | |||
| 468192e849 | |||
| edd01637e2 | |||
| 604fbc4bc9 | |||
| 4617d9f234 | |||
| 41486ba08a | |||
| 2120653104 | |||
| 43869c33f6 | |||
| fcfdc179df | |||
| 30c164d223 | |||
| b28d8b0cca | |||
| 4b5907f5dd | |||
| 78a27ef44c | |||
| 528e27234c | |||
| 8ac885ced1 | |||
| 63872abaef | |||
| 5ba155eb17 | |||
| 2ec3c4c06b | |||
| 6f55cd5169 | |||
| f5f88f4b97 | |||
| d5b92c0fea | |||
| 9355969110 | |||
| 6c998e0b8a | |||
| 51f8d648ad | |||
| a26539d6a5 | |||
| a9f27fcf17 | |||
| 28e5af853d | |||
| a7b3b5261f | |||
| 0dac56dbdf | |||
| 0992f43f11 | |||
| c1c50f8868 | |||
| d733960068 | |||
| 66fa3f0096 | |||
| cbf7b0a77a | |||
| 91f2f9438f | |||
| fcbb6e60ac | |||
| f33d613f9d | |||
| 849dfbed28 | |||
| 8a4bc34fad | |||
| aa18c1b7d6 | |||
| ae45a9b9a5 | |||
| 7aa75d98d3 | |||
| ff6db5f24d | |||
| de7c54b771 | |||
| 7fbf317ad2 | |||
| 0471f17d9a | |||
| bf134eb0f4 | |||
| 7ccb6ee110 | |||
| 9076043a5c | |||
| 1c0fcf7b40 | |||
| 4fdcb87a28 | |||
| 246460dec4 | |||
| a89a40e637 | |||
| c1fcf2528e | |||
| d4f0495caa | |||
| 669295d4ac | |||
| e4a27d49c9 | |||
| e0e8281a1e | |||
| 1cdf695891 | |||
| 580678c2c1 | |||
| a038ae434b | |||
| 698ef40ada | |||
| 276077964d | |||
| 45080a8a1f | |||
| dd348de0b7 | |||
| 61e510e45c | |||
| 05068754b9 | |||
| de561b78e0 | |||
| e6e1978584 | |||
| 0b7bdc0f9d | |||
| e868186897 | |||
| 5450233f8e | |||
| 8e215382f7 | |||
| 4149292a13 | |||
| 117f663f48 | |||
| 960610325f | |||
| ab50609854 | |||
| c203b78d34 | |||
| 5633445008 | |||
| 22ceb35f47 | |||
| 06a988e141 | |||
| 2dc29978f6 | |||
| 10b502512f | |||
| 2d0c44e25a | |||
| 3e52d93db0 | |||
| 07bee4acfa | |||
| 0da5d807b8 | |||
| 657aafbd2e | |||
| 04449d08e8 | |||
| 2b710ffbde | |||
| 7fbe365599 | |||
| f49b9f6781 | |||
| 901318b73b | |||
| 8545d870c0 | |||
| 3124d306d6 | |||
| 8e529f3f10 | |||
| 1ff45165d9 | |||
| 05256fc0f4 | |||
| be9ef33932 | |||
| fc424ebd6b | |||
| 030f3cf307 | |||
| c8eb933453 | |||
| 208132785f | |||
| cfa435e9c8 | |||
| 515e972906 | |||
| 100b0637ac | |||
| 2d8ec8a6bf | |||
| 02461233a6 | |||
| dcac1e69ca | |||
| b118400cc6 | |||
| bc430b80f6 | |||
| d2d3addf10 | |||
| 683e316088 | |||
| e279fee518 | |||
| 93c5906ad7 | |||
| 6a06607f3f | |||
| 9f73bd43b4 | |||
| 5f7d92aeb0 | |||
| c754d15da8 | |||
| 13f5d2d5de | |||
| 280eac6127 | |||
| ea0e70ef0d | |||
| a5af8807ee | |||
| 0bae861b6a |
@@ -28,7 +28,7 @@ jobs:
|
||||
sudo apt update
|
||||
sudo apt-get install --no-install-recommends \
|
||||
git cmake gcc g++ libboost-all-dev python3-all-dev swig libpcre3-dev libxml2-dev \
|
||||
liboce-foundation-dev liboce-modeling-dev liboce-ocaf-dev liboce-visualization-dev liboce-ocaf-lite-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
|
||||
|
||||
-
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
-DCMAKE_PREFIX_PATH=/usr \
|
||||
-DCMAKE_SYSTEM_PREFIX_PATH=/usr \
|
||||
-DBUILD_PACKAGE=On \
|
||||
-DOCC_INCLUDE_DIR=/usr/include/oce \
|
||||
-DOCC_INCLUDE_DIR=/usr/include/opencascade \
|
||||
-DOCC_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
|
||||
-DPYTHON_EXECUTABLE:FILEPATH=/usr/bin/python3 \
|
||||
-DPYTHON_INCLUDE_DIR:PATH=/usr/include/python3.8 \
|
||||
|
||||
@@ -23,12 +23,13 @@ jobs:
|
||||
run: echo ok go
|
||||
|
||||
build:
|
||||
needs: activate
|
||||
name: ${{ matrix.config.name }}-${{ matrix.pyver }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pyver: [py37, py39]
|
||||
pyver: [py37, py39, py310]
|
||||
config:
|
||||
- {
|
||||
name: "Windows Build",
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
python3-all-dev python3-pip \
|
||||
swig libpcre3-dev libxml2-dev \
|
||||
libtbb-dev nlohmann-json3-dev \
|
||||
liboce-foundation-dev liboce-modeling-dev liboce-ocaf-dev liboce-visualization-dev liboce-ocaf-lite-dev \
|
||||
libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \
|
||||
libhdf5-dev libcgal-dev
|
||||
|
||||
- name: ccache
|
||||
@@ -53,11 +53,10 @@ jobs:
|
||||
cmake \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_INSTALL_PREFIX=$PWD/install/ \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_PREFIX_PATH=/usr \
|
||||
-DCMAKE_SYSTEM_PREFIX_PATH=/usr \
|
||||
-DOCC_INCLUDE_DIR=/usr/include/oce \
|
||||
-DOCC_INCLUDE_DIR=/usr/include/opencascade \
|
||||
-DOCC_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
|
||||
-DPYTHON_EXECUTABLE:FILEPATH=/usr/bin/python3 \
|
||||
-DPYTHON_INCLUDE_DIR:PATH=/usr/include/python3.8 \
|
||||
@@ -83,6 +82,8 @@ jobs:
|
||||
sudo /usr/bin/python -m pip install src/bcf
|
||||
sudo /usr/bin/python -m pip install pytest
|
||||
sudo /usr/bin/python -m pip install isodate
|
||||
sudo /usr/bin/python -m pip install lark
|
||||
sudo /usr/bin/python -m pip install networkx
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
|
||||
@@ -18,14 +18,14 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
lfs: true
|
||||
submodules: recursive
|
||||
-
|
||||
name: Update ubuntu
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt-get install git cmake gcc g++ libboost1.67-all-dev python3-all-dev swig libpcre3-dev libxml2-dev
|
||||
sudo apt-get install liboce-foundation-dev liboce-modeling-dev liboce-ocaf-dev liboce-visualization-dev liboce-ocaf-lite-dev \
|
||||
libhdf5-dev
|
||||
sudo apt-get install libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \
|
||||
libhdf5-dev libcgal-dev
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
-DCMAKE_PREFIX_PATH=/usr \
|
||||
-DCMAKE_SYSTEM_PREFIX_PATH=/usr \
|
||||
-DBUILD_PACKAGE=On \
|
||||
-DOCC_INCLUDE_DIR=/usr/include/oce \
|
||||
-DOCC_INCLUDE_DIR=/usr/include/opencascade \
|
||||
-DOCC_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
|
||||
-DPYTHON_EXECUTABLE:FILEPATH=/usr/bin/python3 \
|
||||
-DPYTHON_INCLUDE_DIR:PATH=/usr/include/python3.8 \
|
||||
|
||||
@@ -8,3 +8,9 @@
|
||||
[submodule "src/svgfill"]
|
||||
path = src/svgfill
|
||||
url = https://github.com/IfcOpenShell/svgfill
|
||||
[submodule "src/ifcopenshell-python/ifcopenshell/Sample-BIM-Files"]
|
||||
path = src/ifcopenshell-python/ifcopenshell/Sample-BIM-Files
|
||||
url = https://github.com/atomczak/Sample-BIM-Files
|
||||
[submodule "src/ifcopenshell-python/test/Sample-BIM-Files"]
|
||||
path = src/ifcopenshell-python/test/Sample-BIM-Files
|
||||
url = https://github.com/atomczak/Sample-BIM-Files
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
IfcOpenShell
|
||||
============
|
||||
IfcOpenShell is an open source ([LGPL]) software library for working with the Industry Foundation Classes ([IFC])
|
||||
file format. Currently supported IFC releases are [IFC2x3 TC1] and [IFC4 Add2 TC1].
|
||||
file format. Extensive geometric support is implemented for the IFC releases [IFC2x3 TC1] and [IFC4 Add2 TC1].
|
||||
Support for parsing is provided for IFC4x1, IFC4x2, and the IFC4x3 release candidates. Extending with support for
|
||||
arbitrary IFC schemas is possible at compile-time when using C++ and at run-time when using Python.
|
||||
|
||||
For more information, see
|
||||
* [http://ifcopenshell.org](http://ifcopenshell.org)
|
||||
@@ -13,7 +15,7 @@ Prerequisites
|
||||
-------------
|
||||
* Git
|
||||
* CMake (2.6 or newer)
|
||||
* Windows: [Visual Studio] 2008 or newer with C++ toolset (or [Visual C++ Build Tools]) or [MSYS2] + MinGW
|
||||
* Windows: [Visual Studio] 2008 to 2019 (2022 not yet supported by dependency CMake) with C++ toolset (or [Visual C++ Build Tools]) or [MSYS2] + MinGW
|
||||
* *nix: GCC 4.7 or newer, or Clang (any version)
|
||||
|
||||
Dependencies
|
||||
@@ -33,7 +35,11 @@ Dependencies
|
||||
Building IfcOpenShell
|
||||
---------------------
|
||||
|
||||
**Note:** The path where the source code is cloned to can contain spaces but non-ASCII characters are very likely to cause problems with the build.
|
||||
Note 1: The path where the source code is cloned to can contain spaces but non-ASCII characters are very likely to cause problems with the build.
|
||||
|
||||
Note 2: If you had not used `git clone --recursive https://github.com/IfcOpenShell/IfcOpenshell.git`, update the submodules by running `git submodule init & git submodule update`.
|
||||
|
||||
Note 3: Be careful with special characters is the path when using the nix or win build scripts, because the OpenCASCADE build will fail on paths containing ++ and likely other situations.
|
||||
|
||||
### Compiling on Windows
|
||||
The preferred way to fetch and build this project's dependencies is to use the build scripts
|
||||
@@ -81,17 +87,26 @@ The following instructions are for Ubuntu, modify as required for other operatin
|
||||
can be experimented with and studied for pointers for other operating systems, but note that this script is not currently
|
||||
meant to be used for a typical IfcOpenShell workspace setup.
|
||||
|
||||
Note: where `make -j` is written, add a number roughly equal to the amount of CPU cores + 1.
|
||||
Note 1: It is recommeded to use OCCT for IfcOpenShell. You could use OCE as well, but sometimes it may lag behind OCCT and
|
||||
therefore not compile with the latest IfcOpenShell.
|
||||
|
||||
Note 2: where `make -j` is written, add a number roughly equal to the amount of CPU cores + 1.
|
||||
|
||||
**1)** Install most of the prerequisites and dependencies:
|
||||
|
||||
$ sudo apt-get install git cmake gcc g++ libboost-all-dev
|
||||
$ sudo apt-get install git cmake gcc g++ libboost-all-dev libcgal-dev
|
||||
|
||||
**2a)** Either use an OCE package from your operating system's software repository
|
||||
**2a)** Either use an OCCT package from your operating system's software repository
|
||||
|
||||
$ sudo apt-get install libocct-data-exchange-dev libocct-draw-dev libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev
|
||||
|
||||
**2b)** or, if OCCT is not available or the latest code is wanted, obtain and compile OCCT from https://dev.opencascade.org/release
|
||||
|
||||
**2c)** or, if you'd like to try using OCE, use the package from your operating system's software repository
|
||||
|
||||
$ sudo apt-get install liboce-foundation-dev liboce-modeling-dev liboce-ocaf-dev liboce-visualization-dev liboce-ocaf-lite-dev
|
||||
|
||||
**2b)** or (if not available, or the latest code is wanted) compile OCE yourself (note that the build takes a long time):
|
||||
**2d)** or if OCE is not available, or the latest code is wanted, compile OCE yourself (note that the build takes a long time)
|
||||
|
||||
$ sudo apt-get install libftgl-dev libtbb2 libtbb-dev libgl1-mesa-dev libfreetype6-dev
|
||||
$ git clone https://github.com/tpaviot/oce.git
|
||||
@@ -101,8 +116,6 @@ Note: where `make -j` is written, add a number roughly equal to the amount of CP
|
||||
$ make -j
|
||||
$ sudo make install
|
||||
|
||||
**2c)** or obtain and compile OCCT from https://dev.opencascade.org/release
|
||||
|
||||
**3)** For building IfcConvert with COLLADA (.dae) support (on by default), OpenCOLLADA is needed:
|
||||
|
||||
$ sudo apt-get install libpcre3-dev libxml2-dev
|
||||
@@ -128,7 +141,13 @@ dependencies' paths. `OCC_INCLUDE_DIR` might be needed to set also. `OPENCOLLADA
|
||||
$ cmake ../cmake -DOCC_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu/ \
|
||||
-DOPENCOLLADA_INCLUDE_DIR="/usr/local/include/opencollada" \
|
||||
-DOPENCOLLADA_LIBRARY_DIR="/usr/local/lib/opencollada" \
|
||||
-DPCRE_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu/
|
||||
-DPCRE_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu/ \
|
||||
-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_SUPPORT=Off
|
||||
$ make -j
|
||||
|
||||
If all worked out correctly you can now use IfcOpenShell. See the examples below.
|
||||
|
||||
@@ -60,6 +60,11 @@ if (${BUILD_CONVERT})
|
||||
OPTION(GLTF_SUPPORT "Build IfcConvert with glTF support (requires json.hpp)." OFF)
|
||||
endif()
|
||||
OPTION(USERSPACE_PYTHON_PREFIX "Installs IfcPython for the current user only instead of system-wide." OFF)
|
||||
OPTION(ADD_COMMIT_SHA "Add commit sha and branch in version number, warning results in many rebuilds, requires git" OFF)
|
||||
|
||||
IF(NOT CMAKE_BUILD_TYPE)
|
||||
SET(CMAKE_BUILD_TYPE "Release")
|
||||
ENDIF()
|
||||
|
||||
# TODO QtViewer is deprecated ATM as it uses the 0.4 API
|
||||
# OPTION(BUILD_QTVIEWER "Build IfcOpenShell Qt GUI Viewer (requires Qt 4 framework)." OFF)
|
||||
@@ -144,6 +149,7 @@ ELSE()
|
||||
MESSAGE(FATAL_ERROR "Unable to find JSON for Modern C++ header file, aborting")
|
||||
ENDIF()
|
||||
add_definitions(-DWITH_GLTF)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_GLTF)
|
||||
endif()
|
||||
|
||||
# Set INSTALL_RPATH for target
|
||||
@@ -200,6 +206,7 @@ find_package(LibXml2 REQUIRED)
|
||||
|
||||
if (IFCXML_SUPPORT)
|
||||
add_definitions(-DWITH_IFCXML)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_IFCXML)
|
||||
endif()
|
||||
|
||||
FIND_PACKAGE(Boost REQUIRED COMPONENTS ${BOOST_COMPONENTS})
|
||||
@@ -371,6 +378,8 @@ IF(COLLADA_SUPPORT)
|
||||
IF(COLLADASWStreamWriter_h)
|
||||
MESSAGE(STATUS "OpenCOLLADA header files found")
|
||||
ADD_DEFINITIONS(-DWITH_OPENCOLLADA)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_OPENCOLLADA)
|
||||
|
||||
SET(OPENCOLLADA_LIBRARY_NAMES
|
||||
GeneratedSaxParser MathMLSolver OpenCOLLADABaseUtils OpenCOLLADAFramework OpenCOLLADASaxFrameworkLoader
|
||||
OpenCOLLADAStreamWriter UTF buffer ftoa
|
||||
@@ -439,13 +448,17 @@ if(HDF5_SUPPORT)
|
||||
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.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libz${zlib_post}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libsz.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libaec.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5_cpp${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libz${zlib_post}${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libsz${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libaec${debug_postfix}.${lib_ext}"
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -462,12 +475,9 @@ if(HDF5_SUPPORT)
|
||||
endif()
|
||||
|
||||
add_definitions(-DWITH_HDF5)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_HDF5)
|
||||
endif()
|
||||
|
||||
IF(NOT CMAKE_BUILD_TYPE)
|
||||
SET(CMAKE_BUILD_TYPE "Release")
|
||||
ENDIF()
|
||||
|
||||
if(ENABLE_BUILD_OPTIMIZATIONS)
|
||||
if(MSVC)
|
||||
# NOTE: RelWithDebInfo and Release use O2 (= /Ox /Gl /Gy/ = Og /Oi /Ot /Oy /Ob2 /Gs /GF /Gy) by default,
|
||||
@@ -692,7 +702,7 @@ TARGET_LINK_LIBRARIES(IfcParse ${Boost_LIBRARIES} ${BCRYPT_LIBRARIES} ${LIBXML2_
|
||||
if (BUILD_IFCGEOM)
|
||||
|
||||
# IfcGeom
|
||||
file(GLOB IFCGEOM_H_FILES ../src/ifcgeom/*.h)
|
||||
file(GLOB IFCGEOM_H_FILES ../src/ifcgeom/*.h ../src/ifcgeom/*.i)
|
||||
file(GLOB IFCGEOM_CPP_FILES ../src/ifcgeom/*.cpp)
|
||||
set(IFCGEOM_FILES ${IFCGEOM_CPP_FILES} ${IFCGEOM_H_FILES})
|
||||
|
||||
@@ -784,21 +794,20 @@ INSTALL(TARGETS IfcGeomServer
|
||||
|
||||
endif()
|
||||
|
||||
if (ADD_COMMIT_SHA)
|
||||
find_package (Git)
|
||||
if (GIT_FOUND)
|
||||
message("git found: ${GIT_EXECUTABLE} with version ${GIT_VERSION_STRING}")
|
||||
execute_process(
|
||||
COMMAND ${GIT_EXECUTABLE} show -s --pretty=%D HEAD
|
||||
COMMAND ${GIT_EXECUTABLE} branch --contains HEAD
|
||||
OUTPUT_VARIABLE git_branches
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
string(REPLACE ", " ";" git_branch_list "${git_branches}")
|
||||
string(REPLACE " -> " ";" git_branch_list "${git_branch_list}")
|
||||
string(REPLACE "\n" ";" git_branch_list "${git_branches}")
|
||||
foreach(git_branch_candidate IN ITEMS ${git_branch_list})
|
||||
if (NOT git_branch_candidate MATCHES "/")
|
||||
if (NOT git_branch_candidate MATCHES "^HEAD$")
|
||||
set(git_branch ${git_branch_candidate})
|
||||
endif()
|
||||
string(STRIP "${git_branch_candidate}" git_branch_candidate_2)
|
||||
if (NOT git_branch_candidate_2 MATCHES "^HEAD$")
|
||||
set(git_branch ${git_branch_candidate_2})
|
||||
endif()
|
||||
endforeach()
|
||||
execute_process(
|
||||
@@ -806,11 +815,12 @@ if (GIT_FOUND)
|
||||
OUTPUT_VARIABLE git_sha
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
message(STATUS "IfcOpenShell branch ${git_branch}")
|
||||
message(STATUS "IfcOpenShell commit ${git_sha}")
|
||||
message(STATUS "IfcOpenShell branch: \"${git_branch}\"")
|
||||
message(STATUS "IfcOpenShell commit: \"${git_sha}\"")
|
||||
add_definitions(-DIFCOPENSHELL_BRANCH=${git_branch})
|
||||
add_definitions(-DIFCOPENSHELL_COMMIT=${git_sha})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Documentation
|
||||
IF(BUILD_DOCUMENTATION)
|
||||
|
||||
@@ -24,13 +24,19 @@ requirements:
|
||||
- swig >=3.0.9
|
||||
- libboost
|
||||
- icu
|
||||
- xmlschema
|
||||
- lxml
|
||||
- numpy
|
||||
run:
|
||||
- libgcc # [osx]
|
||||
- python
|
||||
- oce ==0.18.3
|
||||
- libboost
|
||||
- icu
|
||||
|
||||
- xmlschema
|
||||
- lxml
|
||||
- numpy
|
||||
|
||||
about:
|
||||
home: http://ifcopenshell.org
|
||||
license: LGPL
|
||||
|
||||
@@ -70,6 +70,7 @@ logger.addHandler(ch)
|
||||
|
||||
PROJECT_NAME = "IfcOpenShell"
|
||||
USE_CURRENT_PYTHON_VERSION = os.getenv("USE_CURRENT_PYTHON_VERSION")
|
||||
ADD_COMMIT_SHA = os.getenv("ADD_COMMIT_SHA")
|
||||
|
||||
PYTHON_VERSIONS = ["3.6.14", "3.7.12", "3.8.12", "3.9.7", "3.10.0"]
|
||||
JSON_VERSION = "v3.6.1"
|
||||
@@ -664,13 +665,12 @@ if "cgal" in targets:
|
||||
cecho("Building IfcOpenShell:", GREEN)
|
||||
|
||||
IFCOS_DIR = os.path.join(DEPS_DIR, "build", "ifcopenshell")
|
||||
if os.path.exists(IFCOS_DIR):
|
||||
shutil.rmtree(IFCOS_DIR)
|
||||
os.makedirs(IFCOS_DIR)
|
||||
|
||||
if os.environ.get("NO_CLEAN", "").lower() not in {"1", "on", "true"}:
|
||||
if os.path.exists(IFCOS_DIR):
|
||||
shutil.rmtree(IFCOS_DIR)
|
||||
os.makedirs(IFCOS_DIR, exist_ok=True)
|
||||
executables_dir = os.path.join(IFCOS_DIR, "executables")
|
||||
if not os.path.exists(executables_dir):
|
||||
os.makedirs(executables_dir)
|
||||
os.makedirs(executables_dir, exist_ok=True)
|
||||
|
||||
logger.info("\rConfiguring executables...")
|
||||
|
||||
@@ -692,6 +692,7 @@ cmake_args = [
|
||||
"-DGLTF_SUPPORT=" "ON",
|
||||
"-DJSON_INCLUDE_DIR=" f"{DEPS_DIR}/install/json",
|
||||
"-DBoost_NO_BOOST_CMAKE=" "On",
|
||||
"-DADD_COMMIT_SHA=" +("On" if ADD_COMMIT_SHA else "Off")
|
||||
]
|
||||
|
||||
if "cgal" in targets:
|
||||
@@ -782,6 +783,7 @@ if "IfcOpenShell-Python" in targets:
|
||||
"-DPYTHON_INCLUDE_DIR=" +python_include,
|
||||
"-DSWIG_EXECUTABLE=" f"{DEPS_DIR}/install/swig/bin/swig",
|
||||
"-DCMAKE_INSTALL_PREFIX=" f"{DEPS_DIR}/install/ifcopenshell/tmp",
|
||||
"-DUSERSPACE_PYTHON_PREFIX=" +["Off", "On"][os.environ.get("PYTHON_USER_SITE", "").lower() in {"1", "on", "true"}]
|
||||
], cmake_dir=CMAKE_DIR, cwd=python_dir)
|
||||
|
||||
logger.info(f"\rBuilding python {python_version} wrapper... ")
|
||||
@@ -799,7 +801,12 @@ if "IfcOpenShell-Python" in targets:
|
||||
|
||||
if USE_CURRENT_PYTHON_VERSION:
|
||||
python_info = sysconfig.get_paths()
|
||||
compile_python_wrapper(platform.python_version(), python_info["purelib"], python_info["include"], sys.executable)
|
||||
python_lib = os.path.join(
|
||||
sysconfig.get_config_var('LIBDIR'),
|
||||
sysconfig.get_config_var('multiarchsubdir').replace("/", ""),
|
||||
sysconfig.get_config_var("INSTSONAME")
|
||||
)
|
||||
compile_python_wrapper(platform.python_version(), python_lib, python_info["include"], sys.executable)
|
||||
else:
|
||||
for python_version in PYTHON_VERSIONS:
|
||||
python_library = run([bash, "-c", f"ls {DEPS_DIR}/install/python-{python_version}/lib/libpython*.*"])
|
||||
|
||||
@@ -46,7 +46,7 @@ class RelatedTopic:
|
||||
|
||||
class HeaderFile:
|
||||
def __init__(self):
|
||||
self.file_name = ""
|
||||
self.filename = ""
|
||||
self.date = None
|
||||
self.reference = ""
|
||||
self.ifc_project = None
|
||||
|
||||
@@ -27,6 +27,10 @@ ifeq ($(PYVERSION), py39)
|
||||
PYLIBDIR:=python3.9
|
||||
PYNUMBER:=39
|
||||
endif
|
||||
ifeq ($(PYVERSION), py310)
|
||||
PYLIBDIR:=python3.10
|
||||
PYNUMBER:=31
|
||||
endif
|
||||
|
||||
ifeq ($(PLATFORM), linux)
|
||||
ifeq ($(PYVERSION), py37)
|
||||
@@ -41,6 +45,12 @@ EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/linux-64/ei
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/linux-64/boost-1.74.0-py39h5472131_3.tar.bz2
|
||||
LXML_URL:=https://files.pythonhosted.org/packages/19/d9/a69c6aff5673554df48120565a14a50eaa41d29ae03b02faa0b023666318/lxml-4.6.3-cp39-cp39-manylinux2014_x86_64.whl
|
||||
endif
|
||||
ifeq ($(PYVERSION), py310)
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.8.0/download/linux-64/hpp-fcl-1.8.0-py310hdaf7e41_1.tar.bz2
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.11/download/linux-64/eigenpy-2.6.11-py310hf3e5c9c_0.tar.bz2
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/linux-64/boost-1.74.0-py310h7c3ba0c_5.tar.bz2
|
||||
LXML_URL:=https://files.pythonhosted.org/packages/25/1e/19b46d8e8881fe0df2e20945d51919eeb1817836d62a90efa8506530e45c/lxml-4.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl
|
||||
endif
|
||||
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/linux-64/assimp-5.0.1-hedfc422_6.tar.bz2
|
||||
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.7/download/linux-64/octomap-1.9.7-h4bd325d_0.tar.bz2
|
||||
ZLIB_URL:=https://anaconda.org/conda-forge/zlib/1.2.11/download/linux-64/zlib-1.2.11-h516909a_1010.tar.bz2
|
||||
@@ -59,6 +69,12 @@ EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/osx-64/eige
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/osx-64/boost-1.74.0-py39ha641261_3.tar.bz2
|
||||
LXML_URL:=https://files.pythonhosted.org/packages/b8/74/a71f7ad72e8db54ce899efab84507b801660750cbbfa6a39e6717557d36a/lxml-4.6.3-cp39-cp39-macosx_10_9_x86_64.whl
|
||||
endif
|
||||
ifeq ($(PYVERSION), py310)
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.8.0/download/osx-64/hpp-fcl-1.8.0-py310h651ac30_1.tar.bz2
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.11/download/osx-64/eigenpy-2.6.11-py310hc03097c_0.tar.bz2
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/osx-64/boost-1.74.0-py310h509978a_5.tar.bz2
|
||||
LXML_URL:=https://files.pythonhosted.org/packages/a1/44/17b7dac7a18807d30e2fe10c3328c152808f5464565e230bfd0e77f178c6/lxml-4.8.0-cp310-cp310-macosx_10_15_x86_64.whl
|
||||
endif
|
||||
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/osx-64/assimp-5.0.1-h1224e73_6.tar.bz2
|
||||
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.7/download/osx-64/octomap-1.9.7-h940c156_0.tar.bz2
|
||||
ZLIB_URL:=https://anaconda.org/conda-forge/zlib/1.2.11/download/osx-64/zlib-1.2.11-h7795811_1010.tar.bz2
|
||||
@@ -77,6 +93,12 @@ EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/win-64/eige
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/win-64/boost-1.74.0-py39hefe7e4c_3.tar.bz2
|
||||
LXML_URL:=https://files.pythonhosted.org/packages/72/d4/426ecb8849c47c3e370c87aa0ac05d85768df917ffea27fcd6686a5e6495/lxml-4.6.3-cp39-cp39-win_amd64.whl
|
||||
endif
|
||||
ifeq ($(PYVERSION), py310)
|
||||
HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.8.0/download/win-64/hpp-fcl-1.8.0-py310hc5a3c62_1.tar.bz2
|
||||
EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.11/download/win-64/eigenpy-2.6.11-py310hbd43d28_0.tar.bz2
|
||||
BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/win-64/boost-1.74.0-py310hc781a3c_5.tar.bz2
|
||||
LXML_URL:=https://files.pythonhosted.org/packages/f6/71/65c80a4caa1617a4c6e8fe1500cebb179db96232e2f623bfe6a1f4294e39/lxml-4.8.0-cp310-cp310-win_amd64.whl
|
||||
endif
|
||||
ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/win-64/assimp-5.0.1-hc2aa0de_6.tar.bz2
|
||||
OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.7/download/win-64/octomap-1.9.7-h5362a0b_0.tar.bz2
|
||||
ZLIB_URL:=https://anaconda.org/conda-forge/zlib/1.2.11/download/win-64/zlib-1.2.11-h62dcd97_1010.tar.bz2
|
||||
@@ -93,9 +115,9 @@ endif
|
||||
cp -r blenderbim/* dist/blenderbim/
|
||||
|
||||
# Provides IfcOpenShell Python functionality
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcblender-python-$(PYNUMBER)-v0.7.0-58ee049-$(PLATFORM)64.zip
|
||||
cd dist/working && unzip ifcblender*
|
||||
cp -r dist/working/io_import_scene_ifc/ifcopenshell dist/blenderbim/libs/site/packages/
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-2985bba-$(PLATFORM)64.zip
|
||||
cd dist/working && unzip ifcopenshell-python*
|
||||
cp -r dist/working/ifcopenshell dist/blenderbim/libs/site/packages/
|
||||
|
||||
# See bug #812
|
||||
cd dist/working && wget https://raw.githubusercontent.com/IfcOpenShell/IfcOpenShell/v0.7.0/src/blenderbim/occ_utils.py
|
||||
@@ -133,6 +155,8 @@ endif
|
||||
cp -r dist/working/IfcOpenShell-0.7.0/src/ifc4d/ifc4d dist/blenderbim/libs/site/packages/
|
||||
# Provides IFC5D functionality
|
||||
cp -r dist/working/IfcOpenShell-0.7.0/src/ifc5d/ifc5d dist/blenderbim/libs/site/packages/
|
||||
# Provides IFCCityJSON functionality
|
||||
cp -r dist/working/IfcOpenShell-0.7.0/src/ifccityjson dist/blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Provides Mustache templating in construction documentation
|
||||
@@ -255,6 +279,15 @@ endif
|
||||
cp -r dist/working/tqdm-4.50.0/tqdm dist/blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Required by IFCCityJSON
|
||||
mkdir dist/working
|
||||
cd dist/working && python -m venv env
|
||||
cd dist/working && mkdir site-packages
|
||||
cd dist/working && . env/bin/activate && pip install cjio --target=./site-packages
|
||||
cd dist/working/site-packages/ && rm -r *dist-info*
|
||||
cp -r dist/working/site-packages/* dist/blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Required by IFCClash
|
||||
mkdir dist/working
|
||||
cd dist/working && wget $(HPPFCL_URL)
|
||||
@@ -360,7 +393,7 @@ endif
|
||||
cd dist/working/ && patch ../blenderbim/libs/site/packages/behave/runner_util.py < runner_util.patch
|
||||
rm -rf dist/working
|
||||
|
||||
# Required by ids
|
||||
# Required by ids - uh, not any more, but I'm trying it out in the drawing module
|
||||
mkdir dist/working
|
||||
cd dist/working && wget $(LXML_URL)
|
||||
cd dist/working && cp *.whl lxml.zip && unzip lxml.zip
|
||||
@@ -444,6 +477,47 @@ endif
|
||||
cd dist/working/olca-ipc-0.0.10/ && cp -r olca ../../blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Provides Brickschema fucntionality
|
||||
mkdir dist/working
|
||||
cd dist/working && wget https://files.pythonhosted.org/packages/ca/37/8309da9a72407d2f5eb5489c197ac3cfe96ee3ccba1902d2a0ecc7843f92/brickschema-0.5.1.tar.gz
|
||||
cd dist/working && tar -xzvf brickschema*
|
||||
# This is an evil hack because we don't want to bundle flask
|
||||
echo "" > dist/working/brickschema-0.5.1/brickschema/web.py
|
||||
cd dist/working/brickschema-0.5.1/ && cp -r brickschema ../../blenderbim/libs/site/packages/
|
||||
# For now lets bundle the latest nightly schema
|
||||
cd dist/working && wget https://github.com/BrickSchema/Brick/releases/download/nightly/Brick.ttl
|
||||
cd dist/working && cp Brick.ttl ../../blenderbim/bim/schema/Brick.ttl
|
||||
rm -rf dist/working
|
||||
|
||||
# Required by brickschema
|
||||
mkdir dist/working
|
||||
cd dist/working && wget https://files.pythonhosted.org/packages/c7/22/37bb938be8e5c20d443b0c9ba0a243573b671b01739efb0a19f81bc5b470/pyshacl-0.17.2.tar.gz
|
||||
cd dist/working && tar -xzvf pyshacl*
|
||||
cd dist/working/pyshacl-0.17.2/ && cp -r pyshacl ../../blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Required by brickschema
|
||||
mkdir dist/working
|
||||
cd dist/working && wget https://files.pythonhosted.org/packages/d2/a7/be8244688bfcee37c23733ab4fe8e6afa6d4403bd2674a3ae7bd2cecc77b/rdflib-6.0.2.tar.gz
|
||||
cd dist/working && tar -xzvf rdflib*
|
||||
cd dist/working/rdflib-6.0.2/ && cp -r rdflib ../../blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Required by brickschema
|
||||
mkdir dist/working
|
||||
cd dist/working && wget https://files.pythonhosted.org/packages/7d/c7/208aece36279e4f1236e437119358786e39530ecc1719d4e1afeddba5288/owlrl-6.0.2.tar.gz
|
||||
cd dist/working && tar -xzvf owlrl*
|
||||
cd dist/working/owlrl-6.0.2/ && cp -r owlrl ../../blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Required by brickschema
|
||||
# This is a bit of a dodgy one, it should be provided by setuptools which Blender doesn't ship.
|
||||
mkdir dist/working
|
||||
cd dist/working && wget https://blenderbim.org/builds/pkg_resources.tar.gz
|
||||
cd dist/working && tar -xzvf pkg_resources*
|
||||
cd dist/working/ && cp -r pkg_resources ../blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
cd dist/blenderbim && sed -i "s/999999/$(VERSION)/" __init__.py
|
||||
cd dist && zip -r blenderbim-$(VERSION)-$(PYVERSION)-$(PLATFORM).zip ./*
|
||||
rm -rf dist/blenderbim
|
||||
@@ -479,7 +553,7 @@ coverage:
|
||||
|
||||
.PHONY: license
|
||||
license:
|
||||
copyright-header --license GPL3 --copyright-holder "Dion Moult <dion@thinkmoult.com>" --copyright-year "2021" --copyright-software "BlenderBIM Add-on" --copyright-software-description "OpenBIM Blender Add-on" -a ./ -o ./
|
||||
copyright-header --license GPL3 --copyright-holder "Dion Moult <dion@thinkmoult.com>" --copyright-year "2022" --copyright-software "BlenderBIM Add-on" --copyright-software-description "OpenBIM Blender Add-on" -a ./ -o ./
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
|
||||
@@ -1,127 +1,5 @@
|
||||
# Blender BIM IFC
|
||||
# BlenderBIM Add-on
|
||||
|
||||
_Under heavy development!_
|
||||
An add-on to Blender to allow BIM functionality.
|
||||
|
||||
Let's use Blender to create BIM models, because why not?
|
||||
|
||||
## Features
|
||||
|
||||
- IFC4 is a first-class citizen. IFC4 has been an ISO standard since 2013, with
|
||||
the latest published in 2018. It contains many more features than IFC2X3,
|
||||
published back in 2005.
|
||||
- Available for Windows, Mac, and Linux!
|
||||
- Use open-source Blender 2.8 to do digital modeling! Used by NASA, Hollywood,
|
||||
and Ubisoft, Blender allows real-time modeling in rendered view, animations,
|
||||
file linking, proxy objects, Blender Cloud collaboration, lighting analysis,
|
||||
CFD, energy modeling, and a huge ecosystem of add-ons with a thriving
|
||||
community. No licensing, and with a strong Python API to integrate into your
|
||||
custom workflow.
|
||||
- Create complex spatial structures with multiple sites, buildings, facilities,
|
||||
bridges, and building parts. Sites can be broken down into sub-sites and
|
||||
multiple buildings per site. You can categorise different floors to different
|
||||
buildings.
|
||||
- Classify any geometry as any IFC type. Just because it is a wall doesn't
|
||||
mean you should be limited to modeling it in a certain way. Easily change
|
||||
from one type to another, switch relationships and aggregations, reuse or
|
||||
even remove geometry altogether, without damaging your geometry or breaking
|
||||
your BIM model.
|
||||
- Use collections to create element compositions and decompositions with full
|
||||
control over the classifications of their parts. Most BIM programs don't give
|
||||
this flexibility. Create collection instances to efficiently duplicate
|
||||
composite objects.
|
||||
- Allow BIM objects to instance a shared geometric representations. This allows
|
||||
you to place the same shape in multiple objects without drastically
|
||||
increasing your filesize.
|
||||
- Provide a custom value to any simple IFC attribute of an IFC product.
|
||||
- Create product types that regular IFC products can inherit attributes from.
|
||||
- Define your own property sets with control over exactly how the data in the
|
||||
property sets are stored using property set template definitions which you
|
||||
can download or create yourself. Property sets can apply to both types and
|
||||
objects, and object property sets can selectively override type property
|
||||
sets.
|
||||
- Applies array modifiers to create instances.
|
||||
- Basic quantity take-off support for costing.
|
||||
- Settings to toggle export of quantities or geometric representations.
|
||||
Exporting IFC data without geometric representations is much faster and a
|
||||
fraction of the filesize.
|
||||
- Maintain `GlobalId` between exports, so that you can manipulate the IFC
|
||||
reliably in other software while making changes.
|
||||
- Not all objects need full solid geometry. Create representations as 2D or 3D
|
||||
curves / wireframes for rapid and lightweight BIM development.
|
||||
- Create map conversions and coordinate reference system definitions to link
|
||||
BIM to GIS.
|
||||
- Export basic materials with surface and diffuse colours for representation.
|
||||
You can also link materials to an externally defined file, such as a `.blend`
|
||||
Cycles material, V-Ray material, or even Radiance material for lighting
|
||||
simulation.
|
||||
- Switch between different types of metric units when modeling.
|
||||
- Model multiple representations for different purposes for a single BIM
|
||||
object or type, such as footprints, axis, reference, or clearance geometry.
|
||||
You can export just the type of representation required for your usecase, or
|
||||
export multiple for the user to choose from.
|
||||
- Record qualitative objectives such as design intentions, health and safety
|
||||
strategies, or code compliance, performance solutions, and more, in the BIM
|
||||
model and directly associate them to objects or types.
|
||||
- Classify objects and types to different classification systems such as
|
||||
Uniclass, Omniclass, or your custom system.
|
||||
- Associate external document files such as plans, brochures, specifications,
|
||||
warranties, or anything to objects and types.
|
||||
- Specify swept solid geometries, allowing you to round-trip geometry
|
||||
manipulation with other BIM software which have more limited geometric tools,
|
||||
like Revit.
|
||||
- Create material layer sets, for objects which have materials in predefined
|
||||
layers with thicknesses.
|
||||
- Create material constituent sets, for objects which are made up of
|
||||
arbitrarily mixed ingredients.
|
||||
- Specify nested and hosted element relationships.
|
||||
- Specify predefined door attributes as defined in the IFC4 specification.
|
||||
- Specify predefined window attributes as defined in the IFC4 specification.
|
||||
- Specify surface styles either defined as part of a material or assigned
|
||||
directly to an object's representation.
|
||||
- Colour coding by IFC class to visually spot mistakes
|
||||
|
||||
Some more experimental features:
|
||||
|
||||
- Cut sections for documentation with OpenCascade
|
||||
- Export gbXML files that are linked to your IFC file to do energy analysis
|
||||
- QA module to generate Gherkin unit tests to validate your BIM models
|
||||
- HTML formatter to generate reports from QA results
|
||||
- COBie spreadsheet extraction to import into CAFM systems
|
||||
|
||||
## Demo
|
||||
|
||||
[See demo video](https://www.youtube.com/watch?v=iD3v3eu2AjY)
|
||||
|
||||
It's only just started, so everything can and will change. But if you want to
|
||||
check it out:
|
||||
|
||||
1. Install [Blender](https://www.blender.org/) 64-bit version.
|
||||
2. Download the latest packaged version of Blender BIM from
|
||||
[here](https://blenderbim.org/builds/).
|
||||
3. Launch Blender. Go to `Edit->Preferences->Add-ons->Install...`. Browse to
|
||||
the downloaded `.zip` file from step 2.
|
||||
4. Enable the checkbox next to the Import-Export IFC Blender plugin which will
|
||||
show up.
|
||||
5. All done! You will find an import and export IFC option in the `File` menu.
|
||||
|
||||
By default, Blender BIM will export whatever you have selected. The objects you
|
||||
have selected _must_ belong in an `IfcProject` and spatially contained, such as
|
||||
witihn an `IfcBuilding`. It must also follow the naming convention. Please see
|
||||
the screenshot below for an example scene.
|
||||
|
||||
A naming convention of `IfcClass/IfcName` is enforced. If you name your object
|
||||
`IfcWall/Foo`, it will turn into an `IfcWall` with the name attribute set to
|
||||
`Foo`. You can override object attributes using Blender's `Custom Properties`.
|
||||
Your objects must live in a top level `Collection` defined as a context class,
|
||||
usually something like `IfcProject/Foo`. You can then nest spatial structures
|
||||
underneath to create a hierarchy.
|
||||
|
||||
## Pictures
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
... this Blender files creates this IFC file ...
|
||||
|
||||

|
||||
More information on the [BlenderBIM Add-on website](https://blenderbim.org).
|
||||
|
||||
|
Before Width: | Height: | Size: 399 KiB |
@@ -43,6 +43,7 @@ modules = {
|
||||
"sequence": None,
|
||||
"group": None,
|
||||
"system": None,
|
||||
"brick": None,
|
||||
"structural": None,
|
||||
"boundary": None,
|
||||
"profile": None,
|
||||
@@ -53,6 +54,7 @@ modules = {
|
||||
"pset": None,
|
||||
"qto": None,
|
||||
"classification": None,
|
||||
"library": None,
|
||||
"constraint": None,
|
||||
"document": None,
|
||||
"pset_template": None,
|
||||
@@ -62,9 +64,13 @@ modules = {
|
||||
"bimtester": None,
|
||||
"diff": None,
|
||||
"patch": None,
|
||||
"gis": None,
|
||||
"covetool": None,
|
||||
"augin": None,
|
||||
"debug": None,
|
||||
# Uncomment this line to enable loading of the demo module. Happy hacking!
|
||||
# The name "demo" must correlate to a folder name in `bim/module/`.
|
||||
# "demo": None,
|
||||
}
|
||||
|
||||
|
||||
@@ -73,17 +79,18 @@ for name in modules.keys():
|
||||
|
||||
|
||||
classes = [
|
||||
operator.OpenUri,
|
||||
operator.SelectDataDir,
|
||||
operator.SelectSchemaDir,
|
||||
operator.SelectIfcFile,
|
||||
operator.OpenUpstream,
|
||||
operator.AddSectionPlane,
|
||||
operator.RemoveSectionPlane,
|
||||
operator.ReloadIfcFile,
|
||||
operator.AddIfcFile,
|
||||
operator.RemoveIfcFile,
|
||||
operator.BIM_OT_add_section_plane,
|
||||
operator.BIM_OT_remove_section_plane,
|
||||
operator.ConfigureVisibility,
|
||||
operator.OpenUpstream,
|
||||
operator.OpenUri,
|
||||
operator.ReloadIfcFile,
|
||||
operator.RemoveIfcFile,
|
||||
operator.SelectDataDir,
|
||||
operator.SelectIfcFile,
|
||||
operator.SelectSchemaDir,
|
||||
operator.SelectURIAttribute,
|
||||
prop.StrProperty,
|
||||
prop.ObjProperty,
|
||||
prop.Attribute,
|
||||
@@ -99,6 +106,22 @@ classes = [
|
||||
ui.BIM_UL_generic,
|
||||
ui.BIM_UL_topics,
|
||||
ui.BIM_ADDON_preferences,
|
||||
# Scene panel groups
|
||||
ui.BIM_PT_project_info,
|
||||
ui.BIM_PT_project_setup,
|
||||
ui.BIM_PT_collaboration,
|
||||
ui.BIM_PT_geometry,
|
||||
ui.BIM_PT_services,
|
||||
ui.BIM_PT_structural,
|
||||
ui.BIM_PT_4D5D,
|
||||
ui.BIM_PT_quality_control,
|
||||
ui.BIM_PT_integrations,
|
||||
# Object panel groups
|
||||
ui.BIM_PT_object_metadata,
|
||||
ui.BIM_PT_geometry_object,
|
||||
ui.BIM_PT_services_object,
|
||||
ui.BIM_PT_utilities_object,
|
||||
ui.BIM_PT_misc_object,
|
||||
]
|
||||
|
||||
for mod in modules.values():
|
||||
|
||||
@@ -1,10 +1,44 @@
|
||||
<link href="jsgantt.css" rel="stylesheet" type="text/css"/>
|
||||
<script src="jsgantt.js" type="text/javascript"></script>
|
||||
<style>
|
||||
.gtaskcellwkend,
|
||||
.gtaskcellcurrent,
|
||||
.gminorheadingwkend {
|
||||
background-color: #e1e1e1;
|
||||
}
|
||||
|
||||
.gitemhighlight td {
|
||||
background-color: #ffdaaa;
|
||||
}
|
||||
|
||||
.gtaskblue {
|
||||
background: #4281A4;
|
||||
}
|
||||
|
||||
.gtaskred {
|
||||
background: #C1666B;
|
||||
}
|
||||
|
||||
.gtaskgreen {
|
||||
background: #48A9A6;
|
||||
}
|
||||
|
||||
.gtaskyellow {
|
||||
background: #D4B483;
|
||||
}
|
||||
|
||||
.gmainleft {
|
||||
overflow: visible;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
</style>
|
||||
<a href="#print" id="print">Print Mode</a>
|
||||
<div style="position:relative" class="gantt" id="GanttChartDIV"></div>
|
||||
<script type="text/javascript">
|
||||
var g = new JSGantt.GanttChart(document.getElementById('GanttChartDIV'), 'day');
|
||||
g.setOptions({
|
||||
vCaptionType: 'Complete', // Set to Show Caption : None,Caption,Resource,Duration,Complete,
|
||||
vCaptionType: 'Caption', // Set to Show Caption : None,Caption,Resource,Duration,Complete,
|
||||
vQuarterColWidth: 36,
|
||||
vDateTaskDisplayFormat: 'day dd month yyyy', // Shown in tool tip box
|
||||
vDayMajorDateDisplayFormat: 'mon yyyy - Week ww',// Set format to dates in the "Major" header of the "Day" view
|
||||
@@ -14,7 +48,16 @@ g.setOptions({
|
||||
vShowEndWeekDate: 0, // Show/Hide the date for the last day of the week in header for daily
|
||||
vUseSingleCell: 10000, // Set the threshold cell per table row (Helps performance for large data.
|
||||
vFormatArr: ['Day', 'Week', 'Month', 'Quarter'], // Even with setUseSingleCell using Hour format on such a large chart can cause issues in some browsers,
|
||||
vTotalHeight: 1000,
|
||||
vShowRes: false, // Disable the resource column.
|
||||
vShowComp: false, // Disable the completion column.
|
||||
vShowDur: false, // Disable the duration column, because jsgantt doesn't calculate durations the way we want.
|
||||
vAdditionalHeaders: {ifcduration: {title: 'Duration'}},
|
||||
vUseToolTip: false, // Disable tooltips.
|
||||
vTotalHeight: 900,
|
||||
});
|
||||
document.getElementById('print').addEventListener('click', function() {
|
||||
g.setTotalHeight("");
|
||||
g.Draw();
|
||||
});
|
||||
var json_data = `
|
||||
{{{json_data}}}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION((),'2;1');
|
||||
FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',(),(),'EPset_Drawing','EPset_Drawing',$);
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4,#5,#6,#7));
|
||||
#2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#5=IFCSIMPLEPROPERTYTEMPLATE('0AK5C2UpL4$eaac2LszAx$',$,'HasUnderlay','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#6=IFCSIMPLEPROPERTYTEMPLATE('2j2ZEZR8X5tONm7kli5hM6',$,'HasLinework','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#7=IFCSIMPLEPROPERTYTEMPLATE('1ttChRysH9UuEX2FeMj5Hu',$,'HasAnnotation','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
@@ -19,14 +19,26 @@
|
||||
*/
|
||||
|
||||
* { stroke-linecap: round; stroke-linejoin: round; }
|
||||
*[id] { fill: black; stroke: black; stroke-linecap: 'round'; stroke-width: 0.35; }
|
||||
text { fill: black; stroke: none; }
|
||||
.cut { fill: black; stroke: black; stroke-linecap: 'round'; stroke-width: 0.35; fill-rule: evenodd; }
|
||||
.projection { fill: white; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
|
||||
.annotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
|
||||
.hidden { stroke-dasharray: 3, 2; }
|
||||
.solid {}
|
||||
.leader { marker-end: url(#leader-marker); }
|
||||
.stair { marker-start: url(#stair-marker-start); marker-end: url(#stair-marker-end); }
|
||||
.IfcAnnotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
|
||||
.PredefinedType-MISC { stroke: black; stroke-width: 0.25; }
|
||||
.PredefinedType-BACKGROUND { stroke: black; stroke-width: 0.18; }
|
||||
.PredefinedType-GRID { marker-start: url(#grid-marker); marker-end: url(#grid-marker); }
|
||||
.PredefinedType-SECTIONLEVEL { marker-start: url(#section-level-marker); stroke-dasharray: 12.5, 3, 3, 3; }
|
||||
.PredefinedType-PLANLEVEL { marker-end: url(#plan-level-marker); }
|
||||
.PredefinedType-DIMENSION { marker-start: url(#dimension-marker-start); marker-end: url(#dimension-marker-end); }
|
||||
.PredefinedType-HIDDENLINE { stroke-dasharray: 3, 2; }
|
||||
.PredefinedType-STAIR { marker-start: url(#stair-marker-start); marker-end: url(#stair-marker-end); }
|
||||
.PredefinedType-BOUNDARY { fill: none; stroke: red; stroke-width: 1; stroke-dasharray: 12, 4, 3, 4, 3, 4; }
|
||||
.PredefinedType-BATTING { marker-start: url(#batting); stroke: none; }
|
||||
.PredefinedType-SEALANT { fill: url(#crosshatch1); stroke-width: 0.25; }
|
||||
.PredefinedType-TEXT { fill: black; stroke: none; }
|
||||
path.PredefinedType-TEXTLEADER { marker-end: url(#leader-marker); }
|
||||
text.PredefinedType-TEXTLEADER { fill: black; stroke: none; }
|
||||
.break { fill: white; }
|
||||
.breakline { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; marker-mid: url(#breakline-marker); }
|
||||
.material-blank { fill: white; }
|
||||
@@ -44,5 +56,9 @@ text { fill: black; stroke: none; }
|
||||
.material-honeycomb { fill: url(#honeycomb); }
|
||||
.material-sand { fill: url(#sand); }
|
||||
.material-concrete { fill: url(#concrete); stroke-width: 0.5; }
|
||||
.material-boundary { fill: none; stroke: red; stroke-width: 1; stroke-dasharray: 12,4,3,4,3,4; }
|
||||
.IfcSpace { fill: none; stroke: none; }
|
||||
.PredefinedType-STUD { stroke: black; stroke-width: 1; }
|
||||
.PredefinedType-WOOD { fill: url(#wood); stroke: black; stroke-width: 0.5; }
|
||||
.PredefinedType-STEEL { fill: url(#steel); stroke: black; stroke-width: 0.5; }
|
||||
.PredefinedType-CONCRETE { fill: url(#concrete); stroke: black; stroke-width: 0.5; }
|
||||
.PredefinedType-PLASTERBOARD { fill: url(#sand); stroke: black; stroke-width: 0.25; }
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<svg baseProfile="full" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<marker id="batting" markerHeight="10" markerWidth="4" orient="auto" refX="0" refY="5" markerUnits="userSpaceOnUse">
|
||||
<!-- Tip: https://svg-path-visualizer.netlify.app/ -->
|
||||
<path style="fill: none; stroke:black; stroke-width:0.18" d="M 0 2 A 2 2 0 0 1 4 2 L 2 8 M 0 2 L 2 8 A 2 2 0 0 0 4 10 M 2 8 A 2 2 0 0 1 0 10" />
|
||||
</marker>
|
||||
<marker id="eng-dimension-marker-start" markerHeight="14" markerWidth="11" orient="auto" refX="1" refY="7">
|
||||
<g>
|
||||
<path d="M 11 3.5 L 11 10.5 L 1 7" class="annotation" style="fill:black;" />
|
||||
|
||||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.9 KiB |
@@ -3,6 +3,10 @@
|
||||
<pattern id="demolish" width="1" height="1" patternTransform="rotate(45 0 0)" patternUnits="userSpaceOnUse">
|
||||
<line x1="0" y1="0" x2="0" y2="1" style="stroke:red; stroke-width:0.25" />
|
||||
</pattern>
|
||||
<pattern id="steel" width="3" height="1" patternTransform="rotate(45 0 0)" patternUnits="userSpaceOnUse">
|
||||
<line x1="0" y1="0" x2="0" y2="1" style="stroke:black; stroke-width:0.25" />
|
||||
<line x1="1" y1="0" x2="1" y2="1" style="stroke:black; stroke-width:0.25" />
|
||||
</pattern>
|
||||
<pattern id="diagonal1" width="1" height="1" patternTransform="rotate(45 0 0)" patternUnits="userSpaceOnUse">
|
||||
<line x1="0" y1="0" x2="0" y2="1" style="stroke:black; stroke-width:0.25" />
|
||||
</pattern>
|
||||
@@ -80,6 +84,9 @@
|
||||
<path d="M 1.25,2.165 L 2.5,4.33 L 5,4.33 L 6.25,2.165 L 5,0 L 2.5,0 L 1.25,2.165" style="stroke:black;stroke-width:0.25;stroke-linecap:round;fill:none" />
|
||||
<path d="M 6.25,2.165 L 7.5,2.165" style="stroke:black;stroke-width:0.25;stroke-linecap:round" />
|
||||
</pattern>
|
||||
<pattern id="wood" width="50" height="50" patternUnits="userSpaceOnUse">
|
||||
<path style="fill:none; stroke: black; stroke-width:0.25;" d="M 49.649999,1.3460737 A 69.019995,69.019995 0 0 0 48.653938,0.34999949 M 49.649999,4.9389583 A 66.554994,66.554994 0 0 0 45.06105,0.34999949 M 49.649999,8.6983527 A 64.089999,64.089999 0 0 0 41.301656,0.34999949 M 49.649999,12.675003 A 61.624998,61.624998 0 0 0 37.325,0.34999949 M 49.649999,16.948067 A 59.159997,59.159997 0 0 0 33.05192,0.34999949 M 49.649999,21.652972 A 56.694997,56.694997 0 0 0 28.347017,0.34999949 M 49.649999,27.057906 A 54.229999,54.229999 0 0 0 22.942096,0.34999949 M 49.649999,33.866298 A 51.764998,51.764998 0 0 0 16.1337,0.34999949 M 49.649999,49.649989 A 49.299997,49.299997 0 0 0 0.34999949,0.34999949 M 47.185,49.649989 A 46.834999,46.834999 0 0 0 0.34999949,2.8149951 M 44.72,49.649989 A 44.369998,44.369998 0 0 0 0.34999949,5.2799906 M 42.254999,49.649989 A 41.904997,41.904997 0 0 0 0.34999949,7.7450123 M 39.790001,49.649989 A 39.439999,39.439999 0 0 0 0.34999949,10.210008 M 37.325,49.649989 A 36.974998,36.974998 0 0 0 0.34999949,12.675003 M 34.859999,49.649989 A 34.509998,34.509998 0 0 0 0.34999949,15.139998 M 32.395001,49.649989 A 32.044999,32.044999 0 0 0 0.34999949,17.604994 M 29.93,49.649989 A 29.579999,29.579999 0 0 0 0.34999949,20.069989 M 27.465,49.649989 A 27.114998,27.114998 0 0 0 0.34999949,22.535011 M 24.999997,49.649989 A 24.649999,24.649999 0 0 0 0.34999949,25.000007 M 22.534998,49.649989 A 22.184999,22.184999 0 0 0 0.34999949,27.465003 M 20.069998,49.649989 A 19.719999,19.719999 0 0 0 0.34999949,29.929999 M 17.604998,49.649989 A 17.254999,17.254999 0 0 0 0.34999949,32.394994 M 15.139998,49.649989 A 14.789999,14.789999 0 0 0 0.34999949,34.85999 M 12.674999,49.649989 A 12.325,12.325 0 0 0 0.34999949,37.325011 M 10.209999,49.649989 A 9.8599994,9.8599994 0 0 0 0.34999949,39.790007 M 7.7449994,49.649989 A 7.3949999,7.3949999 0 0 0 0.34999949,42.255002 M 5.2799992,49.649989 A 4.9299997,4.9299997 0 0 0 0.34999949,44.719998 M 2.8149995,49.649989 A 2.4649999,2.4649999 0 0 0 0.34999949,47.184993 M 0.34999949,49.649989 H 3.8010003 M 0.34999949,49.649989 H 3.8010003 M 0.34999949,49.649989 H 3.8010003" />
|
||||
</pattern>
|
||||
<pattern id="sand" width="50" height="50" patternUnits="userSpaceOnUse">
|
||||
<path style="fill: white;" d="M 0 0 50 0 50 50 0 50" />
|
||||
<path
|
||||
|
||||
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 67 KiB |
@@ -5,6 +5,20 @@
|
||||
</g>
|
||||
<g id="door-tag">
|
||||
<circle cy="1.75" r="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
|
||||
<line x1="-5" y1="1.75" x2="5" y2="1.75" style="stroke:black; stroke-width: 0.25;" />
|
||||
<line x1="-5" y1="1.75" x2="5" y2="1.75" style="stroke: black; stroke-width: 0.25;" />
|
||||
</g>
|
||||
<g id="elevation-arrow">
|
||||
<path style="fill: black;" d="M -7.07106 0 0 -7.07106 7.07106 0" />
|
||||
</g>
|
||||
<g id="elevation-tag">
|
||||
<circle r="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
|
||||
<line x1="-5" y1="0" x2="5" y2="0" style="stroke: black; stroke-width: 0.25;" />
|
||||
</g>
|
||||
<g id="section-arrow">
|
||||
<path style="fill: black;" d="M -7.07106 0 0 -7.07106 7.07106 0" />
|
||||
</g>
|
||||
<g id="section-tag">
|
||||
<circle r="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
|
||||
<line x1="-5" y1="0" x2="5" y2="0" style="stroke: black; stroke-width: 0.25;" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 572 B After Width: | Height: | Size: 1.2 KiB |
@@ -173,7 +173,7 @@
|
||||
style="stroke-width:0.999999px"
|
||||
y="1036.1166"
|
||||
x="3030.9653"
|
||||
id="tspan2527">{{revision}}</tspan></text>
|
||||
id="tspan2527">{{Revision}}</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.3918px;line-height:125%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.999999px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
@@ -183,7 +183,7 @@
|
||||
id="tspan2531"
|
||||
x="2804.7021"
|
||||
y="1036.1166"
|
||||
style="stroke-width:0.999999px">{{number}}</tspan></text>
|
||||
style="stroke-width:0.999999px">{{Identification}} - {{Name}}</tspan></text>
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(0.26458333,0,0,0.26458334,-3.180917e-5,297.00003)"
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -169,7 +169,7 @@
|
||||
style="stroke-width:0.264583px"
|
||||
y="397.13919"
|
||||
x="554.94287"
|
||||
id="tspan2527">{{revision}}</tspan></text>
|
||||
id="tspan2527">{{Revision}}</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.54325px;line-height:125%;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
@@ -179,7 +179,7 @@
|
||||
id="tspan2531"
|
||||
x="495.07742"
|
||||
y="397.13919"
|
||||
style="stroke-width:0.264583px">{{number}}</tspan></text>
|
||||
style="stroke-width:0.264583px">{{Identification}} - {{Name}}</tspan></text>
|
||||
<g
|
||||
style="stroke-width:3.77953"
|
||||
id="g90669"
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -172,7 +172,7 @@
|
||||
id="tspan2527"
|
||||
x="544.94287"
|
||||
y="284.13919"
|
||||
style="stroke-width:0.264583px">{{revision}}</tspan></text>
|
||||
style="stroke-width:0.264583px">{{Revision}}</tspan></text>
|
||||
<text
|
||||
id="text2533"
|
||||
y="284.13919"
|
||||
@@ -182,7 +182,7 @@
|
||||
style="stroke-width:0.264583px"
|
||||
y="284.13919"
|
||||
x="485.07742"
|
||||
id="tspan2531">{{number}}</tspan></text>
|
||||
id="tspan2531">{{Identification}} - {{Name}}</tspan></text>
|
||||
</g>
|
||||
<g
|
||||
style="stroke-width:3.77953"
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -70,7 +70,7 @@
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.96055365px;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:center;text-anchor:middle;stroke-width:0.26458332px"
|
||||
y="103.10933"
|
||||
x="32.822662"
|
||||
id="tspan857">{{no}}</tspan></text>
|
||||
id="tspan857">{{Identification}}</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:17.71626091px;line-height:125%;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458329px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
@@ -80,7 +80,7 @@
|
||||
id="tspan861"
|
||||
x="39.12896"
|
||||
y="100.19173"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.54325247px;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:start;text-anchor:start;stroke-width:0.26458329px">{{name}}</tspan></text>
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.54325247px;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:start;text-anchor:start;stroke-width:0.26458329px">{{Name}}</tspan></text>
|
||||
<text
|
||||
id="text867"
|
||||
y="105.0553"
|
||||
@@ -90,6 +90,6 @@
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.54325247px;font-family:'OpenGost Type B TT';-inkscape-font-specification:'OpenGost Type B TT';text-align:start;text-anchor:start;stroke-width:0.26458332px"
|
||||
y="105.0553"
|
||||
x="39.285267"
|
||||
id="tspan865">{{scale}}</tspan></text>
|
||||
id="tspan865">{{Scale}}</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
@@ -28,10 +28,12 @@ import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.placement
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.geometry
|
||||
import blenderbim.core.aggregate
|
||||
import blenderbim.core.spatial
|
||||
import blenderbim.core.style
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from mathutils import Vector
|
||||
|
||||
|
||||
class IfcExporter:
|
||||
@@ -41,20 +43,22 @@ class IfcExporter:
|
||||
def export(self):
|
||||
self.file = IfcStore.get_file()
|
||||
self.set_header()
|
||||
IfcStore.update_cache()
|
||||
if bpy.context.scene.BIMProjectProperties.is_authoring:
|
||||
self.sync_object_placements_and_deletions()
|
||||
self.sync_deletions()
|
||||
self.sync_all_objects()
|
||||
self.sync_edited_objects()
|
||||
extension = self.ifc_export_settings.output_file.split(".")[-1]
|
||||
extension = self.ifc_export_settings.output_file.split(".")[-1].lower()
|
||||
if extension == "ifczip":
|
||||
with tempfile.TemporaryDirectory() as unzipped_path:
|
||||
filename, ext = os.path.splitext(os.path.basename(self.ifc_export_settings.output_file))
|
||||
tmp_name = "{}.ifc".format(filename)
|
||||
tmp_file = os.path.join(unzipped_path, tmp_name)
|
||||
tmp_filename = f"{filename}.ifc"
|
||||
tmp_file = os.path.join(unzipped_path, tmp_filename)
|
||||
self.file.write(tmp_file)
|
||||
with zipfile.ZipFile(
|
||||
self.ifc_export_settings.output_file, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=9
|
||||
) as zf:
|
||||
zf.write(tmp_file)
|
||||
zf.write(tmp_file, arcname=tmp_filename)
|
||||
elif extension == "ifc":
|
||||
self.file.write(self.ifc_export_settings.output_file)
|
||||
elif extension == "ifcjson":
|
||||
@@ -83,27 +87,39 @@ class IfcExporter:
|
||||
self.get_application_name(), self.get_application_version()
|
||||
)
|
||||
|
||||
def sync_object_placements_and_deletions(self):
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
||||
to_delete = []
|
||||
def sync_deletions(self):
|
||||
results = []
|
||||
for ifc_definition_id in IfcStore.deleted_ids:
|
||||
try:
|
||||
product = self.file.by_id(ifc_definition_id)
|
||||
if hasattr(product, "GlobalId"):
|
||||
results.append(product.GlobalId)
|
||||
except:
|
||||
continue
|
||||
ifcopenshell.api.run("root.remove_product", self.file, **{"product": product})
|
||||
return results
|
||||
|
||||
for ifc_definition_id, obj in IfcStore.id_map.items():
|
||||
def sync_all_objects(self):
|
||||
results = []
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
||||
for ifc_definition_id in list(IfcStore.id_map.keys()):
|
||||
obj = IfcStore.id_map[ifc_definition_id]
|
||||
try:
|
||||
if isinstance(obj, bpy.types.Material):
|
||||
continue
|
||||
tool.Collector.sync(obj)
|
||||
self.sync_object_placement(obj)
|
||||
result = self.sync_object_placement(obj)
|
||||
if result:
|
||||
results.append(result)
|
||||
result = self.sync_object_material(obj)
|
||||
if result:
|
||||
results.append(result)
|
||||
except ReferenceError:
|
||||
pass # The object is likely deleted
|
||||
if self.should_delete(obj):
|
||||
to_delete.append(ifc_definition_id)
|
||||
|
||||
for ifc_definition_id in to_delete:
|
||||
product = self.file.by_id(ifc_definition_id)
|
||||
IfcStore.unlink_element(product)
|
||||
ifcopenshell.api.run("root.remove_product", self.file, **{"product": product})
|
||||
return results
|
||||
|
||||
def sync_edited_objects(self):
|
||||
results = []
|
||||
for obj in IfcStore.edited_objs.copy():
|
||||
if not obj:
|
||||
continue
|
||||
@@ -111,35 +127,51 @@ class IfcExporter:
|
||||
if isinstance(obj, bpy.types.Material):
|
||||
blenderbim.core.style.update_style_colours(tool.Ifc, tool.Style, obj=obj)
|
||||
else:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element:
|
||||
results.append(element)
|
||||
bpy.ops.bim.update_representation(obj=obj.name)
|
||||
except ReferenceError:
|
||||
pass # The object is likely deleted
|
||||
IfcStore.edited_objs.clear()
|
||||
return results
|
||||
|
||||
def sync_object_material(self, obj):
|
||||
if not obj.data or not isinstance(obj.data, bpy.types.Mesh):
|
||||
return
|
||||
if not self.has_changed_materials(obj):
|
||||
return
|
||||
bpy.ops.bim.update_representation(obj=obj.name)
|
||||
|
||||
def has_changed_materials(self, obj):
|
||||
checksum = obj.data.BIMMeshProperties.material_checksum
|
||||
return checksum != str([s.id() for s in tool.Geometry.get_styles(obj) if s])
|
||||
|
||||
def sync_object_placement(self, obj):
|
||||
if not self.has_object_moved(obj):
|
||||
return
|
||||
blender_matrix = np.array(obj.matrix_world)
|
||||
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
if (obj.scale - Vector((1.0, 1.0, 1.0))).length > 1e-4:
|
||||
bpy.ops.bim.update_representation(obj=obj.name)
|
||||
return element
|
||||
if element.is_a("IfcGridAxis"):
|
||||
return self.sync_grid_axis_object_placement(obj, element)
|
||||
if not hasattr(element, "ObjectPlacement"):
|
||||
return
|
||||
ifc_matrix = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
|
||||
ifc_matrix[0][3] *= self.unit_scale
|
||||
ifc_matrix[1][3] *= self.unit_scale
|
||||
ifc_matrix[2][3] *= self.unit_scale
|
||||
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
return element
|
||||
|
||||
props = bpy.context.scene.BIMGeoreferenceProperties
|
||||
if props.has_blender_offset and obj.BIMObjectProperties.blender_offset_type == "OBJECT_PLACEMENT":
|
||||
ifc_matrix = ifcopenshell.util.geolocation.global2local(
|
||||
ifc_matrix,
|
||||
float(props.blender_eastings) * self.unit_scale,
|
||||
float(props.blender_northings) * self.unit_scale,
|
||||
float(props.blender_orthogonal_height) * self.unit_scale,
|
||||
float(props.blender_x_axis_abscissa),
|
||||
float(props.blender_x_axis_ordinate),
|
||||
)
|
||||
if not np.allclose(ifc_matrix, blender_matrix, atol=0.0001):
|
||||
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Surveyor, obj=obj)
|
||||
def has_object_moved(self, obj):
|
||||
if not obj.BIMObjectProperties.location_checksum:
|
||||
return True # Let's be conservative
|
||||
loc_check = np.frombuffer(eval(obj.BIMObjectProperties.location_checksum))
|
||||
rot_check = np.frombuffer(eval(obj.BIMObjectProperties.rotation_checksum))
|
||||
loc_real = np.array(obj.matrix_world.translation).flatten()
|
||||
rot_real = np.array(obj.matrix_world.to_3x3()).flatten()
|
||||
if np.allclose(loc_check, loc_real, atol=1e-4) and np.allclose(rot_check, rot_real, atol=1e-2):
|
||||
return False
|
||||
return True
|
||||
|
||||
def sync_grid_axis_object_placement(self, obj, element):
|
||||
grid = (element.PartOfU or element.PartOfV or element.PartOfW)[0]
|
||||
@@ -148,15 +180,7 @@ class IfcExporter:
|
||||
self.sync_object_placement(grid_obj)
|
||||
if grid_obj.matrix_world != obj.matrix_world:
|
||||
bpy.ops.bim.update_representation(obj=obj.name)
|
||||
|
||||
def should_delete(self, obj):
|
||||
try:
|
||||
# This will throw an exception if the Blender object no longer exists
|
||||
foo = obj.name
|
||||
foo
|
||||
return False
|
||||
except:
|
||||
return True
|
||||
tool.Geometry.record_object_position(obj)
|
||||
|
||||
def get_application_name(self):
|
||||
return "BlenderBIM"
|
||||
|
||||
@@ -26,9 +26,7 @@ from blenderbim.bim.module.drawing.prop import RasterStyleProperty
|
||||
from bpy.app.handlers import persistent
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.owner.prop import get_user_person, get_user_organisation
|
||||
from ifcopenshell.api.attribute.data import Data as AttributeData
|
||||
from ifcopenshell.api.material.data import Data as MaterialData
|
||||
from ifcopenshell.api.style.data import Data as StyleData
|
||||
from ifcopenshell.api.type.data import Data as TypeData
|
||||
|
||||
|
||||
@@ -68,11 +66,10 @@ def name_callback(obj, data):
|
||||
if isinstance(obj, bpy.types.Material):
|
||||
if obj.BIMObjectProperties.ifc_definition_id:
|
||||
IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id).Name = obj.name
|
||||
AttributeData.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id)
|
||||
MaterialData.load_materials()
|
||||
if obj.BIMMaterialProperties.ifc_style_id:
|
||||
IfcStore.get_file().by_id(obj.BIMMaterialProperties.ifc_style_id).Name = obj.name
|
||||
StyleData.load(IfcStore.get_file(), obj.BIMMaterialProperties.ifc_style_id)
|
||||
refresh_ui_data()
|
||||
return
|
||||
|
||||
if not obj.BIMObjectProperties.ifc_definition_id or "/" not in obj.name:
|
||||
@@ -96,7 +93,7 @@ def name_callback(obj, data):
|
||||
if element.is_a("IfcTypeProduct"):
|
||||
TypeData.purge()
|
||||
element.Name = "/".join(obj.name.split("/")[1:])
|
||||
AttributeData.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id)
|
||||
refresh_ui_data()
|
||||
|
||||
|
||||
def color_callback(obj, data):
|
||||
@@ -160,12 +157,12 @@ def loadIfcStore(scene):
|
||||
if not IfcStore.get_file():
|
||||
return
|
||||
IfcStore.get_schema()
|
||||
IfcStore.reload_linked_elements()
|
||||
IfcStore.relink_all_objects()
|
||||
|
||||
|
||||
@persistent
|
||||
def undo_pre(scene):
|
||||
IfcStore.update_undo_redo_stack_objects()
|
||||
IfcStore.track_undo_redo_stack_object_map()
|
||||
|
||||
|
||||
@persistent
|
||||
@@ -174,13 +171,13 @@ def undo_post(scene):
|
||||
IfcStore.last_transaction = bpy.context.scene.BIMProperties.last_transaction
|
||||
IfcStore.undo()
|
||||
purge_module_data()
|
||||
IfcStore.update_undo_redo_stack_objects()
|
||||
IfcStore.reload_linked_elements(objects=[bpy.data.objects.get(o) for o in IfcStore.undo_redo_stack_objects])
|
||||
IfcStore.track_undo_redo_stack_selected_objects()
|
||||
IfcStore.reload_undo_redo_stack_objects()
|
||||
|
||||
|
||||
@persistent
|
||||
def redo_pre(scene):
|
||||
IfcStore.update_undo_redo_stack_objects()
|
||||
IfcStore.track_undo_redo_stack_object_map()
|
||||
|
||||
|
||||
@persistent
|
||||
@@ -189,8 +186,8 @@ def redo_post(scene):
|
||||
IfcStore.last_transaction = bpy.context.scene.BIMProperties.last_transaction
|
||||
IfcStore.redo()
|
||||
purge_module_data()
|
||||
IfcStore.update_undo_redo_stack_objects()
|
||||
IfcStore.reload_linked_elements(objects=[bpy.data.objects.get(o) for o in IfcStore.undo_redo_stack_objects])
|
||||
IfcStore.track_undo_redo_stack_selected_objects()
|
||||
IfcStore.reload_undo_redo_stack_objects()
|
||||
|
||||
|
||||
@persistent
|
||||
@@ -236,6 +233,7 @@ def setDefaultProperties(scene):
|
||||
)
|
||||
ifcopenshell.api.owner.settings.get_user = lambda ifc: core_owner.get_user(tool.Owner)
|
||||
ifcopenshell.api.owner.settings.get_application = get_application
|
||||
# TODO: Move to drawing module
|
||||
if len(bpy.context.scene.DocProperties.drawing_styles) == 0:
|
||||
drawing_style = bpy.context.scene.DocProperties.drawing_styles.add()
|
||||
drawing_style.name = "Technical"
|
||||
@@ -289,7 +287,7 @@ def setDefaultProperties(scene):
|
||||
RasterStyleProperty.SHADING_SHADOW_INTENSITY.value: 0.5,
|
||||
RasterStyleProperty.DISPLAY_LIGHT_DIRECTION.value: (0.5, 0.5, 0.5),
|
||||
RasterStyleProperty.VIEW_USE_CURVE_MAPPING.value: False,
|
||||
RasterStyleProperty.OVERLAY_SHOW_WIREFRAMES.value: True,
|
||||
RasterStyleProperty.OVERLAY_SHOW_WIREFRAMES.value: False,
|
||||
RasterStyleProperty.OVERLAY_WIREFRAME_THRESHOLD.value: 0,
|
||||
RasterStyleProperty.OVERLAY_SHOW_FLOOR.value: False,
|
||||
RasterStyleProperty.OVERLAY_SHOW_AXIS_X.value: False,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import bpy
|
||||
import json
|
||||
import math
|
||||
import zipfile
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.attribute
|
||||
from mathutils import geometry
|
||||
@@ -47,6 +48,9 @@ def draw_attribute(attribute, layout, copy_operator=None):
|
||||
if copy_operator:
|
||||
op = layout.operator(f"{copy_operator}", text="", icon="COPYDOWN")
|
||||
op.name = attribute.name
|
||||
if attribute.is_uri:
|
||||
op = layout.operator("bim.select_uri_attribute", text="", icon="FILE_FOLDER")
|
||||
op.data_path = attribute.path_from_id("string_value")
|
||||
|
||||
|
||||
def import_attributes(ifc_class, props, data, callback=None):
|
||||
@@ -77,6 +81,8 @@ def import_attribute(attribute, props, data, callback=None):
|
||||
props.remove(len(props) - 1)
|
||||
elif data_type == "string":
|
||||
new.string_value = "" if new.is_null else data[attribute.name()]
|
||||
if attribute.type_of_attribute().declared_type().name() == "IfcURIReference":
|
||||
new.is_uri = True
|
||||
elif data_type == "boolean":
|
||||
new.bool_value = False if new.is_null else data[attribute.name()]
|
||||
elif data_type == "integer":
|
||||
@@ -97,3 +103,45 @@ def export_attributes(props, callback=None):
|
||||
continue # Our job is done
|
||||
attributes[prop.name] = prop.get_value()
|
||||
return attributes
|
||||
|
||||
|
||||
class IfcHeaderExtractor:
|
||||
def __init__(self, filepath: str):
|
||||
self.filepath = filepath
|
||||
|
||||
def extract(self):
|
||||
extension = self.filepath.split(".")[-1]
|
||||
if extension.lower() == "ifc":
|
||||
with open(self.filepath) as ifc_file:
|
||||
return self.extract_ifc_spf(ifc_file)
|
||||
elif extension.lower() == "ifczip":
|
||||
return self.extract_ifc_zip()
|
||||
|
||||
def extract_ifc_spf(self, ifc_file):
|
||||
# https://www.steptools.com/stds/step/IS_final_p21e3.html#clause-8
|
||||
data = {}
|
||||
max_lines_to_parse = 50
|
||||
for _ in range(max_lines_to_parse):
|
||||
line = next(ifc_file)
|
||||
if isinstance(line, bytes):
|
||||
line = line.decode("utf-8")
|
||||
if line.startswith("FILE_DESCRIPTION"):
|
||||
for i, part in enumerate(line.split("'")):
|
||||
if i == 1:
|
||||
data["description"] = part
|
||||
elif i == 3:
|
||||
data["implementation_level"] = part
|
||||
elif line.startswith("FILE_NAME"):
|
||||
for i, part in enumerate(line.split("'")):
|
||||
if i == 1:
|
||||
data["name"] = part
|
||||
elif i == 3:
|
||||
data["time_stamp"] = part
|
||||
elif line.startswith("FILE_SCHEMA"):
|
||||
data["schema_name"] = line.split("'")[1]
|
||||
break
|
||||
return data
|
||||
|
||||
def extract_ifc_zip(self):
|
||||
archive = zipfile.ZipFile(self.filepath, "r")
|
||||
return self.extract_ifc_spf(archive.open(archive.filelist[0]))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
# Copyright (C) 2020, 2021, 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
@@ -16,8 +16,10 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import bpy
|
||||
import uuid
|
||||
import hashlib
|
||||
import zipfile
|
||||
import tempfile
|
||||
import ifcopenshell
|
||||
@@ -29,8 +31,11 @@ class IfcStore:
|
||||
path = ""
|
||||
file = None
|
||||
schema = None
|
||||
cache = None
|
||||
cache_path = None
|
||||
id_map = {}
|
||||
guid_map = {}
|
||||
deleted_ids = set()
|
||||
edited_objs = set()
|
||||
pset_template_path = ""
|
||||
pset_template_file = None
|
||||
@@ -38,28 +43,36 @@ class IfcStore:
|
||||
library_file = None
|
||||
element_listeners = set()
|
||||
undo_redo_stack_objects = set()
|
||||
undo_redo_stack_object_names = {}
|
||||
current_transaction = ""
|
||||
last_transaction = ""
|
||||
history = []
|
||||
future = []
|
||||
schema_identifiers = ["IFC4", "IFC2X3"]
|
||||
|
||||
@staticmethod
|
||||
def purge():
|
||||
IfcStore.path = ""
|
||||
IfcStore.file = None
|
||||
IfcStore.schema = None
|
||||
IfcStore.cache = None
|
||||
IfcStore.cache_path = None
|
||||
IfcStore.id_map = {}
|
||||
IfcStore.guid_map = {}
|
||||
IfcStore.deleted_ids = set()
|
||||
IfcStore.edited_objs = set()
|
||||
IfcStore.pset_template_path = ""
|
||||
IfcStore.pset_template_file = None
|
||||
IfcStore.library_path = ""
|
||||
IfcStore.library_file = None
|
||||
IfcStore.schema_identifiers = ["IFC4", "IFC2X3"]
|
||||
|
||||
@staticmethod
|
||||
def get_file():
|
||||
if IfcStore.file is None:
|
||||
IfcStore.path = bpy.context.scene.BIMProperties.ifc_file
|
||||
if not os.path.isabs(IfcStore.path):
|
||||
IfcStore.path = os.path.abspath(os.path.join(bpy.path.abspath("//"), IfcStore.path))
|
||||
if IfcStore.path:
|
||||
try:
|
||||
IfcStore.load_file(IfcStore.path)
|
||||
@@ -67,6 +80,31 @@ class IfcStore:
|
||||
pass
|
||||
return IfcStore.file
|
||||
|
||||
@staticmethod
|
||||
def get_cache():
|
||||
if IfcStore.cache is None and IfcStore.path:
|
||||
ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp
|
||||
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest()
|
||||
IfcStore.cache_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5")
|
||||
cache_settings = ifcopenshell.geom.settings()
|
||||
cache_settings.set(cache_settings.STRICT_TOLERANCE, True)
|
||||
try:
|
||||
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(IfcStore.cache_path, cache_settings)
|
||||
except:
|
||||
return
|
||||
return IfcStore.cache
|
||||
|
||||
@staticmethod
|
||||
def update_cache():
|
||||
if not IfcStore.cache:
|
||||
return
|
||||
ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp
|
||||
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest()
|
||||
new_cache_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5")
|
||||
IfcStore.cache = None
|
||||
os.replace(IfcStore.cache_path, new_cache_path)
|
||||
IfcStore.get_cache()
|
||||
|
||||
@staticmethod
|
||||
def load_file(path):
|
||||
extension = path.split(".")[-1]
|
||||
@@ -85,7 +123,9 @@ class IfcStore:
|
||||
@staticmethod
|
||||
def get_schema():
|
||||
if IfcStore.file is None:
|
||||
return
|
||||
IfcStore.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(
|
||||
bpy.context.scene.BIMProjectProperties.export_schema
|
||||
)
|
||||
elif IfcStore.schema is None:
|
||||
IfcStore.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(IfcStore.file.schema)
|
||||
return IfcStore.schema
|
||||
@@ -108,14 +148,32 @@ class IfcStore:
|
||||
IfcStore.element_listeners.add(callback)
|
||||
|
||||
@staticmethod
|
||||
def update_undo_redo_stack_objects():
|
||||
def track_undo_redo_stack_object_map():
|
||||
"""Keeps track of currently mapped object names, typically during undo and redo
|
||||
|
||||
When any Blender object is stored outside a Blender PointerProperty, such as
|
||||
in a regular Python list, there is the likely probability that the object
|
||||
will be invalidated when undo or redo occurs. Object invalidation seems to
|
||||
occur whenever an object is affected during an operation.
|
||||
|
||||
For example, if an operator deletes a modifier on o1, then o1 will be invalidated.
|
||||
"""
|
||||
for key, value in IfcStore.id_map.items():
|
||||
try:
|
||||
IfcStore.undo_redo_stack_object_names[key] = value.name
|
||||
except:
|
||||
continue
|
||||
|
||||
@staticmethod
|
||||
def track_undo_redo_stack_selected_objects():
|
||||
"""Keeps track of selected object names, typically during undo and redo
|
||||
|
||||
When any Blender object is stored outside a Blender PointerProperty, such as
|
||||
in a regular Python list, there is the likely probability that the object
|
||||
will be invalidated when undo or redo occurs. Object invalidation seems to
|
||||
only occur for selected objects either pre/post undo/redo event, including
|
||||
selected objects for consecutive undo/redos, and all children.
|
||||
occur for selected objects either pre/post undo/redo event, including
|
||||
selected objects for consecutive undo/redos, and all children. This is
|
||||
important because selected objects are often deleted from the scene.
|
||||
|
||||
So if I first select o1, then o2, then o3, then press undo, o3 will be
|
||||
invalidated. If instead I press undo twice, o3 and o2 will be invalidated.
|
||||
@@ -130,14 +188,23 @@ class IfcStore:
|
||||
IfcStore.undo_redo_stack_objects |= objects
|
||||
|
||||
@staticmethod
|
||||
def reload_linked_elements(objects=None):
|
||||
def reload_undo_redo_stack_objects():
|
||||
"""Reloads any invalidated objects after undo or redo
|
||||
|
||||
After an undo or redo operation, objects may have been invalidated in
|
||||
our id_map and guid_map. Invalidated objects are typically those that
|
||||
have been manipulated or deleted. This checks the cache of mapped and
|
||||
selected objects prior to the operation and ensures that if the object
|
||||
is invalidated, they are reloaded based on the object name that was
|
||||
tracked prior to the undo / redo.
|
||||
"""
|
||||
file = IfcStore.get_file()
|
||||
if not file:
|
||||
return
|
||||
if objects is None:
|
||||
objects = bpy.data.objects
|
||||
|
||||
for obj in objects:
|
||||
# First, reload objects that were selected or active
|
||||
for name in IfcStore.undo_redo_stack_objects:
|
||||
obj = bpy.data.objects.get(name)
|
||||
if not obj:
|
||||
continue
|
||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
||||
@@ -148,6 +215,55 @@ class IfcStore:
|
||||
data["guid"] = element.GlobalId
|
||||
IfcStore.commit_link_element(data)
|
||||
|
||||
# Scan for any straggling invalidated objects which were indirectly affected and reload them too.
|
||||
for key, value in IfcStore.id_map.items():
|
||||
try:
|
||||
value.name
|
||||
except:
|
||||
obj = bpy.data.objects.get(IfcStore.undo_redo_stack_object_names[key])
|
||||
if not obj or not obj.BIMObjectProperties.ifc_definition_id:
|
||||
continue
|
||||
element = file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
data = {"id": element.id(), "obj": obj.name}
|
||||
if hasattr(element, "GlobalId"):
|
||||
data["guid"] = element.GlobalId
|
||||
IfcStore.commit_link_element(data)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def relink_all_objects():
|
||||
file = IfcStore.get_file()
|
||||
if not file:
|
||||
return
|
||||
for obj in bpy.data.objects:
|
||||
if not obj:
|
||||
continue
|
||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
||||
continue
|
||||
element = file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
data = {"id": element.id(), "obj": obj.name}
|
||||
if hasattr(element, "GlobalId"):
|
||||
data["guid"] = element.GlobalId
|
||||
IfcStore.commit_link_element(data)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def delete_element(element):
|
||||
IfcStore.deleted_ids.add(element.id())
|
||||
if IfcStore.history:
|
||||
data = {"id": element.id()}
|
||||
IfcStore.history[-1]["operations"].append(
|
||||
{"rollback": IfcStore.rollback_delete_element, "commit": IfcStore.commit_delete_element, "data": data}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def rollback_delete_element(data):
|
||||
IfcStore.deleted_ids.remove(data["id"])
|
||||
|
||||
@staticmethod
|
||||
def commit_delete_element(data):
|
||||
IfcStore.deleted_ids.add(data["id"])
|
||||
|
||||
@staticmethod
|
||||
def link_element(element, obj):
|
||||
IfcStore.id_map[element.id()] = obj
|
||||
@@ -224,7 +340,9 @@ class IfcStore:
|
||||
except:
|
||||
pass
|
||||
|
||||
if obj:
|
||||
if element and element.is_a("IfcSurfaceStyle"):
|
||||
obj.BIMMaterialProperties.ifc_style_id = 0
|
||||
elif obj:
|
||||
obj.BIMObjectProperties.ifc_definition_id = 0
|
||||
|
||||
@staticmethod
|
||||
@@ -232,7 +350,6 @@ class IfcStore:
|
||||
is_top_level_operator = not bool(IfcStore.current_transaction)
|
||||
|
||||
if is_top_level_operator:
|
||||
active_object = context.active_object
|
||||
IfcStore.begin_transaction(operator)
|
||||
IfcStore.get_file().begin_transaction()
|
||||
# This empty transaction ensures that each operator has at least one transaction
|
||||
@@ -248,17 +365,14 @@ class IfcStore:
|
||||
operator, rollback=lambda d: IfcStore.get_file().undo(), commit=lambda d: IfcStore.get_file().redo()
|
||||
)
|
||||
IfcStore.end_transaction(operator)
|
||||
try:
|
||||
active_object.name
|
||||
context.view_layer.objects.active = active_object
|
||||
except:
|
||||
pass
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def begin_transaction(operator):
|
||||
IfcStore.undo_redo_stack_objects = set()
|
||||
IfcStore.undo_redo_stack_object_names = {}
|
||||
IfcStore.current_transaction = str(uuid.uuid4())
|
||||
operator.transaction_key = IfcStore.current_transaction
|
||||
|
||||
|
||||
@@ -16,11 +16,13 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import re
|
||||
import bpy
|
||||
import time
|
||||
import bmesh
|
||||
import shutil
|
||||
import logging
|
||||
import threading
|
||||
import mathutils
|
||||
import numpy as np
|
||||
@@ -31,6 +33,8 @@ import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.selector
|
||||
import ifcopenshell.util.geolocation
|
||||
import blenderbim.tool as tool
|
||||
from itertools import chain, accumulate
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.drawing.prop import get_diagram_scales
|
||||
|
||||
@@ -67,6 +71,7 @@ class MaterialCreator:
|
||||
self.add_default_material(element)
|
||||
if self.parse_representations(element):
|
||||
self.assign_material_slots_to_faces()
|
||||
tool.Geometry.record_object_materials(obj)
|
||||
|
||||
def add_default_material(self, element):
|
||||
element_material = ifcopenshell.util.element.get_material(element)
|
||||
@@ -107,7 +112,7 @@ class MaterialCreator:
|
||||
|
||||
def parse_representation(self, representation):
|
||||
has_parsed = False
|
||||
representation_items = self.resolve_mapped_representation_items(representation)
|
||||
representation_items = self.resolve_all_representation_items(representation)
|
||||
for item in representation_items:
|
||||
if self.parse_representation_item(item):
|
||||
has_parsed = True
|
||||
@@ -141,13 +146,12 @@ class MaterialCreator:
|
||||
]
|
||||
self.mesh.polygons.foreach_set("material_index", material_index)
|
||||
|
||||
def resolve_mapped_representation_items(self, representation):
|
||||
def resolve_all_representation_items(self, representation):
|
||||
items = []
|
||||
for item in representation.Items:
|
||||
if item.is_a("IfcMappedItem"):
|
||||
items.extend(item.MappingSource.MappedRepresentation.Items)
|
||||
else:
|
||||
items.append(item)
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
|
||||
@@ -159,6 +163,7 @@ class IfcImporter:
|
||||
self.settings = ifcopenshell.geom.settings()
|
||||
self.settings.set_deflection_tolerance(self.ifc_import_settings.deflection_tolerance)
|
||||
self.settings.set_angular_tolerance(self.ifc_import_settings.angular_tolerance)
|
||||
self.settings.set(self.settings.STRICT_TOLERANCE, True)
|
||||
self.settings_native = ifcopenshell.geom.settings()
|
||||
self.settings_native.set(self.settings_native.INCLUDE_CURVES, True)
|
||||
self.settings_2d = ifcopenshell.geom.settings()
|
||||
@@ -217,7 +222,7 @@ class IfcImporter:
|
||||
self.profile_code("Create materials")
|
||||
self.create_styles()
|
||||
self.profile_code("Create styles")
|
||||
self.create_annotation()
|
||||
self.create_annotations()
|
||||
self.profile_code("Create annotation")
|
||||
self.parse_native_elements()
|
||||
self.profile_code("Parsing native elements")
|
||||
@@ -278,10 +283,22 @@ class IfcImporter:
|
||||
for c in self.file.by_type("IfcGeometricRepresentationSubContext")
|
||||
if c.ContextIdentifier in ["Body", "Facetation"]
|
||||
]
|
||||
# Ideally, all representations should be in a subcontext, but some BIM programs don't do this correctly
|
||||
self.body_contexts.extend(
|
||||
[
|
||||
c.id()
|
||||
for c in self.file.by_type("IfcGeometricRepresentationContext", include_subtypes=False)
|
||||
if c.ContextType == "Model"
|
||||
]
|
||||
)
|
||||
if self.body_contexts:
|
||||
self.settings.set_context_ids(self.body_contexts)
|
||||
# Annotation is to accommodate broken Revit files
|
||||
# See https://github.com/Autodesk/revit-ifc/issues/187
|
||||
self.plan_contexts = [
|
||||
c.id() for c in self.file.by_type("IfcGeometricRepresentationContext") if c.ContextType == "Plan"
|
||||
c.id()
|
||||
for c in self.file.by_type("IfcGeometricRepresentationContext")
|
||||
if c.ContextType in ["Plan", "Annotation"]
|
||||
]
|
||||
if self.plan_contexts:
|
||||
self.settings_2d.set_context_ids(self.plan_contexts)
|
||||
@@ -289,8 +306,11 @@ class IfcImporter:
|
||||
def process_element_filter(self):
|
||||
if self.ifc_import_settings.has_filter:
|
||||
self.elements = set(self.ifc_import_settings.elements)
|
||||
# TODO: enable filtering for annotations
|
||||
self.annotations = set(self.file.by_type("IfcAnnotation"))
|
||||
else:
|
||||
self.elements = set(self.file.by_type("IfcElement"))
|
||||
self.annotations = set(self.file.by_type("IfcAnnotation"))
|
||||
|
||||
if self.ifc_import_settings.has_filter and self.ifc_import_settings.should_filter_spatial_elements:
|
||||
self.spatial_elements = self.get_spatial_elements_filtered_by_elements(self.elements)
|
||||
@@ -337,16 +357,17 @@ class IfcImporter:
|
||||
# FacetedBreps (without voids) are meshes. See #841.
|
||||
# Commented out as seems currently too slow.
|
||||
# if self.is_native_faceted_brep(representations):
|
||||
# self.native_data[element.GlobalId] = {
|
||||
# "representations": representations,
|
||||
# "representation": self.get_body_representation(element.Representation.Representations),
|
||||
# "type": "IfcFacetedBrep",
|
||||
# }
|
||||
# return True
|
||||
# self.native_data[element.GlobalId] = {
|
||||
# "representations": representations,
|
||||
# "representation": self.get_body_representation(element.Representation.Representations),
|
||||
# "type": "IfcFacetedBrep",
|
||||
# }
|
||||
# return True
|
||||
|
||||
def is_native_swept_disk_solid(self, representations):
|
||||
for representation in representations:
|
||||
if len(representation["raw"].Items) == 1 and representation["raw"].Items[0].is_a("IfcSweptDiskSolid"):
|
||||
items = representation["raw"].Items or [] # Be forgiving of invalid IFCs because Revit :(
|
||||
if len(items) == 1 and items[0].is_a("IfcSweptDiskSolid"):
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -443,7 +464,9 @@ class IfcImporter:
|
||||
def apply_blender_offset_to_matrix_world(self, obj, matrix):
|
||||
props = bpy.context.scene.BIMGeoreferenceProperties
|
||||
if props.has_blender_offset:
|
||||
if self.is_point_far_away((matrix[0, 3], matrix[1, 3], matrix[2, 3])):
|
||||
if obj.data and obj.data.get("has_cartesian_point_offset", None):
|
||||
obj.BIMObjectProperties.blender_offset_type = "CARTESIAN_POINT"
|
||||
elif self.is_point_far_away((matrix[0, 3], matrix[1, 3], matrix[2, 3])):
|
||||
obj.BIMObjectProperties.blender_offset_type = "OBJECT_PLACEMENT"
|
||||
matrix = ifcopenshell.util.geolocation.global2local(
|
||||
matrix,
|
||||
@@ -453,8 +476,6 @@ class IfcImporter:
|
||||
float(props.blender_x_axis_abscissa),
|
||||
float(props.blender_x_axis_ordinate),
|
||||
)
|
||||
else:
|
||||
obj.BIMObjectProperties.blender_offset_type = "CARTESIAN_POINT"
|
||||
|
||||
if self.ifc_import_settings.should_offset_model:
|
||||
matrix[0, 3] += self.ifc_import_settings.model_offset_coordinates[0]
|
||||
@@ -502,7 +523,7 @@ class IfcImporter:
|
||||
mesh = self.create_mesh(axis, shape)
|
||||
obj = bpy.data.objects.new(f"IfcGridAxis/{axis.AxisTag}", mesh)
|
||||
self.link_element(axis, obj)
|
||||
obj.matrix_world = grid_obj.matrix_world
|
||||
self.set_matrix_world(obj, grid_obj.matrix_world)
|
||||
grid_collection.objects.link(obj)
|
||||
|
||||
def create_type_products(self):
|
||||
@@ -553,7 +574,6 @@ class IfcImporter:
|
||||
def create_native_elements(self):
|
||||
total = 0
|
||||
checkpoint = time.time()
|
||||
bm = bmesh.new()
|
||||
for element in self.native_elements:
|
||||
total += 1
|
||||
if total % 250 == 0:
|
||||
@@ -575,19 +595,14 @@ class IfcImporter:
|
||||
mesh.name = mesh_name
|
||||
self.meshes[mesh_name] = mesh
|
||||
self.create_product(element, mesh=mesh)
|
||||
if native_data["type"] == "IfcFacetedBrep":
|
||||
# The current implementation doesn't reuse vertices, so we weld it after assigning materials.
|
||||
# This welding isn't true to the representation, but is easy and seems inexpensive.
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
|
||||
bm.to_mesh(mesh)
|
||||
bm.clear()
|
||||
bm.free()
|
||||
print("Done creating geometry")
|
||||
|
||||
def create_spatial_elements(self):
|
||||
self.create_generic_elements(self.spatial_elements)
|
||||
|
||||
def create_annotations(self):
|
||||
self.create_generic_elements(self.annotations)
|
||||
|
||||
def create_elements(self):
|
||||
self.create_generic_elements(self.elements)
|
||||
|
||||
@@ -615,6 +630,9 @@ class IfcImporter:
|
||||
)
|
||||
else:
|
||||
iterator = ifcopenshell.geom.iterator(self.settings, self.file, include=products)
|
||||
cache = IfcStore.get_cache()
|
||||
if cache:
|
||||
iterator.set_cache(cache)
|
||||
valid_file = iterator.initialize()
|
||||
if not valid_file:
|
||||
return results
|
||||
@@ -631,7 +649,7 @@ class IfcImporter:
|
||||
)
|
||||
)
|
||||
checkpoint = time.time()
|
||||
# self.update_progress(((total / approx_total_products) * progress_range) + start_progress)
|
||||
self.update_progress((iterator.progress() / 100 * progress_range) + start_progress)
|
||||
shape = iterator.get()
|
||||
if shape:
|
||||
product = self.file.by_id(shape.guid)
|
||||
@@ -650,9 +668,6 @@ class IfcImporter:
|
||||
print("Done creating geometry")
|
||||
return results
|
||||
|
||||
def create_annotation(self):
|
||||
self.create_curve_products(self.file.by_type("IfcAnnotation"))
|
||||
|
||||
def create_structural_items(self):
|
||||
# Create structural collections
|
||||
self.structural_member_collection = bpy.data.collections.new("Members")
|
||||
@@ -690,7 +705,7 @@ class IfcImporter:
|
||||
mesh.from_pydata([mathutils.Vector(vertex) * self.unit_scale], [], [])
|
||||
|
||||
obj = bpy.data.objects.new("{}/{}".format(product.is_a(), product.Name), mesh)
|
||||
obj.matrix_world = self.apply_blender_offset_to_matrix_world(obj, placement_matrix)
|
||||
self.set_matrix_world(obj, self.apply_blender_offset_to_matrix_world(obj, placement_matrix))
|
||||
self.link_element(product, obj)
|
||||
|
||||
def get_pointcloud_representation(self, product):
|
||||
@@ -749,7 +764,7 @@ class IfcImporter:
|
||||
mesh.from_pydata(vertex_list, [], [])
|
||||
|
||||
obj = bpy.data.objects.new("{}/{}".format(product.is_a(), product.Name), mesh)
|
||||
obj.matrix_world = self.apply_blender_offset_to_matrix_world(obj, placement_matrix)
|
||||
self.set_matrix_world(obj, self.apply_blender_offset_to_matrix_world(obj, placement_matrix))
|
||||
self.link_element(product, obj)
|
||||
return product
|
||||
|
||||
@@ -763,6 +778,9 @@ class IfcImporter:
|
||||
)
|
||||
else:
|
||||
iterator = ifcopenshell.geom.iterator(self.settings_2d, self.file, include=products)
|
||||
cache = IfcStore.get_cache()
|
||||
if cache:
|
||||
iterator.set_cache(cache)
|
||||
valid_file = iterator.initialize()
|
||||
if not valid_file:
|
||||
return results
|
||||
@@ -793,6 +811,10 @@ class IfcImporter:
|
||||
pass
|
||||
elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
|
||||
mesh = self.create_camera(element, shape)
|
||||
self.link_mesh(shape, mesh)
|
||||
elif element.is_a("IfcAnnotation") and self.is_curve_annotation(element):
|
||||
mesh = self.create_curve(element, shape)
|
||||
self.link_mesh(shape, mesh)
|
||||
elif shape:
|
||||
mesh_name = self.get_mesh_name(shape.geometry)
|
||||
mesh = self.meshes.get(mesh_name)
|
||||
@@ -812,13 +834,13 @@ class IfcImporter:
|
||||
mat = np.array(
|
||||
([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1])
|
||||
)
|
||||
obj.matrix_world = self.apply_blender_offset_to_matrix_world(obj, mat)
|
||||
self.set_matrix_world(obj, self.apply_blender_offset_to_matrix_world(obj, mat))
|
||||
self.material_creator.create(element, obj, mesh)
|
||||
elif mesh:
|
||||
obj.matrix_world = self.apply_blender_offset_to_matrix_world(obj, self.get_element_matrix(element))
|
||||
self.set_matrix_world(obj, self.apply_blender_offset_to_matrix_world(obj, self.get_element_matrix(element)))
|
||||
self.material_creator.create(element, obj, mesh)
|
||||
elif hasattr(element, "ObjectPlacement"):
|
||||
obj.matrix_world = self.apply_blender_offset_to_matrix_world(obj, self.get_element_matrix(element))
|
||||
self.set_matrix_world(obj, self.apply_blender_offset_to_matrix_world(obj, self.get_element_matrix(element)))
|
||||
|
||||
self.add_opening_relation(element, obj)
|
||||
|
||||
@@ -834,79 +856,167 @@ class IfcImporter:
|
||||
|
||||
def create_native_faceted_brep(self, element, mesh_name):
|
||||
# TODO: georeferencing?
|
||||
# Note: to make this algorithm simpler (it's already confusing) we don't reuse / weld verts
|
||||
# co [x y z x y z x y z ...]
|
||||
# vertex_index [i i i i i ...]
|
||||
# loop_start [0 3 6 9 ...] (for tris)
|
||||
# loop_total [3 3 3 3 ...] (for tris)
|
||||
co = []
|
||||
vertex_index = []
|
||||
loop_start = []
|
||||
loop_total = []
|
||||
total_verts = 0
|
||||
total_polygons = 0
|
||||
materials = []
|
||||
material_ids = []
|
||||
item_index = 0
|
||||
self.mesh_data = {
|
||||
"co": [],
|
||||
"vertex_index": [],
|
||||
"loop_start": [],
|
||||
"loop_total": [],
|
||||
"total_verts": 0,
|
||||
"total_polygons": 0,
|
||||
"materials": [],
|
||||
"material_ids": [],
|
||||
}
|
||||
|
||||
for representation in element.Representation.Representations:
|
||||
if representation.ContextOfItems.id() not in self.body_contexts:
|
||||
continue
|
||||
self.convert_representation(representation)
|
||||
|
||||
mesh = bpy.data.meshes.new("Native")
|
||||
mesh.vertices.add(self.mesh_data["total_verts"])
|
||||
mesh.vertices.foreach_set("co", [c * self.unit_scale for c in self.mesh_data["co"]])
|
||||
# mesh.vertices.foreach_set("co", self.mesh_data["co"])
|
||||
mesh.loops.add(len(self.mesh_data["vertex_index"]))
|
||||
mesh.loops.foreach_set("vertex_index", self.mesh_data["vertex_index"])
|
||||
mesh.polygons.add(self.mesh_data["total_polygons"])
|
||||
mesh.polygons.foreach_set("loop_start", self.mesh_data["loop_start"])
|
||||
mesh.polygons.foreach_set("loop_total", self.mesh_data["loop_total"])
|
||||
mesh.update()
|
||||
|
||||
mesh["ios_materials"] = self.mesh_data["materials"]
|
||||
mesh["ios_material_ids"] = self.mesh_data["material_ids"]
|
||||
return mesh
|
||||
|
||||
def convert_representation(self, representation):
|
||||
for item in representation.Items:
|
||||
self.convert_representation_item(item)
|
||||
|
||||
def convert_representation_item(self, item):
|
||||
if item.is_a("IfcMappedItem"):
|
||||
# mapping_target = matrix
|
||||
self.convert_representation(item.MappingSource.MappedRepresentation)
|
||||
elif item.is_a() == "IfcFacetedBrep":
|
||||
self.convert_representation_item_faceted_brep(item)
|
||||
|
||||
def convert_representation_item_faceted_brep(self, item):
|
||||
# On a few occasions, we flatten a list. This seems to be the most efficient way to do it.
|
||||
# https://stackoverflow.com/questions/20112776/how-do-i-flatten-a-list-of-lists-nested-lists
|
||||
mesh = item.get_info_2(recursive=True)
|
||||
|
||||
# For huge face sets it might be better to do a "flatmap" instead of sum()
|
||||
# bounds = sum((f["Bounds"] for f in mesh["Outer"]["CfsFaces"] if len(f["Bounds"]) == 1), ())
|
||||
bounds = tuple(chain.from_iterable(f["Bounds"] for f in mesh["Outer"]["CfsFaces"] if len(f["Bounds"]) == 1))
|
||||
# Here are some untested alternatives, are they faster?
|
||||
# bounds = tuple((f["Bounds"] for f in mesh["Outer"]["CfsFaces"] if len(f["Bounds"]) == 1))[0]
|
||||
# bounds = chain.from_iterable(f["Bounds"] for f in mesh["Outer"]["CfsFaces"] if len(f["Bounds"]) == 1)
|
||||
|
||||
polygons = [[(p["id"], p["Coordinates"]) for p in b["Bound"]["Polygon"]] for b in bounds]
|
||||
|
||||
for face in mesh["Outer"]["CfsFaces"]:
|
||||
# Blender cannot handle faces with holes.
|
||||
if len(face["Bounds"]) > 1:
|
||||
inner_bounds = []
|
||||
inner_bound_point_ids = []
|
||||
for bound in face["Bounds"]:
|
||||
if bound["type"] == "IfcFaceOuterBound":
|
||||
outer_bound = [[p["Coordinates"] for p in bound["Bound"]["Polygon"]]]
|
||||
outer_bound_point_ids = [[p["id"] for p in bound["Bound"]["Polygon"]]]
|
||||
else:
|
||||
inner_bounds.append([p["Coordinates"] for p in bound["Bound"]["Polygon"]])
|
||||
inner_bound_point_ids.append([p["id"] for p in bound["Bound"]["Polygon"]])
|
||||
points = outer_bound[0].copy()
|
||||
[points.extend(p) for p in inner_bounds]
|
||||
point_ids = outer_bound_point_ids[0].copy()
|
||||
[point_ids.extend(p) for p in inner_bound_point_ids]
|
||||
|
||||
tessellated_polygons = mathutils.geometry.tessellate_polygon(outer_bound + inner_bounds)
|
||||
polygons.extend([[(point_ids[pi], points[pi]) for pi in t] for t in tessellated_polygons])
|
||||
|
||||
# Clever vertex welding algorithm by Thomas Krijnen. See #841.
|
||||
|
||||
# by id
|
||||
di0 = {}
|
||||
# by coords
|
||||
di1 = {}
|
||||
|
||||
vertex_index_offset = self.mesh_data["total_verts"]
|
||||
|
||||
def lookup(id_coords):
|
||||
idx = di0.get(id_coords[0])
|
||||
if idx is None:
|
||||
idx = di1.get(id_coords[1])
|
||||
if idx is None:
|
||||
l = len(di0)
|
||||
di0[id_coords[0]] = l
|
||||
di1[id_coords[1]] = l
|
||||
return l + vertex_index_offset
|
||||
else:
|
||||
return idx + vertex_index_offset
|
||||
else:
|
||||
return idx + vertex_index_offset
|
||||
|
||||
mapped_polygons = [list(map(lookup, p)) for p in polygons]
|
||||
|
||||
self.mesh_data["vertex_index"].extend(chain.from_iterable(mapped_polygons))
|
||||
|
||||
# Flattened vertex coords
|
||||
self.mesh_data["co"].extend(chain.from_iterable(di1.keys()))
|
||||
self.mesh_data["total_verts"] += len(di1.keys())
|
||||
loop_total = [len(p) for p in mapped_polygons]
|
||||
total_polygons = len(mapped_polygons)
|
||||
self.mesh_data["total_polygons"] += total_polygons
|
||||
|
||||
self.mesh_data["materials"].append(self.get_representation_item_material_name(item) or "NULLMAT")
|
||||
material_index = len(self.mesh_data["materials"]) - 1
|
||||
if self.mesh_data["materials"][material_index] == "NULLMAT":
|
||||
# Magic number -1 represents no material, until this has a better approach
|
||||
self.mesh_data["material_ids"] += [-1] * total_polygons
|
||||
else:
|
||||
self.mesh_data["material_ids"] += [material_index] * total_polygons
|
||||
|
||||
if self.mesh_data["loop_start"]:
|
||||
loop_start_offset = self.mesh_data["loop_start"][-1] + self.mesh_data["loop_total"][-1]
|
||||
else:
|
||||
loop_start_offset = 0
|
||||
|
||||
loop_start = [loop_start_offset] + [loop_start_offset + i for i in list(accumulate(loop_total[0:-1]))]
|
||||
self.mesh_data["loop_total"].extend(loop_total)
|
||||
self.mesh_data["loop_start"].extend(loop_start)
|
||||
# list(di1.keys())
|
||||
|
||||
def create_native_swept_disk_solid(self, element, mesh_name):
|
||||
# TODO: georeferencing?
|
||||
curve = bpy.data.curves.new(mesh_name, type="CURVE")
|
||||
curve.dimensions = "3D"
|
||||
curve.resolution_u = 2
|
||||
polyline = curve.splines.new("POLY")
|
||||
|
||||
for representation in self.native_data[element.GlobalId]["representations"]:
|
||||
for item in representation["raw"].Items:
|
||||
# TODO: if I reimplement native faceted breps, recheck this material implementation
|
||||
materials.append(self.get_representation_item_material_name(item) or "NULLMAT")
|
||||
mesh = item.get_info_2(recursive=True) # See bug #841
|
||||
total_item_polygons = 0
|
||||
for face in mesh["Outer"]["CfsFaces"]:
|
||||
# Blender cannot handle faces with holes.
|
||||
if len(face["Bounds"]) > 1:
|
||||
inner_bounds = []
|
||||
for bound in face["Bounds"]:
|
||||
if bound["type"] == "IfcFaceOuterBound":
|
||||
outer_bound = [[p["Coordinates"] for p in bound["Bound"]["Polygon"]]]
|
||||
else:
|
||||
inner_bounds.append([p["Coordinates"] for p in bound["Bound"]["Polygon"]])
|
||||
points = outer_bound[0].copy()
|
||||
[points.extend(p) for p in inner_bounds]
|
||||
tessellated_polygons = mathutils.geometry.tessellate_polygon(outer_bound + inner_bounds)
|
||||
tessellated_faces = [({"Coordinates": points[pi]} for pi in t) for t in tessellated_polygons]
|
||||
else:
|
||||
tessellated_faces = [face["Bounds"][0]["Bound"]["Polygon"]]
|
||||
# TODO: support inner radius, start param, and end param
|
||||
geometry = ifcopenshell.geom.create_shape(self.settings_native, item.Directrix)
|
||||
e = geometry.edges
|
||||
v = geometry.verts
|
||||
vertices = [[v[i], v[i + 1], v[i + 2], 1] for i in range(0, len(v), 3)]
|
||||
edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)]
|
||||
v2 = None
|
||||
for edge in edges:
|
||||
v1 = vertices[edge[0]]
|
||||
if v1 != v2:
|
||||
polyline = curve.splines.new("POLY")
|
||||
polyline.points[-1].co = representation["matrix"] @ mathutils.Vector(v1)
|
||||
v2 = vertices[edge[1]]
|
||||
polyline.points.add(1)
|
||||
polyline.points[-1].co = representation["matrix"] @ mathutils.Vector(v2)
|
||||
|
||||
for tessellated_face in tessellated_faces:
|
||||
loop_start.append(total_verts)
|
||||
loop_count = 0
|
||||
total_polygons += 1
|
||||
total_item_polygons += 1
|
||||
for point in tessellated_face:
|
||||
co.extend(
|
||||
representation["matrix"]
|
||||
@ mathutils.Vector((c * self.unit_scale for c in point["Coordinates"]))
|
||||
)
|
||||
total_verts += 1
|
||||
loop_count += 1
|
||||
loop_total.append(loop_count)
|
||||
vertex_index = range(0, total_verts)
|
||||
if materials[item_index] == "NULLMAT":
|
||||
# Magic number -1 represents no material, until this has a better approach
|
||||
material_ids += [-1] * total_item_polygons
|
||||
else:
|
||||
material_ids += [item_index] * total_item_polygons
|
||||
item_index += 1
|
||||
mesh = bpy.data.meshes.new("Tester")
|
||||
curve.bevel_depth = self.unit_scale * item.Radius
|
||||
return curve
|
||||
|
||||
mesh.vertices.add(total_verts)
|
||||
mesh.vertices.foreach_set("co", co)
|
||||
mesh.loops.add(total_verts)
|
||||
mesh.loops.foreach_set("vertex_index", vertex_index)
|
||||
mesh.polygons.add(total_polygons)
|
||||
mesh.polygons.foreach_set("loop_start", loop_start)
|
||||
mesh.polygons.foreach_set("loop_total", loop_total)
|
||||
mesh.update()
|
||||
|
||||
mesh["ios_materials"] = materials
|
||||
mesh["ios_material_ids"] = material_ids
|
||||
return mesh
|
||||
|
||||
def create_native_swept_disk_solid(self, element, mesh_name):
|
||||
def create_native_annotation(self, element, mesh_name):
|
||||
# TODO: georeferencing?
|
||||
curve = bpy.data.curves.new(mesh_name, type="CURVE")
|
||||
curve.dimensions = "3D"
|
||||
@@ -1082,6 +1192,7 @@ class IfcImporter:
|
||||
def create_spatial_decomposition_collections(self):
|
||||
for rel_aggregate in self.project["ifc"].IsDecomposedBy or []:
|
||||
self.create_spatial_decomposition_collection(self.project["blender"], rel_aggregate.RelatedObjects)
|
||||
self.create_views_collection()
|
||||
self.create_type_collection()
|
||||
|
||||
def create_type_collection(self):
|
||||
@@ -1093,6 +1204,22 @@ class IfcImporter:
|
||||
self.type_collection = bpy.data.collections.new("Types")
|
||||
self.project["blender"].children.link(self.type_collection)
|
||||
|
||||
def create_views_collection(self):
|
||||
view_collection = None
|
||||
for collection in self.project["blender"].children:
|
||||
if collection.name == "Views":
|
||||
view_collection = collection
|
||||
break
|
||||
if not view_collection:
|
||||
view_collection = bpy.data.collections.new("Views")
|
||||
self.project["blender"].children.link(view_collection)
|
||||
for element in self.file.by_type("IfcAnnotation"):
|
||||
if element.ObjectType == "DRAWING":
|
||||
group = [r for r in element.HasAssignments if r.is_a("IfcRelAssignsToGroup")][0].RelatingGroup
|
||||
collection = bpy.data.collections.new("IfcGroup/" + group.Name)
|
||||
self.collections[group.GlobalId] = collection
|
||||
view_collection.children.link(collection)
|
||||
|
||||
def create_spatial_decomposition_collection(self, parent, related_objects):
|
||||
for element in related_objects:
|
||||
if element not in self.spatial_elements:
|
||||
@@ -1171,25 +1298,172 @@ class IfcImporter:
|
||||
name = style.Name or str(style.id())
|
||||
blender_material = bpy.data.materials.new(name)
|
||||
|
||||
old_definition_id = blender_material.BIMObjectProperties.ifc_definition_id
|
||||
if not old_definition_id:
|
||||
self.link_element(style, blender_material)
|
||||
blender_material.BIMObjectProperties.ifc_definition_id = old_definition_id
|
||||
self.link_element(style, blender_material)
|
||||
|
||||
blender_material.BIMMaterialProperties.ifc_style_id = style.id()
|
||||
self.material_creator.styles[style.id()] = blender_material
|
||||
|
||||
rendering_style = None
|
||||
texture_style = None
|
||||
|
||||
for surface_style in style.Styles:
|
||||
if surface_style.is_a("IfcSurfaceStyleShading"):
|
||||
alpha = 1.0
|
||||
# Transparency was added in IFC4
|
||||
if hasattr(surface_style, "Transparency") and surface_style.Transparency:
|
||||
alpha = 1 - surface_style.Transparency
|
||||
blender_material.diffuse_color = (
|
||||
surface_style.SurfaceColour.Red,
|
||||
surface_style.SurfaceColour.Green,
|
||||
surface_style.SurfaceColour.Blue,
|
||||
alpha,
|
||||
if surface_style.is_a() == "IfcSurfaceStyleShading":
|
||||
self.create_surface_style_shading(blender_material, surface_style)
|
||||
elif surface_style.is_a("IfcSurfaceStyleRendering"):
|
||||
rendering_style = surface_style
|
||||
self.create_surface_style_rendering(blender_material, surface_style)
|
||||
elif surface_style.is_a("IfcSurfaceStyleWithTextures"):
|
||||
texture_style = surface_style
|
||||
|
||||
if rendering_style and texture_style:
|
||||
self.create_surface_style_with_textures(blender_material, rendering_style, texture_style)
|
||||
|
||||
def create_surface_style_shading(self, blender_material, surface_style):
|
||||
alpha = 1.0
|
||||
# Transparency was added in IFC4
|
||||
if hasattr(surface_style, "Transparency") and surface_style.Transparency:
|
||||
alpha = 1 - surface_style.Transparency
|
||||
blender_material.diffuse_color = (
|
||||
surface_style.SurfaceColour.Red,
|
||||
surface_style.SurfaceColour.Green,
|
||||
surface_style.SurfaceColour.Blue,
|
||||
alpha,
|
||||
)
|
||||
|
||||
def create_surface_style_rendering(self, blender_material, surface_style):
|
||||
self.create_surface_style_shading(blender_material, surface_style)
|
||||
if surface_style.ReflectanceMethod in ["PHYSICAL", "NOTDEFINED"]:
|
||||
blender_material.use_nodes = True
|
||||
bsdf = blender_material.node_tree.nodes["Principled BSDF"]
|
||||
if surface_style.DiffuseColour and surface_style.DiffuseColour.is_a("IfcColourRgb"):
|
||||
bsdf.inputs["Base Color"].default_value = (
|
||||
surface_style.DiffuseColour.Red,
|
||||
surface_style.DiffuseColour.Green,
|
||||
surface_style.DiffuseColour.Blue,
|
||||
1,
|
||||
)
|
||||
if surface_style.SpecularColour and surface_style.SpecularColour.is_a("IfcNormalisedRatioMeasure"):
|
||||
bsdf.inputs["Metallic"].default_value = surface_style.SpecularColour.wrappedValue
|
||||
if surface_style.SpecularHighlight and surface_style.SpecularHighlight.is_a("IfcSpecularRoughness"):
|
||||
bsdf.inputs["Roughness"].default_value = surface_style.SpecularHighlight.wrappedValue
|
||||
if hasattr(surface_style, "Transparency") and surface_style.Transparency:
|
||||
bsdf.inputs["Alpha"].default_value = 1 - surface_style.Transparency
|
||||
blender_material.blend_method = "BLEND"
|
||||
elif surface_style.ReflectanceMethod == "FLAT":
|
||||
blender_material.use_nodes = True
|
||||
|
||||
output = {n.type: n for n in blender_material.node_tree.nodes}.get("OUTPUT_MATERIAL", None)
|
||||
bsdf = blender_material.node_tree.nodes["Principled BSDF"]
|
||||
|
||||
mix = blender_material.node_tree.nodes.new(type="ShaderNodeMixShader")
|
||||
mix.location = bsdf.location
|
||||
blender_material.node_tree.links.new(mix.outputs[0], output.inputs["Surface"])
|
||||
|
||||
blender_material.node_tree.nodes.remove(bsdf)
|
||||
|
||||
lightpath = blender_material.node_tree.nodes.new(type="ShaderNodeLightPath")
|
||||
lightpath.location = mix.location - mathutils.Vector((200, -200))
|
||||
blender_material.node_tree.links.new(lightpath.outputs[0], mix.inputs[0])
|
||||
|
||||
bsdf = blender_material.node_tree.nodes.new(type="ShaderNodeBsdfTransparent")
|
||||
bsdf.location = mix.location - mathutils.Vector((200, 0))
|
||||
blender_material.node_tree.links.new(bsdf.outputs[0], mix.inputs[1])
|
||||
|
||||
rgb = blender_material.node_tree.nodes.new(type="ShaderNodeRGB")
|
||||
rgb.location = mix.location - mathutils.Vector((200, 200))
|
||||
blender_material.node_tree.links.new(rgb.outputs[0], mix.inputs[2])
|
||||
|
||||
if surface_style.DiffuseColour and surface_style.DiffuseColour.is_a("IfcColourRgb"):
|
||||
rgb.outputs[0].default_value = (
|
||||
surface_style.DiffuseColour.Red,
|
||||
surface_style.DiffuseColour.Green,
|
||||
surface_style.DiffuseColour.Blue,
|
||||
1,
|
||||
)
|
||||
|
||||
def create_surface_style_with_textures(self, blender_material, rendering_style, texture_style):
|
||||
for texture in texture_style.Textures:
|
||||
mode = getattr(texture, "Mode", None)
|
||||
node = None
|
||||
|
||||
if texture.is_a("IfcImageTexture"):
|
||||
image_url = texture.URLReference
|
||||
if not os.path.abspath(texture.URLReference) and tool.Ifc.get_path():
|
||||
image_url = os.path.join(os.path.dirname(tool.Ifc.get_path()), texture.URLReference)
|
||||
|
||||
if rendering_style.ReflectanceMethod in ["PHYSICAL", "NOTDEFINED"]:
|
||||
bsdf = blender_material.node_tree.nodes["Principled BSDF"]
|
||||
if mode == "NORMAL":
|
||||
normalmap = blender_material.node_tree.nodes.new(type="ShaderNodeNormalMap")
|
||||
normalmap.location = bsdf.location - mathutils.Vector((200, 0))
|
||||
blender_material.node_tree.links.new(normalmap.outputs[0], bsdf.inputs["Normal"])
|
||||
|
||||
node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage")
|
||||
node.location = normalmap.location - mathutils.Vector((200, 0))
|
||||
image = bpy.data.images.load(image_url)
|
||||
image.colorspace_settings.name = "Non-Color"
|
||||
node.image = image
|
||||
blender_material.node_tree.links.new(node.outputs[0], normalmap.inputs["Color"])
|
||||
elif mode == "EMISSIVE":
|
||||
output = {n.type: n for n in blender_material.node_tree.nodes}.get("OUTPUT_MATERIAL", None)
|
||||
|
||||
add = blender_material.node_tree.nodes.new(type="ShaderNodeAddShader")
|
||||
add.location = bsdf.location + mathutils.Vector((200, 0))
|
||||
blender_material.node_tree.links.new(bsdf.outputs[0], add.inputs[1])
|
||||
blender_material.node_tree.links.new(add.outputs[0], output.inputs[0])
|
||||
|
||||
emission = blender_material.node_tree.nodes.new(type="ShaderNodeEmission")
|
||||
emission.location = add.location - mathutils.Vector((200, 0))
|
||||
blender_material.node_tree.links.new(emission.outputs[0], add.inputs[0])
|
||||
|
||||
node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage")
|
||||
node.location = emission.location - mathutils.Vector((200, 0))
|
||||
image = bpy.data.images.load(image_url)
|
||||
node.image = image
|
||||
blender_material.node_tree.links.new(node.outputs[0], emission.inputs[0])
|
||||
elif mode == "METALLICROUGHNESS":
|
||||
separate = blender_material.node_tree.nodes.new(type="ShaderNodeSeparateRGB")
|
||||
separate.location = bsdf.location - mathutils.Vector((200, 0))
|
||||
blender_material.node_tree.links.new(separate.outputs[1], bsdf.inputs["Roughness"])
|
||||
blender_material.node_tree.links.new(separate.outputs[2], bsdf.inputs["Metallic"])
|
||||
|
||||
node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage")
|
||||
node.location = separate.location - mathutils.Vector((200, 0))
|
||||
image = bpy.data.images.load(image_url)
|
||||
image.colorspace_settings.name = "Non-Color"
|
||||
node.image = image
|
||||
blender_material.node_tree.links.new(node.outputs[0], separate.inputs[0])
|
||||
elif mode == "OCCLUSION":
|
||||
# TODO work out how to implement glTF settings here
|
||||
# https://docs.blender.org/manual/en/dev/addons/import_export/scene_gltf2.html
|
||||
pass
|
||||
elif mode == "DIFFUSE":
|
||||
node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage")
|
||||
node.location = bsdf.location - mathutils.Vector((400, 0))
|
||||
image = bpy.data.images.load(image_url)
|
||||
node.image = image
|
||||
blender_material.node_tree.links.new(node.outputs[0], bsdf.inputs["Base Color"])
|
||||
blender_material.node_tree.links.new(node.outputs[1], bsdf.inputs["Alpha"])
|
||||
blender_material.blend_method = "BLEND"
|
||||
elif rendering_style.ReflectanceMethod == "FLAT":
|
||||
bsdf = blender_material.node_tree.nodes["Mix Shader"]
|
||||
if mode == "EMISSIVE":
|
||||
node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage")
|
||||
node.location = bsdf.location - mathutils.Vector((200, 0))
|
||||
image = bpy.data.images.load(image_url)
|
||||
node.image = image
|
||||
blender_material.node_tree.links.new(node.outputs[0], bsdf.inputs[2])
|
||||
|
||||
if node and getattr(texture, "IsMappedBy", None):
|
||||
coordinates = texture.IsMappedBy[0]
|
||||
coord = blender_material.node_tree.nodes.new(type="ShaderNodeTexCoord")
|
||||
coord.location = node.location - mathutils.Vector((200, 0))
|
||||
if coordinates.is_a("IfcTextureCoordinateGenerator") and coordinates.Mode == "COORD":
|
||||
blender_material.node_tree.links.new(coord.outputs["Generated"], node.inputs["Vector"])
|
||||
elif coordinates.is_a("IfcTextureCoordinateGenerator") and coordinates.Mode == "COORD-EYE":
|
||||
blender_material.node_tree.links.new(coord.outputs["Camera"], node.inputs["Vector"])
|
||||
else:
|
||||
blender_material.node_tree.links.new(coord.outputs["UV"], node.inputs["Vector"])
|
||||
|
||||
def get_name(self, element):
|
||||
return "{}/{}".format(element.is_a(), element.Name)
|
||||
@@ -1235,16 +1509,9 @@ class IfcImporter:
|
||||
return self.structural_member_collection.objects.link(obj)
|
||||
elif element.is_a("IfcStructuralConnection"):
|
||||
return self.structural_connection_collection.objects.link(obj)
|
||||
elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
|
||||
view_collection = bpy.data.collections.get("Views")
|
||||
if not view_collection:
|
||||
view_collection = bpy.data.collections.new("Views")
|
||||
bpy.context.scene.collection.children.link(view_collection)
|
||||
elif element.is_a("IfcAnnotation") and self.is_drawing_annotation(element):
|
||||
group = [r for r in element.HasAssignments if r.is_a("IfcRelAssignsToGroup")][0].RelatingGroup
|
||||
drawing_collection = bpy.data.collections.new("IfcGroup/" + group.Name)
|
||||
view_collection.children.link(drawing_collection)
|
||||
drawing_collection.objects.link(obj)
|
||||
return
|
||||
return self.collections[group.GlobalId].objects.link(obj)
|
||||
|
||||
container = ifcopenshell.util.element.get_container(element)
|
||||
if container:
|
||||
@@ -1260,6 +1527,37 @@ class IfcImporter:
|
||||
self.ifc_import_settings.logger.warning("Warning: this object is outside the spatial hierarchy %s", element)
|
||||
bpy.context.scene.collection.objects.link(obj)
|
||||
|
||||
def is_curve_annotation(self, element):
|
||||
return element.ObjectType in [
|
||||
"DIMENSION",
|
||||
"EQUAL_DIMENSION",
|
||||
"MISC",
|
||||
"PLAN_LEVEL",
|
||||
"SECTION_LEVEL",
|
||||
"STAIR_ARROW",
|
||||
"TEXT_LEADER",
|
||||
]
|
||||
|
||||
def is_drawing_annotation(self, element):
|
||||
return element.ObjectType in [
|
||||
"BREAKLINE",
|
||||
"DIMENSION",
|
||||
"DRAWING",
|
||||
"EQUAL_DIMENSION",
|
||||
"GRID",
|
||||
"HIDDEN_LINE",
|
||||
"MISC",
|
||||
"PLAN_LEVEL",
|
||||
"SECTION_LEVEL",
|
||||
"STAIR_ARROW",
|
||||
"TEXT",
|
||||
"TEXT_LEADER",
|
||||
"LEADER",
|
||||
"HATCH",
|
||||
"LINEWORK",
|
||||
"AREA",
|
||||
]
|
||||
|
||||
def get_element_matrix(self, element):
|
||||
result = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
|
||||
result[0][3] *= self.unit_scale
|
||||
@@ -1384,8 +1682,39 @@ class IfcImporter:
|
||||
else:
|
||||
camera.BIMCameraProperties.diagram_scale = "CUSTOM"
|
||||
camera.BIMCameraProperties.custom_diagram_scale = pset["Scale"]
|
||||
if "HasUnderlay" in pset:
|
||||
camera.BIMCameraProperties.has_underlay = pset["HasUnderlay"]
|
||||
if "HasLinework" in pset:
|
||||
camera.BIMCameraProperties.has_linework = pset["HasLinework"]
|
||||
if "HasAnnotation" in pset:
|
||||
camera.BIMCameraProperties.has_annotation = pset["HasAnnotation"]
|
||||
return camera
|
||||
|
||||
def create_curve(self, element, shape):
|
||||
if hasattr(shape, "geometry"):
|
||||
geometry = shape.geometry
|
||||
else:
|
||||
geometry = shape
|
||||
|
||||
curve = bpy.data.curves.new(self.get_mesh_name(geometry), type="CURVE")
|
||||
curve.dimensions = "3D"
|
||||
curve.resolution_u = 2
|
||||
|
||||
e = geometry.edges
|
||||
v = geometry.verts
|
||||
vertices = [[v[i], v[i + 1], v[i + 2], 1] for i in range(0, len(v), 3)]
|
||||
edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)]
|
||||
v2 = None
|
||||
for edge in edges:
|
||||
v1 = vertices[edge[0]]
|
||||
if v1 != v2:
|
||||
polyline = curve.splines.new("POLY")
|
||||
polyline.points[-1].co = mathutils.Vector(v1)
|
||||
v2 = vertices[edge[1]]
|
||||
polyline.points.add(1)
|
||||
polyline.points[-1].co = mathutils.Vector(v2)
|
||||
return curve
|
||||
|
||||
def create_mesh(self, element, shape):
|
||||
try:
|
||||
if hasattr(shape, "geometry"):
|
||||
@@ -1401,20 +1730,22 @@ class IfcImporter:
|
||||
and geometry.verts
|
||||
and self.is_point_far_away((geometry.verts[0], geometry.verts[1], geometry.verts[2]))
|
||||
):
|
||||
ordinate_index = 0
|
||||
verts = [None] * len(geometry.verts)
|
||||
offset_point = (
|
||||
float(props.blender_eastings) * self.unit_scale,
|
||||
float(props.blender_northings) * self.unit_scale,
|
||||
float(props.blender_orthogonal_height) * self.unit_scale,
|
||||
)
|
||||
for i, vert in enumerate(geometry.verts):
|
||||
if ordinate_index > 2:
|
||||
ordinate_index = 0
|
||||
verts[i] = vert - offset_point[ordinate_index]
|
||||
ordinate_index += 1
|
||||
for i in range(0, len(geometry.verts), 3):
|
||||
verts[i], verts[i + 1], verts[i + 2] = ifcopenshell.util.geolocation.enh2xyz(
|
||||
geometry.verts[i],
|
||||
geometry.verts[i + 1],
|
||||
geometry.verts[i + 2],
|
||||
float(props.blender_eastings) * self.unit_scale,
|
||||
float(props.blender_northings) * self.unit_scale,
|
||||
float(props.blender_orthogonal_height) * self.unit_scale,
|
||||
float(props.blender_x_axis_abscissa),
|
||||
float(props.blender_x_axis_ordinate),
|
||||
)
|
||||
mesh["has_cartesian_point_offset"] = True
|
||||
else:
|
||||
verts = geometry.verts
|
||||
mesh["has_cartesian_point_offset"] = False
|
||||
|
||||
if geometry.faces:
|
||||
num_vertices = len(verts) // 3
|
||||
@@ -1502,7 +1833,12 @@ class IfcImporter:
|
||||
if "-" in geometry.id:
|
||||
mesh.BIMMeshProperties.ifc_definition_id = int(geometry.id.split("-")[0])
|
||||
else:
|
||||
mesh.BIMMeshProperties.ifc_definition_id = int(geometry.id)
|
||||
# TODO: See #2002
|
||||
mesh.BIMMeshProperties.ifc_definition_id = int(geometry.id.replace(",", ""))
|
||||
|
||||
def set_matrix_world(self, obj, matrix_world):
|
||||
obj.matrix_world = matrix_world
|
||||
tool.Geometry.record_object_position(obj)
|
||||
|
||||
|
||||
class IfcImportSettings:
|
||||
@@ -1525,11 +1861,13 @@ class IfcImportSettings:
|
||||
self.collection_mode = "DECOMPOSITION"
|
||||
|
||||
@staticmethod
|
||||
def factory(context, input_file, logger):
|
||||
scene_diff = context.scene.DiffProperties
|
||||
props = context.scene.BIMProjectProperties
|
||||
def factory(context=None, input_file=None, logger=None):
|
||||
scene_diff = bpy.context.scene.DiffProperties
|
||||
props = bpy.context.scene.BIMProjectProperties
|
||||
settings = IfcImportSettings()
|
||||
settings.input_file = input_file
|
||||
if logger is None:
|
||||
logger = logging.getLogger("ImportIFC")
|
||||
settings.logger = logger
|
||||
settings.diff_file = scene_diff.diff_json_file
|
||||
settings.collection_mode = props.collection_mode
|
||||
|
||||
@@ -20,11 +20,14 @@ import bpy
|
||||
from . import ui, prop, operator
|
||||
|
||||
classes = (
|
||||
operator.AssignObject,
|
||||
operator.UnassignObject,
|
||||
operator.EnableEditingAggregate,
|
||||
operator.DisableEditingAggregate,
|
||||
operator.AddAggregate,
|
||||
operator.BIM_OT_assign_object,
|
||||
operator.BIM_OT_unassign_object,
|
||||
operator.BIM_OT_enable_editing_aggregate,
|
||||
operator.BIM_OT_disable_editing_aggregate,
|
||||
operator.BIM_OT_add_aggregate,
|
||||
operator.BIM_OT_select_parts,
|
||||
operator.BIM_OT_select_aggregate,
|
||||
operator.BIM_OT_add_part_to_object,
|
||||
prop.BIMObjectAggregateProperties,
|
||||
ui.BIM_PT_aggregate,
|
||||
)
|
||||
|
||||
@@ -32,24 +32,50 @@ class AggregateData:
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {
|
||||
"has_aggregate": cls.has_aggregate(),
|
||||
"label": cls.get_label(),
|
||||
"has_relating_object": cls.has_relating_object(),
|
||||
"relating_object_label": cls.get_relating_object_label(),
|
||||
"relating_object_id": cls.get_relating_object_id(),
|
||||
"has_related_objects": cls.has_related_objects(),
|
||||
"related_objects_amount": cls.get_related_objects_amount(),
|
||||
"ifc_class": cls.ifc_class(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def has_aggregate(cls):
|
||||
def get_relating_object(cls):
|
||||
return ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(bpy.context.active_object))
|
||||
|
||||
@classmethod
|
||||
def get_label(cls):
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(bpy.context.active_object))
|
||||
def has_relating_object(cls) -> bool:
|
||||
return cls.get_relating_object() is not None
|
||||
|
||||
@classmethod
|
||||
def get_relating_object_label(cls) -> str:
|
||||
aggregate = cls.get_relating_object()
|
||||
if aggregate:
|
||||
return f"{aggregate.is_a()}/{aggregate.Name or ''}"
|
||||
|
||||
@classmethod
|
||||
def get_relating_object_id(cls):
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(bpy.context.active_object))
|
||||
def get_relating_object_id(cls) -> int:
|
||||
aggregate = cls.get_relating_object()
|
||||
if aggregate:
|
||||
return aggregate.id()
|
||||
|
||||
@classmethod
|
||||
def get_related_objects(cls):
|
||||
return ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(bpy.context.active_object))
|
||||
|
||||
@classmethod
|
||||
def get_related_objects_amount(cls):
|
||||
parts = cls.get_related_objects()
|
||||
return len(parts) if parts else 0
|
||||
|
||||
@classmethod
|
||||
def has_related_objects(cls) -> bool:
|
||||
return bool(cls.get_related_objects())
|
||||
|
||||
@classmethod
|
||||
def ifc_class(cls) -> str:
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
if element:
|
||||
return element.is_a()
|
||||
|
||||
@@ -17,8 +17,11 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.element
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.aggregate as core
|
||||
import blenderbim.core.spatial
|
||||
import blenderbim.bim.handler
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
|
||||
@@ -30,7 +33,9 @@ class Operator:
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AssignObject(bpy.types.Operator, Operator):
|
||||
class BIM_OT_assign_object(bpy.types.Operator, Operator):
|
||||
"""Create aggregation relationship between two ifc elements"""
|
||||
|
||||
bl_idname = "bim.assign_object"
|
||||
bl_label = "Assign Object"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
@@ -47,7 +52,9 @@ class AssignObject(bpy.types.Operator, Operator):
|
||||
)
|
||||
|
||||
|
||||
class UnassignObject(bpy.types.Operator, Operator):
|
||||
class BIM_OT_unassign_object(bpy.types.Operator, Operator):
|
||||
"""Remove aggregation relationship between two ifc elements"""
|
||||
|
||||
bl_idname = "bim.unassign_object"
|
||||
bl_label = "Unassign Object"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
@@ -63,7 +70,9 @@ class UnassignObject(bpy.types.Operator, Operator):
|
||||
)
|
||||
|
||||
|
||||
class EnableEditingAggregate(bpy.types.Operator, Operator):
|
||||
class BIM_OT_enable_editing_aggregate(bpy.types.Operator, Operator):
|
||||
"""Enable editing aggregation relationship"""
|
||||
|
||||
bl_idname = "bim.enable_editing_aggregate"
|
||||
bl_label = "Enable Editing Aggregate"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
@@ -72,7 +81,9 @@ class EnableEditingAggregate(bpy.types.Operator, Operator):
|
||||
core.enable_editing_aggregate(tool.Aggregate, obj=context.active_object)
|
||||
|
||||
|
||||
class DisableEditingAggregate(bpy.types.Operator, Operator):
|
||||
class BIM_OT_disable_editing_aggregate(bpy.types.Operator, Operator):
|
||||
"""Disable editing aggregation relationship"""
|
||||
|
||||
bl_idname = "bim.disable_editing_aggregate"
|
||||
bl_label = "Disable Editing Aggregate"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
@@ -81,7 +92,9 @@ class DisableEditingAggregate(bpy.types.Operator, Operator):
|
||||
core.disable_editing_aggregate(tool.Aggregate, obj=context.active_object)
|
||||
|
||||
|
||||
class AddAggregate(bpy.types.Operator):
|
||||
class BIM_OT_add_aggregate(bpy.types.Operator):
|
||||
"""Add aggregate to IFC element"""
|
||||
|
||||
bl_idname = "bim.add_aggregate"
|
||||
bl_label = "Add Aggregate"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
@@ -92,10 +105,99 @@ class AddAggregate(bpy.types.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return {"FINISHED"}
|
||||
|
||||
aggregate = self.create_aggregate(context)
|
||||
tool.Collector.sync(obj)
|
||||
current_aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
current_container = ifcopenshell.util.element.get_container(element)
|
||||
if current_aggregate:
|
||||
core.assign_object(
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
tool.Collector,
|
||||
relating_obj=tool.Ifc.get_object(current_aggregate),
|
||||
related_obj=aggregate,
|
||||
)
|
||||
elif current_container:
|
||||
blenderbim.core.spatial.assign_container(
|
||||
tool.Ifc,
|
||||
tool.Collector,
|
||||
tool.Spatial,
|
||||
structure_obj=tool.Ifc.get_object(current_container),
|
||||
element_obj=aggregate,
|
||||
)
|
||||
core.assign_object(tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=aggregate, related_obj=obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
def create_aggregate(self, context):
|
||||
aggregate_collection = bpy.data.collections.new("IfcElementAssembly/Assembly")
|
||||
context.scene.collection.children.link(aggregate_collection)
|
||||
aggregate = bpy.data.objects.new("Assembly", None)
|
||||
aggregate_collection.objects.link(aggregate)
|
||||
bpy.ops.bim.assign_class(obj=aggregate.name, ifc_class="IfcElementAssembly")
|
||||
core.assign_object(tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=aggregate, related_obj=obj)
|
||||
return aggregate
|
||||
|
||||
|
||||
class BIM_OT_select_parts(bpy.types.Operator):
|
||||
"""Select Parts"""
|
||||
|
||||
bl_idname = "bim.select_parts"
|
||||
bl_label = "Select Parts"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
obj = bpy.data.objects.get(self.obj) or context.active_object
|
||||
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(obj))
|
||||
parts_objs = set(tool.Ifc.get_object(part) for part in parts)
|
||||
selectable_parts_objs = set(context.selectable_objects).intersection(parts_objs)
|
||||
for selectable_part_obj in selectable_parts_objs:
|
||||
selectable_part_obj.select_set(True)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_select_aggregate(bpy.types.Operator):
|
||||
"""Select Aggregate"""
|
||||
|
||||
bl_idname = "bim.select_aggregate"
|
||||
bl_label = "Select Aggregate"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
obj = bpy.data.objects.get(self.obj) or context.active_object
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(obj))
|
||||
aggregate_obj = tool.Ifc.get_object(aggregate)
|
||||
if aggregate_obj in context.selectable_objects:
|
||||
aggregate_obj.select_set(True)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_add_part_to_object(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.add_part_to_object"
|
||||
bl_label = "Add Part to Object"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
part_class: bpy.props.StringProperty(name="Class", options={"HIDDEN"})
|
||||
part_name: bpy.props.StringProperty(name="Name")
|
||||
obj: bpy.props.StringProperty(options={"HIDDEN"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.part_name = "My " + self.part_class.lstrip("Ifc")
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def _execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj) or context.active_object
|
||||
core.add_part_to_object(
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
tool.Collector,
|
||||
tool.Blender,
|
||||
obj=obj,
|
||||
part_class=self.part_class,
|
||||
part_name=self.part_name,
|
||||
)
|
||||
|
||||
@@ -32,6 +32,17 @@ from bpy.props import (
|
||||
)
|
||||
|
||||
|
||||
def update_relating_object(self, context):
|
||||
def message(self, context):
|
||||
self.layout.label(text="Please select a valid Ifc Element")
|
||||
|
||||
if self.relating_object is None:
|
||||
return
|
||||
if not self.relating_object.BIMObjectProperties.ifc_definition_id:
|
||||
context.window_manager.popup_menu(message, title="Invalid Element Selected", icon="INFO")
|
||||
self.relating_object = None
|
||||
|
||||
|
||||
class BIMObjectAggregateProperties(PropertyGroup):
|
||||
is_editing: BoolProperty(name="Is Editing")
|
||||
relating_object: PointerProperty(name="Aggregate", type=bpy.types.Object)
|
||||
relating_object: PointerProperty(name="Aggregate", type=bpy.types.Object, update=update_relating_object)
|
||||
|
||||
@@ -28,6 +28,7 @@ class BIM_PT_aggregate(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "object"
|
||||
bl_parent_id = "BIM_PT_object_metadata"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -43,13 +44,14 @@ class BIM_PT_aggregate(Panel):
|
||||
return True
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
if not AggregateData.is_loaded:
|
||||
AggregateData.load()
|
||||
|
||||
props = context.active_object.BIMObjectAggregateProperties
|
||||
|
||||
if props.is_editing:
|
||||
row = self.layout.row(align=True)
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "relating_object", text="")
|
||||
if props.relating_object:
|
||||
op = row.operator("bim.assign_object", icon="CHECKMARK", text="")
|
||||
@@ -57,15 +59,37 @@ class BIM_PT_aggregate(Panel):
|
||||
op.related_object = context.active_object.BIMObjectProperties.ifc_definition_id
|
||||
row.operator("bim.disable_editing_aggregate", icon="CANCEL", text="")
|
||||
else:
|
||||
row = self.layout.row(align=True)
|
||||
if AggregateData.data["has_aggregate"]:
|
||||
row.label(text=AggregateData.data["label"])
|
||||
row = layout.row(align=True)
|
||||
if AggregateData.data["has_relating_object"]:
|
||||
row.label(text=AggregateData.data["relating_object_label"], icon="TRIA_UP")
|
||||
op = row.operator("bim.select_aggregate", icon="RESTRICT_SELECT_OFF", text="")
|
||||
op.obj = context.active_object.name
|
||||
row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="")
|
||||
row.operator("bim.add_aggregate", icon="ADD", text="")
|
||||
op = row.operator("bim.unassign_object", icon="X", text="")
|
||||
op.relating_object = AggregateData.data["relating_object_id"]
|
||||
op.related_object = context.active_object.BIMObjectProperties.ifc_definition_id
|
||||
else:
|
||||
row.label(text="No Aggregate Found")
|
||||
row.label(text="No Aggregate", icon="TRIA_UP")
|
||||
row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="")
|
||||
row.operator("bim.add_aggregate", icon="ADD", text="")
|
||||
|
||||
row = layout.row(align=True)
|
||||
parts = AggregateData.data["related_objects_amount"]
|
||||
row.label(text=f"{parts or 'No'} Part{'s' if parts > 1 else ''}", icon="TRIA_DOWN")
|
||||
if AggregateData.data["has_related_objects"]:
|
||||
op = row.operator("bim.select_parts", icon="RESTRICT_SELECT_OFF", text="")
|
||||
op.obj = context.active_object.name
|
||||
|
||||
ifc_class = AggregateData.data["ifc_class"]
|
||||
part_class = ""
|
||||
if ifc_class == "IfcBuilding":
|
||||
part_class = "IfcBuildingStorey"
|
||||
elif ifc_class == "IfcSite":
|
||||
part_class = "IfcBuilding"
|
||||
elif ifc_class == "IfcProject":
|
||||
part_class = "IfcSite"
|
||||
if part_class != "":
|
||||
op = layout.operator("bim.add_part_to_object", text="Add " + part_class.lstrip("Ifc"))
|
||||
op.part_class = part_class
|
||||
op.obj = context.active_object.name
|
||||
|
||||
@@ -59,6 +59,7 @@ class BIM_PT_object_attributes(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "object"
|
||||
bl_parent_id = "BIM_PT_object_metadata"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
|
||||
@@ -26,6 +26,7 @@ class BIM_PT_augin(bpy.types.Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_integrations"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
@@ -842,6 +842,8 @@ class ActivateBcfViewpoint(bpy.types.Operator):
|
||||
obj.data.angle = 2 * atan(
|
||||
(0.5 * cam_height) / (0.5 * cam_width / tan(radians(camera.field_of_view) / 2))
|
||||
)
|
||||
else:
|
||||
return
|
||||
|
||||
z_axis = Vector(
|
||||
(-camera.camera_direction.x, -camera.camera_direction.y, -camera.camera_direction.z)
|
||||
|
||||
@@ -29,6 +29,7 @@ class BIM_PT_bcf(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_collaboration"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from blenderbim.bim.prop import StrProperty
|
||||
from blenderbim.bim.module.root.prop import getIfcClasses
|
||||
from blenderbim.bim.module.root.prop import get_ifc_classes
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
@@ -33,14 +33,11 @@ from bpy.props import (
|
||||
)
|
||||
|
||||
scenarios_enum = []
|
||||
classes_enum = []
|
||||
|
||||
|
||||
def purge():
|
||||
global scenarios_enum
|
||||
global classes_enum
|
||||
scenarios_enum = []
|
||||
classes_enum = []
|
||||
|
||||
|
||||
def getScenarios(self, context):
|
||||
@@ -68,7 +65,7 @@ class BimTesterProperties(PropertyGroup):
|
||||
feature: StringProperty(default="", name="Feature / IDS", update=refreshScenarios)
|
||||
steps: StringProperty(default="", name="Custom Steps")
|
||||
ifc_file: StringProperty(default="", name="IFC File")
|
||||
audit_ifc_class: EnumProperty(items=getIfcClasses, name="Audit Class")
|
||||
audit_ifc_class: EnumProperty(items=get_ifc_classes, name="Audit Class")
|
||||
qa_reject_element_reason: StringProperty(name="Element Rejection Reason")
|
||||
scenario: EnumProperty(items=getScenarios, name="Scenario")
|
||||
should_load_from_memory: BoolProperty(default=False, name="Load from Memory")
|
||||
|
||||
@@ -27,6 +27,7 @@ class BIM_PT_qa(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_quality_control"
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.use_property_split = True
|
||||
|
||||
@@ -17,14 +17,28 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from . import ui
|
||||
from . import ui, operator, prop
|
||||
|
||||
classes = (ui.BIM_PT_boundary,)
|
||||
classes = (
|
||||
operator.LoadProjectSpaceBoundaries,
|
||||
operator.LoadSpaceBoundaries,
|
||||
operator.LoadBoundary,
|
||||
operator.SelectProjectBoundaries,
|
||||
operator.ColourByRelatedBuildingElement,
|
||||
operator.EnableEditingBoundary,
|
||||
operator.DisableEditingBoundary,
|
||||
operator.EditBoundaryAttributes,
|
||||
operator.UpdateBoundaryGeometry,
|
||||
ui.BIM_PT_Boundary,
|
||||
ui.BIM_PT_SpaceBoundaries,
|
||||
ui.BIM_PT_SceneBoundaries,
|
||||
prop.BIMBoundaryProperties,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
pass
|
||||
bpy.types.Object.bim_boundary_properties = bpy.props.PointerProperty(type=prop.BIMBoundaryProperties)
|
||||
|
||||
|
||||
def unregister():
|
||||
pass
|
||||
del bpy.types.Object.bim_boundary_properties
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021, 2022 Cyril Waechter <cyril@biminsight.ch>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import logging
|
||||
import ifcopenshell.api
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from ifcopenshell.api.boundary.data import Data
|
||||
import blenderbim.bim.import_ifc as import_ifc
|
||||
|
||||
|
||||
def get_boundaries_collection(blender_space):
|
||||
space_collection = bpy.data.collections.get(blender_space.name)
|
||||
collection_name = f"Boundaries/{blender_space.BIMObjectProperties.ifc_definition_id}"
|
||||
boundaries_collection = space_collection.children.get(collection_name)
|
||||
if not boundaries_collection:
|
||||
boundaries_collection = bpy.data.collections.new(collection_name)
|
||||
space_collection.children.link(boundaries_collection)
|
||||
return boundaries_collection
|
||||
|
||||
|
||||
class Loader:
|
||||
def __init__(self):
|
||||
self.ifc_file = None
|
||||
self.logger = None
|
||||
self.ifc_importer = None
|
||||
self.settings = None
|
||||
self.load_settings()
|
||||
self.load_importer()
|
||||
|
||||
def create_mesh(self, boundary):
|
||||
# ConnectionGeometry is optional in IFC schema for some reasons.
|
||||
if not boundary.ConnectionGeometry:
|
||||
return None
|
||||
surface = boundary.ConnectionGeometry.SurfaceOnRelatingElement
|
||||
# workaround for unvalid geometry provided by Revit. See https://github.com/IfcOpenShell/IfcOpenShell/issues/635#issuecomment-770366838
|
||||
if surface.is_a("IfcCurveBoundedPlane") and not getattr(surface, "InnerBoundaries", None):
|
||||
surface.InnerBoundaries = ()
|
||||
shape = ifcopenshell.geom.create_shape(self.settings, surface)
|
||||
mesh = self.ifc_importer.create_mesh(None, shape)
|
||||
self.ifc_importer.link_mesh(shape, mesh)
|
||||
return mesh
|
||||
|
||||
def load_settings(self):
|
||||
self.settings = ifcopenshell.geom.settings()
|
||||
self.settings.set(self.settings.EXCLUDE_SOLIDS_AND_SURFACES, False)
|
||||
self.settings.set(self.settings.USE_BREP_DATA, False)
|
||||
|
||||
def load_importer(self):
|
||||
self.ifc_file = tool.Ifc.get()
|
||||
self.logger = logging.getLogger("ImportIFC")
|
||||
ifc_import_settings = import_ifc.IfcImportSettings.factory(bpy.context, IfcStore.path, self.logger)
|
||||
self.ifc_importer = import_ifc.IfcImporter(ifc_import_settings)
|
||||
self.ifc_importer.file = self.ifc_file
|
||||
|
||||
def load_boundary(self, boundary_id, blender_space):
|
||||
boundary = self.ifc_file.by_id(boundary_id)
|
||||
obj = tool.Ifc.get_object(boundary)
|
||||
if obj:
|
||||
return obj
|
||||
mesh = self.create_mesh(boundary)
|
||||
obj = bpy.data.objects.new(f"{boundary.is_a()}/{boundary.Name}", mesh)
|
||||
obj.matrix_world = blender_space.matrix_world
|
||||
boundaries_collection = get_boundaries_collection(blender_space)
|
||||
boundaries_collection.objects.link(obj)
|
||||
tool.Ifc.link(boundary, obj)
|
||||
return obj
|
||||
|
||||
|
||||
class LoadProjectSpaceBoundaries(bpy.types.Operator):
|
||||
bl_idname = "bim.load_project_space_boundaries"
|
||||
bl_label = "Load all project space boundaries"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
loader = Loader()
|
||||
if not Data.is_loaded:
|
||||
Data.load(tool.Ifc.get())
|
||||
for space_id, boundaries_id in Data.spaces.items():
|
||||
blender_space = tool.Ifc.get_object(tool.Ifc.get().by_id(space_id))
|
||||
for boundary_id in boundaries_id:
|
||||
loader.load_boundary(boundary_id, blender_space)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class LoadBoundary(bpy.types.Operator):
|
||||
bl_idname = "bim.load_boundary"
|
||||
bl_label = "Load boundary"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
boundary_id: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
loader = Loader()
|
||||
blender_space = context.active_object
|
||||
for obj in context.visible_objects:
|
||||
obj.select_set(False)
|
||||
obj = loader.load_boundary(self.boundary_id, blender_space)
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class LoadSpaceBoundaries(bpy.types.Operator):
|
||||
bl_idname = "bim.load_space_boundaries"
|
||||
bl_label = "Load selected space boundaries"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
blender_space = context.active_object
|
||||
oprops = context.active_object.BIMObjectProperties
|
||||
loader = Loader()
|
||||
if not Data.is_loaded:
|
||||
Data.load(tool.Ifc.get())
|
||||
for boundary_id in Data.spaces.get(oprops.ifc_definition_id, []):
|
||||
loader.load_boundary(boundary_id, blender_space)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectProjectBoundaries(bpy.types.Operator):
|
||||
bl_idname = "bim.select_project_space_boundaries"
|
||||
bl_label = "Select all project space boundaries"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
for obj in context.visible_objects:
|
||||
obj.select_set(False)
|
||||
ifc_file = tool.Ifc.get()
|
||||
for boundary_id in Data.boundaries:
|
||||
boundary = ifc_file.by_id(boundary_id)
|
||||
obj = tool.Ifc.get_object(boundary)
|
||||
if obj:
|
||||
obj.select_set(True)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def get_colour(ifc_boundary):
|
||||
"""Return a color depending on IfcClass given"""
|
||||
product_colors = {
|
||||
"IfcWall": (0.7, 0.3, 0, 1),
|
||||
"IfcWindow": (0, 0.7, 1, 1),
|
||||
"IfcSlab": (0.7, 0.7, 0.5, 1),
|
||||
"IfcRoof": (0, 0.3, 0, 1),
|
||||
"IfcDoor": (1, 1, 1, 1),
|
||||
}
|
||||
if ifc_boundary.PhysicalOrVirtualBoundary == "VIRTUAL":
|
||||
return (1, 0, 1, 1)
|
||||
|
||||
element = ifc_boundary.RelatedBuildingElement
|
||||
if not element:
|
||||
return (1, 0, 0, 1)
|
||||
for product, colour in product_colors.items():
|
||||
if element.is_a(product):
|
||||
return colour
|
||||
return (0, 0, 0, 1)
|
||||
|
||||
|
||||
class ColourByRelatedBuildingElement(bpy.types.Operator):
|
||||
bl_idname = "bim.colour_by_related_building_element"
|
||||
bl_label = "Apply colour based on related building elements"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
IfcStore.begin_transaction(self)
|
||||
self.store_state(context)
|
||||
result = self._execute(context)
|
||||
IfcStore.add_transaction_operation(self)
|
||||
IfcStore.end_transaction(self)
|
||||
return result
|
||||
|
||||
def _execute(self, context):
|
||||
self.file = tool.Ifc.get()
|
||||
for obj in context.visible_objects:
|
||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
||||
continue
|
||||
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
if not element.is_a("IfcRelSpaceBoundary"):
|
||||
continue
|
||||
obj.color = get_colour(element)
|
||||
areas = [a for a in context.screen.areas if a.type == "VIEW_3D"]
|
||||
if areas:
|
||||
areas[0].spaces[0].shading.color_type = "OBJECT"
|
||||
return {"FINISHED"}
|
||||
|
||||
def store_state(self, context):
|
||||
areas = [a for a in context.screen.areas if a.type == "VIEW_3D"]
|
||||
if areas:
|
||||
self.transaction_data = {"area": areas[0], "color_type": areas[0].spaces[0].shading.color_type}
|
||||
|
||||
def rollback(self, data):
|
||||
if data:
|
||||
data["area"].spaces[0].shading.color_type = data["color_type"]
|
||||
|
||||
def commit(self, data):
|
||||
if data:
|
||||
data["area"].spaces[0].shading.color_type = "OBJECT"
|
||||
|
||||
|
||||
EDITABLE_ATTRIBUTES = {
|
||||
"RelatingSpace": "relating_space",
|
||||
"RelatedBuildingElement": "related_building_element",
|
||||
"ParentBoundary": "parent_boundary",
|
||||
"CorrespondingBoundary": "corresponding_boundary",
|
||||
}
|
||||
|
||||
|
||||
class EnableEditingBoundary(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_boundary"
|
||||
bl_label = "Edit boundary relations"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
bprops = context.active_object.bim_boundary_properties
|
||||
bprops.is_editing = True
|
||||
boundary = tool.Ifc.get_entity(context.active_object)
|
||||
for ifc_attribute, blender_property in EDITABLE_ATTRIBUTES.items():
|
||||
entity = getattr(boundary, ifc_attribute, None)
|
||||
if not entity:
|
||||
continue
|
||||
obj = tool.Ifc.get_object(entity)
|
||||
if entity and obj:
|
||||
setattr(bprops, blender_property, obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisableEditingBoundary(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_editing_boundary"
|
||||
bl_label = "Disable editing boundary relations"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
bprops = context.active_object.bim_boundary_properties
|
||||
bprops.is_editing = False
|
||||
for ifc_attribute, blender_property in EDITABLE_ATTRIBUTES.items():
|
||||
setattr(bprops, blender_property, None)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EditBoundaryAttributes(bpy.types.Operator):
|
||||
bl_idname = "bim.edit_boundary_attributes"
|
||||
bl_label = "Disable editing boundary relations"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
bprops = context.active_object.bim_boundary_properties
|
||||
boundary = tool.Ifc.get_entity(context.active_object)
|
||||
attributes = dict()
|
||||
for ifc_attribute, blender_property in EDITABLE_ATTRIBUTES.items():
|
||||
obj = getattr(bprops, blender_property, None)
|
||||
entity = tool.Ifc.get_entity(obj)
|
||||
attributes[blender_property] = entity
|
||||
ifcopenshell.api.run("boundary.edit_attributes", tool.Ifc.get(), entity=boundary, **attributes)
|
||||
bpy.ops.bim.disable_editing_boundary()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class UpdateBoundaryGeometry(bpy.types.Operator):
|
||||
bl_idname = "bim.update_boundary_geometry"
|
||||
bl_label = "Update boundary geometry"
|
||||
bl_description = """
|
||||
Update boundary connection geometry from mesh.
|
||||
Mesh must lie on a single plane. It should look like a face or a face with holes.
|
||||
"""
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
settings = tool.Boundary.get_assign_connection_geometry_settings(context.active_object)
|
||||
ifcopenshell.api.run("boundary.assign_connection_geometry", tool.Ifc.get(), **settings)
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,60 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
StringProperty,
|
||||
EnumProperty,
|
||||
BoolProperty,
|
||||
IntProperty,
|
||||
FloatProperty,
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
import blenderbim.tool as tool
|
||||
|
||||
|
||||
def space_filter(self, object):
|
||||
entity = tool.Ifc.get_entity(object)
|
||||
if entity:
|
||||
return entity.is_a("IfcSpace") or entity.is_a("IfcExternalSpatialElement")
|
||||
return False
|
||||
|
||||
|
||||
def boundary_filter(self, object):
|
||||
entity = tool.Ifc.get_entity(object)
|
||||
if entity:
|
||||
return entity.is_a("IfcRelSpaceBoundary")
|
||||
return False
|
||||
|
||||
|
||||
def element_filter(self, object):
|
||||
entity = tool.Ifc.get_entity(object)
|
||||
if entity:
|
||||
return entity.is_a("IfcElement")
|
||||
return False
|
||||
|
||||
|
||||
class BIMBoundaryProperties(PropertyGroup):
|
||||
is_editing: BoolProperty(name="Is Editing")
|
||||
relating_space: PointerProperty(name="RelatingSpace", type=bpy.types.Object, poll=space_filter)
|
||||
related_building_element: PointerProperty(name="RelatedBuildingElement", type=bpy.types.Object, poll=element_filter)
|
||||
parent_boundary: PointerProperty(name="ParentBoundary", type=bpy.types.Object, poll=boundary_filter)
|
||||
corresponding_boundary: PointerProperty(name="CorrespondingBoundary", type=bpy.types.Object, poll=boundary_filter)
|
||||
@@ -20,16 +20,37 @@ import bpy
|
||||
import blenderbim.bim.helper
|
||||
from bpy.types import Panel, UIList
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
import blenderbim.tool as tool
|
||||
from ifcopenshell.api.boundary.data import Data
|
||||
|
||||
|
||||
class BIM_PT_boundary(Panel):
|
||||
class BIM_PT_SceneBoundaries(Panel):
|
||||
bl_label = "IFC Space Boundaries"
|
||||
bl_idname = "BIM_PT_boundary"
|
||||
bl_id_name = "BIM_PT_scene_boundaries"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_geometry"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return IfcStore.get_file()
|
||||
|
||||
def draw(self, context):
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.load_project_space_boundaries")
|
||||
row.operator("bim.select_project_space_boundaries", text="", icon="RESTRICT_SELECT_OFF")
|
||||
row.operator("bim.colour_by_related_building_element", text="", icon="BRUSH_DATA")
|
||||
|
||||
|
||||
class BIM_PT_Boundary(Panel):
|
||||
bl_label = "IFC Space Boundary"
|
||||
bl_idname = "BIM_PT_Boundary"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "object"
|
||||
bl_parent_id = "BIM_PT_geometry_object"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -40,15 +61,97 @@ class BIM_PT_boundary(Panel):
|
||||
return False
|
||||
if not IfcStore.get_element(props.ifc_definition_id):
|
||||
return False
|
||||
if IfcStore.get_file().by_id(props.ifc_definition_id).is_a() not in ["IfcSpace", "IfcExternalSpatialElement"]:
|
||||
return False
|
||||
return True
|
||||
entity = IfcStore.get_file().by_id(props.ifc_definition_id)
|
||||
return entity.is_a("IfcRelSpaceBoundary")
|
||||
|
||||
def draw(self, context):
|
||||
self.oprops = context.active_object.BIMObjectProperties
|
||||
if not Data.is_loaded:
|
||||
Data.load(IfcStore.get_file())
|
||||
for boundary_id in Data.spaces.get(self.oprops.ifc_definition_id, []):
|
||||
boundary = Data.boundaries[boundary_id]
|
||||
props = context.active_object.BIMObjectProperties
|
||||
ifc_file = tool.Ifc.get()
|
||||
element = ifc_file.by_id(props.ifc_definition_id)
|
||||
boundary = ifc_file.by_id(props.ifc_definition_id)
|
||||
self.bprops = context.active_object.bim_boundary_properties
|
||||
if self.bprops.is_editing:
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.edit_boundary_attributes", icon="CHECKMARK", text="Save Attributes")
|
||||
row.operator("bim.disable_editing_boundary", icon="CANCEL", text="")
|
||||
self.draw_relation_editor(boundary, "RelatingSpace", "relating_space")
|
||||
self.draw_relation_editor(boundary, "RelatedBuildingElement", "related_building_element")
|
||||
self.draw_relation_editor(boundary, "ParentBoundary", "parent_boundary")
|
||||
self.draw_relation_editor(boundary, "CorrespondingBoundary", "corresponding_boundary")
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.label(text=f"{boundary_id}", icon="GHOST_ENABLED")
|
||||
row.operator("bim.enable_editing_boundary", icon="GREASEPENCIL", text="Edit")
|
||||
self.draw_relation_data(boundary, "RelatingSpace")
|
||||
self.draw_relation_data(boundary, "RelatedBuildingElement")
|
||||
self.draw_relation_data(boundary, "ParentBoundary")
|
||||
self.draw_relation_data(boundary, "CorrespondingBoundary")
|
||||
if hasattr(boundary, "InnerBoundaries"):
|
||||
for i, inner_boundary in enumerate(getattr(boundary, "InnerBoundaries", ())):
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="InnerBoundaries")
|
||||
row.label(text=f"[{i}]")
|
||||
row.label(text=f"{inner_boundary.is_a()}/{inner_boundary.Name}")
|
||||
row = self.layout.row()
|
||||
row.operator("bim.update_boundary_geometry")
|
||||
|
||||
def draw_relation_data(self, boundary, ifc_attribute: str):
|
||||
row = self.layout.row(align=True)
|
||||
if hasattr(boundary, ifc_attribute):
|
||||
row.label(text=ifc_attribute)
|
||||
entity = getattr(boundary, ifc_attribute)
|
||||
if entity:
|
||||
row.label(text=f"{entity.is_a()}/{entity.Name}")
|
||||
op = row.operator("bim.select_global_id", text="", icon="TRACKER")
|
||||
op.global_id = entity.GlobalId
|
||||
else:
|
||||
row.label(text="")
|
||||
|
||||
def draw_relation_editor(self, boundary, ifc_attribute: str, blender_property: str):
|
||||
if hasattr(boundary, ifc_attribute):
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.bprops, blender_property)
|
||||
|
||||
|
||||
class BIM_PT_SpaceBoundaries(Panel):
|
||||
bl_label = "IFC Space Boundaries"
|
||||
bl_idname = "BIM_PT_SpaceBoundaries"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "object"
|
||||
bl_parent_id = "BIM_PT_geometry_object"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
return False
|
||||
props = context.active_object.BIMObjectProperties
|
||||
if not props.ifc_definition_id:
|
||||
return False
|
||||
if not IfcStore.get_element(props.ifc_definition_id):
|
||||
return False
|
||||
valid_classes = ("IfcSpace", "IfcExternalSpatialElement")
|
||||
entity = IfcStore.get_file().by_id(props.ifc_definition_id)
|
||||
for ifc_class in valid_classes:
|
||||
if entity.is_a(ifc_class):
|
||||
return True
|
||||
return False
|
||||
|
||||
def draw(self, context):
|
||||
self.props = context.active_object.BIMObjectProperties
|
||||
self.ifc_file = tool.Ifc.get()
|
||||
row = self.layout.row()
|
||||
row.operator("bim.load_space_boundaries")
|
||||
if not Data.is_loaded:
|
||||
Data.load(self.ifc_file)
|
||||
for boundary_id in Data.spaces.get(self.props.ifc_definition_id, []):
|
||||
boundary_data = Data.boundaries[boundary_id]
|
||||
building_element = self.ifc_file.by_id(boundary_data["RelatedBuildingElement"])
|
||||
row = self.layout.row()
|
||||
if building_element:
|
||||
bld_el_description = f"{building_element.is_a()}/{building_element.Name}"
|
||||
else:
|
||||
bld_el_description = None
|
||||
row.label(text=f"{boundary_id} > {bld_el_description}", icon="GHOST_ENABLED")
|
||||
op = row.operator("bim.load_boundary", text="", icon="RESTRICT_SELECT_OFF")
|
||||
op.boundary_id = boundary_id
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from . import ui, prop, operator
|
||||
|
||||
classes = (
|
||||
operator.AddBrick,
|
||||
operator.AddBrickFeed,
|
||||
operator.AssignBrickReference,
|
||||
operator.CloseBrickProject,
|
||||
operator.ConvertBrickProject,
|
||||
operator.ConvertIfcToBrick,
|
||||
operator.LoadBrickProject,
|
||||
operator.NewBrickFile,
|
||||
operator.RefreshBrickViewer,
|
||||
operator.RemoveBrick,
|
||||
operator.RewindBrickClass,
|
||||
operator.ViewBrickClass,
|
||||
operator.ViewBrickItem,
|
||||
prop.Brick,
|
||||
prop.BIMBrickProperties,
|
||||
ui.BIM_PT_brickschema,
|
||||
ui.BIM_PT_ifc_brickschema_references,
|
||||
ui.BIM_UL_bricks,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.Scene.BIMBrickProperties = bpy.props.PointerProperty(type=prop.BIMBrickProperties)
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.Scene.BIMBrickProperties
|
||||
@@ -0,0 +1,180 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.tool.brick import BrickStore
|
||||
|
||||
try:
|
||||
from rdflib import URIRef, BNode
|
||||
except:
|
||||
# See #1860
|
||||
print("Warning: brickschema not available.")
|
||||
|
||||
|
||||
def refresh():
|
||||
BrickschemaData.is_loaded = False
|
||||
BrickschemaReferencesData.is_loaded = False
|
||||
|
||||
|
||||
class BrickschemaData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.data = {
|
||||
"is_loaded": cls.get_is_loaded(),
|
||||
"attributes": cls.attributes(),
|
||||
"namespaces": cls.namespaces(),
|
||||
"brick_equipment_classes": cls.brick_equipment_classes(),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_is_loaded(cls):
|
||||
return BrickStore.graph is not None
|
||||
|
||||
@classmethod
|
||||
def attributes(cls):
|
||||
if BrickStore.graph is None:
|
||||
return []
|
||||
props = bpy.context.scene.BIMBrickProperties
|
||||
try:
|
||||
brick = props.bricks[props.active_brick_index]
|
||||
except:
|
||||
return []
|
||||
results = []
|
||||
uri = brick.uri
|
||||
query = BrickStore.graph.query(
|
||||
"""
|
||||
PREFIX brick: <https://brickschema.org/schema/Brick#>
|
||||
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
||||
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
|
||||
SELECT DISTINCT ?name ?value ?sp ?sv WHERE {
|
||||
<{uri}> ?name ?value .
|
||||
OPTIONAL {
|
||||
{ ?name rdfs:range brick:TimeseriesReference . }
|
||||
UNION
|
||||
{ ?name a brick:EntityProperty . }
|
||||
?value ?sp ?sv }
|
||||
}
|
||||
""".replace(
|
||||
"{uri}", uri
|
||||
)
|
||||
)
|
||||
|
||||
for row in query:
|
||||
name = row.get("name").toPython().split("#")[-1]
|
||||
value = row.get("value")
|
||||
results.append(
|
||||
{
|
||||
"name": name,
|
||||
"value": value.toPython().split("#")[-1],
|
||||
"is_uri": isinstance(value, URIRef),
|
||||
"value_uri": value.toPython(),
|
||||
"is_globalid": name == "globalID",
|
||||
}
|
||||
)
|
||||
if isinstance(row.get("value"), BNode):
|
||||
for s, p, o in BrickStore.graph.triples((value, None, None)):
|
||||
results.append(
|
||||
{
|
||||
"name": name + ":" + p.toPython().split("#")[-1],
|
||||
"value": o.toPython().split("#")[-1],
|
||||
"is_uri": isinstance(o, URIRef),
|
||||
"value_uri": o.toPython(),
|
||||
"is_globalid": p.toPython().split("#")[-1] == "globalID",
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def namespaces(cls):
|
||||
if BrickStore.graph is None:
|
||||
return []
|
||||
results = []
|
||||
for alias, uri in BrickStore.graph.namespaces():
|
||||
results.append((uri, f"{alias}: {uri}", ""))
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def brick_equipment_classes(cls):
|
||||
if BrickStore.graph is None:
|
||||
return []
|
||||
results = []
|
||||
query = BrickStore.graph.query(
|
||||
"""
|
||||
PREFIX brick: <https://brickschema.org/schema/Brick#>
|
||||
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
||||
SELECT ?class WHERE {
|
||||
?class rdfs:subClassOf* brick:Equipment .
|
||||
}
|
||||
"""
|
||||
)
|
||||
for uri in sorted([x[0].toPython() for x in query]):
|
||||
results.append((uri, uri.split("#")[-1], ""))
|
||||
return results
|
||||
|
||||
|
||||
class BrickschemaReferencesData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.data = {"is_loaded": cls.get_is_loaded(), "libraries": cls.libraries(), "references": cls.references()}
|
||||
|
||||
@classmethod
|
||||
def get_is_loaded(cls):
|
||||
return BrickStore.graph is not None
|
||||
|
||||
@classmethod
|
||||
def libraries(cls):
|
||||
results = []
|
||||
ifc = tool.Ifc.get()
|
||||
if not ifc:
|
||||
return results
|
||||
for library in ifc.by_type("IfcLibraryInformation"):
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
results.append((str(library.id()), library.Name or "Unnamed", ""))
|
||||
elif ".ttl" in library.Location:
|
||||
results.append((str(library.id()), library.Name or "Unnamed", ""))
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def references(cls):
|
||||
results = []
|
||||
for rel in getattr(tool.Ifc.get_entity(bpy.context.active_object), "HasAssociations", []):
|
||||
if rel.is_a("IfcRelAssociatesLibrary"):
|
||||
reference = rel.RelatingLibrary
|
||||
if tool.Ifc.get_schema() == "IFC2X3" and "#" not in reference.ItemReference:
|
||||
continue
|
||||
if tool.Ifc.get_schema() != "IFC2X3" and "#" not in reference.Identification:
|
||||
continue
|
||||
results.append(
|
||||
{
|
||||
"id": reference.id(),
|
||||
"identification": reference.ItemReference
|
||||
if tool.Ifc.get_schema() == "IFC2X3"
|
||||
else reference.Identification,
|
||||
"name": reference.Name or "Unnamed",
|
||||
}
|
||||
)
|
||||
return results
|
||||
@@ -0,0 +1,191 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import ifcopenshell.api
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.brick as core
|
||||
import blenderbim.bim.handler
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
|
||||
|
||||
class Operator:
|
||||
def execute(self, context):
|
||||
self._execute(context)
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class LoadBrickProject(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.load_brick_project"
|
||||
bl_label = "Load Brickschema Project"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.ttl", options={"HIDDEN"})
|
||||
|
||||
def _execute(self, context):
|
||||
core.load_brick_project(tool.Brick, filepath=self.filepath)
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class ViewBrickClass(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.view_brick_class"
|
||||
bl_label = "View Brick Class"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
brick_class: bpy.props.StringProperty(name="Brick Class")
|
||||
|
||||
def _execute(self, context):
|
||||
core.view_brick_class(tool.Brick, brick_class=self.brick_class)
|
||||
|
||||
|
||||
class ViewBrickItem(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.view_brick_item"
|
||||
bl_label = "View Brick Item"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
item: bpy.props.StringProperty(name="Brick Item")
|
||||
|
||||
def _execute(self, context):
|
||||
core.view_brick_item(tool.Brick, item=self.item)
|
||||
|
||||
|
||||
class RewindBrickClass(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.rewind_brick_class"
|
||||
bl_label = "Rewind Brick Class"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
core.rewind_brick_class(tool.Brick)
|
||||
|
||||
|
||||
class CloseBrickProject(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.close_brick_project"
|
||||
bl_label = "Close Brick Project"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
core.close_brick_project(tool.Brick)
|
||||
|
||||
|
||||
class ConvertBrickProject(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.convert_brick_project"
|
||||
bl_label = "Convert Brick Project"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
core.convert_brick_project(tool.Ifc, tool.Brick)
|
||||
|
||||
|
||||
class AssignBrickReference(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.assign_brick_reference"
|
||||
bl_label = "Assign Brick Reference"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMBrickProperties
|
||||
core.assign_brick_reference(
|
||||
tool.Ifc,
|
||||
tool.Brick,
|
||||
element=tool.Ifc.get_entity(context.active_object),
|
||||
library=tool.Ifc.get().by_id(int(props.libraries)),
|
||||
brick_uri=props.bricks[props.active_brick_index].uri,
|
||||
)
|
||||
|
||||
|
||||
class AddBrick(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.add_brick"
|
||||
bl_label = "Add Brick"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMBrickProperties
|
||||
library = None
|
||||
if props.libraries:
|
||||
library = tool.Ifc.get().by_id(int(props.libraries))
|
||||
core.add_brick(
|
||||
tool.Ifc,
|
||||
tool.Brick,
|
||||
element=tool.Ifc.get_entity(context.active_object) if context.selected_objects else None,
|
||||
namespace=props.namespace,
|
||||
brick_class=props.brick_equipment_class,
|
||||
library=library,
|
||||
)
|
||||
|
||||
|
||||
class AddBrickFeed(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.add_brick_feed"
|
||||
bl_label = "Add Brick Feed"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
source = tool.Ifc.get_entity([o for o in context.selected_objects if o != context.active_object][0])
|
||||
destination = tool.Ifc.get_entity(context.active_object)
|
||||
core.add_brick_feed(
|
||||
tool.Ifc,
|
||||
tool.Brick,
|
||||
source=source,
|
||||
destination=destination,
|
||||
)
|
||||
|
||||
|
||||
class ConvertIfcToBrick(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.convert_ifc_to_brick"
|
||||
bl_label = "Convert IFC To Brick"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMBrickProperties
|
||||
library = None
|
||||
if props.libraries:
|
||||
library = tool.Ifc.get().by_id(int(props.libraries))
|
||||
core.convert_ifc_to_brick(tool.Brick, namespace=props.namespace, library=library)
|
||||
|
||||
|
||||
class NewBrickFile(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.new_brick_file"
|
||||
bl_label = "New Brick File"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
core.new_brick_file(tool.Brick)
|
||||
|
||||
|
||||
class RefreshBrickViewer(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.refresh_brick_viewer"
|
||||
bl_label = "Refresh Brick Viewer"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
core.refresh_brick_viewer(tool.Brick)
|
||||
|
||||
|
||||
class RemoveBrick(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.remove_brick"
|
||||
bl_label = "Remove Brick"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMBrickProperties
|
||||
core.remove_brick(
|
||||
tool.Ifc,
|
||||
tool.Brick,
|
||||
library=tool.Ifc.get().by_id(int(props.libraries)) if props.libraries else None,
|
||||
brick_uri=props.bricks[props.active_brick_index].uri,
|
||||
)
|
||||
@@ -0,0 +1,71 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from blenderbim.bim.module.brick.data import BrickschemaData, BrickschemaReferencesData
|
||||
from blenderbim.bim.prop import StrProperty, Attribute
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
StringProperty,
|
||||
EnumProperty,
|
||||
BoolProperty,
|
||||
IntProperty,
|
||||
FloatProperty,
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
|
||||
|
||||
def update_active_brick_index(self, context):
|
||||
BrickschemaData.is_loaded = False
|
||||
|
||||
|
||||
def get_libraries(self, context):
|
||||
if not BrickschemaReferencesData.is_loaded:
|
||||
BrickschemaReferencesData.load()
|
||||
return BrickschemaReferencesData.data["libraries"]
|
||||
|
||||
|
||||
def get_namespaces(self, context):
|
||||
if not BrickschemaData.is_loaded:
|
||||
BrickschemaData.load()
|
||||
return BrickschemaData.data["namespaces"]
|
||||
|
||||
|
||||
def get_brick_equipment_classes(self, context):
|
||||
if not BrickschemaData.is_loaded:
|
||||
BrickschemaData.load()
|
||||
return BrickschemaData.data["brick_equipment_classes"]
|
||||
|
||||
|
||||
class Brick(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
label: StringProperty(name="Label")
|
||||
uri: StringProperty(name="URI")
|
||||
total_items: IntProperty(name="Total Items")
|
||||
|
||||
|
||||
class BIMBrickProperties(PropertyGroup):
|
||||
active_brick_class: StringProperty(name="Active Brick Class")
|
||||
brick_breadcrumbs: CollectionProperty(name="Brick Breadcrumbs", type=StrProperty)
|
||||
bricks: CollectionProperty(name="Bricks", type=Brick)
|
||||
active_brick_index: IntProperty(name="Active Brick Index", update=update_active_brick_index)
|
||||
libraries: EnumProperty(name="Libraries", items=get_libraries)
|
||||
namespace: EnumProperty(name="Namespace", items=get_namespaces)
|
||||
brick_equipment_class: EnumProperty(name="Brick Equipment Class", items=get_brick_equipment_classes)
|
||||
@@ -0,0 +1,132 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import blenderbim.tool as tool
|
||||
from bpy.types import Panel, UIList
|
||||
from blenderbim.bim.module.brick.data import BrickschemaData, BrickschemaReferencesData
|
||||
|
||||
|
||||
class BIM_PT_brickschema(Panel):
|
||||
bl_label = "Brickschema Project"
|
||||
bl_idname = "BIM_PT_brickschema"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
|
||||
def draw(self, context):
|
||||
if not BrickschemaData.is_loaded:
|
||||
BrickschemaData.load()
|
||||
self.props = context.scene.BIMBrickProperties
|
||||
|
||||
if not BrickschemaData.data["is_loaded"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.new_brick_file", text="Create Project")
|
||||
row.operator("bim.load_brick_project", text="Load Project")
|
||||
return
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
if len(self.props.brick_breadcrumbs):
|
||||
row.operator("bim.rewind_brick_class", text="", icon="FRAME_PREV")
|
||||
row.label(text=self.props.active_brick_class)
|
||||
row.operator("bim.refresh_brick_viewer", text="", icon="FILE_REFRESH")
|
||||
row.operator("bim.close_brick_project", text="", icon="CANCEL")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "namespace", text="")
|
||||
row.prop(self.props, "brick_equipment_class", text="")
|
||||
row.operator("bim.add_brick", text="", icon="ADD")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
row.operator("bim.add_brick_feed", text="", icon="PLUGIN")
|
||||
row.operator("bim.remove_brick", text="", icon="X")
|
||||
|
||||
self.layout.template_list("BIM_UL_bricks", "", self.props, "bricks", self.props, "active_brick_index")
|
||||
|
||||
for attribute in BrickschemaData.data["attributes"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=attribute["name"])
|
||||
row.label(text=attribute["value"])
|
||||
if attribute["is_uri"]:
|
||||
op = row.operator("bim.view_brick_item", text="", icon="DISCLOSURE_TRI_RIGHT")
|
||||
op.item = attribute["value_uri"]
|
||||
if attribute["is_globalid"]:
|
||||
op = row.operator("bim.select_global_id", icon="RESTRICT_SELECT_OFF", text="")
|
||||
op.global_id = attribute["value"]
|
||||
|
||||
|
||||
class BIM_PT_ifc_brickschema_references(Panel):
|
||||
bl_label = "IFC Brickschema References"
|
||||
bl_idname = "BIM_PT_ifc_brickschema_references"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_brickschema"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Ifc.get()
|
||||
|
||||
def draw(self, context):
|
||||
if not BrickschemaReferencesData.is_loaded:
|
||||
BrickschemaReferencesData.load()
|
||||
self.props = context.scene.BIMBrickProperties
|
||||
|
||||
if not BrickschemaReferencesData.data["is_loaded"]:
|
||||
row = self.layout.row()
|
||||
row.label(text="No Brickschema Project Loaded")
|
||||
return
|
||||
|
||||
if not BrickschemaReferencesData.data["libraries"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="No IFC Libraries")
|
||||
row.operator("bim.convert_brick_project", text="", icon="ADD")
|
||||
return
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "libraries")
|
||||
row.operator("bim.convert_brick_project", text="", icon="ADD")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.assign_brick_reference", icon="ADD")
|
||||
row.operator("bim.convert_ifc_to_brick", icon="IMPORT")
|
||||
|
||||
if not BrickschemaReferencesData.data["references"]:
|
||||
row = self.layout.row()
|
||||
row.label(text="No References")
|
||||
|
||||
for reference in BrickschemaReferencesData.data["references"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=reference["identification"], icon="ASSET_MANAGER")
|
||||
row.label(text=reference["name"])
|
||||
row.operator("bim.unassign_library_reference", text="", icon="X").reference = reference["id"]
|
||||
row.operator("bim.view_brick_item", text="", icon="DISCLOSURE_TRI_RIGHT").item = reference["identification"]
|
||||
|
||||
|
||||
class BIM_UL_bricks(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
if item.total_items:
|
||||
op = row.operator("bim.view_brick_class", text="", icon="DISCLOSURE_TRI_RIGHT", emboss=False)
|
||||
op.brick_class = item.name
|
||||
row.label(text=item.label if item.label else item.name)
|
||||
if item.total_items:
|
||||
row.label(text=str(item.total_items))
|
||||
@@ -34,9 +34,6 @@ classes = (
|
||||
operator.SmartClashGroup,
|
||||
operator.SelectSmartGroup,
|
||||
operator.LoadSmartGroupsForActiveClashSet,
|
||||
operator.SetBlenderClashSetA,
|
||||
operator.SetBlenderClashSetB,
|
||||
operator.ExecuteBlenderClash,
|
||||
prop.ClashSource,
|
||||
prop.ClashSet,
|
||||
prop.SmartClashGroup,
|
||||
|
||||
@@ -411,82 +411,3 @@ class SelectSmartGroup(bpy.types.Operator):
|
||||
obj.select_set(True)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BlenderClasher:
|
||||
def process_clash_set(self, context):
|
||||
import collision
|
||||
|
||||
a_cm = collision.CollisionManager()
|
||||
b_cm = collision.CollisionManager()
|
||||
self.add_to_cm(a_cm, context.scene.BIMClashProperties.blender_clash_set_a, context)
|
||||
self.add_to_cm(b_cm, context.scene.BIMClashProperties.blender_clash_set_b, context)
|
||||
results = a_cm.in_collision_other(b_cm, return_data=True)
|
||||
if not results[0]:
|
||||
print("No clashes")
|
||||
return
|
||||
for contact in results[1]:
|
||||
if contact.raw.penetration_depth < 0.01:
|
||||
continue
|
||||
print("-----")
|
||||
print(contact.names)
|
||||
print(contact.raw.normal)
|
||||
print(contact.raw.pos)
|
||||
|
||||
def add_to_cm(self, cm, object_names, context):
|
||||
import ifcclash
|
||||
|
||||
for object_name in object_names:
|
||||
name = object_name.name
|
||||
obj = bpy.data.objects[name]
|
||||
triangulated_mesh = self.triangulate_mesh(obj, context)
|
||||
mesh = ifcclash.Mesh()
|
||||
mesh.vertices = np.array([tuple(obj.matrix_world @ v.co) for v in triangulated_mesh.vertices])
|
||||
mesh.faces = np.array([tuple(p.vertices) for p in triangulated_mesh.polygons])
|
||||
cm.add_object(name, mesh)
|
||||
|
||||
def triangulate_mesh(self, obj, context):
|
||||
mesh = obj.evaluated_get(context.evaluated_depsgraph_get()).to_mesh()
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.triangulate(bm, faces=bm.faces)
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
del bm
|
||||
return mesh
|
||||
|
||||
|
||||
class SetBlenderClashSetA(bpy.types.Operator):
|
||||
bl_idname = "bim.set_blender_clash_set_a"
|
||||
bl_label = "Set Blender Clash Set A"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.BIMClashProperties.blender_clash_set_a.clear()
|
||||
for obj in context.selected_objects:
|
||||
new = context.scene.BIMClashProperties.blender_clash_set_a.add()
|
||||
new.name = obj.name
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SetBlenderClashSetB(bpy.types.Operator):
|
||||
bl_idname = "bim.set_blender_clash_set_b"
|
||||
bl_label = "Set Blender Clash Set B"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.BIMClashProperties.blender_clash_set_b.clear()
|
||||
for obj in context.selected_objects:
|
||||
new = context.scene.BIMClashProperties.blender_clash_set_b.add()
|
||||
new.name = obj.name
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ExecuteBlenderClash(bpy.types.Operator):
|
||||
bl_idname = "bim.execute_blender_clash"
|
||||
bl_label = "Execute Blender Clash"
|
||||
|
||||
def execute(self, context):
|
||||
blender_clasher = BlenderClasher()
|
||||
blender_clasher.process_clash_set(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -27,6 +27,7 @@ class BIM_PT_ifcclash(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_quality_control"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
@@ -34,17 +35,6 @@ class BIM_PT_ifcclash(Panel):
|
||||
scene = context.scene
|
||||
props = scene.BIMClashProperties
|
||||
|
||||
layout.label(text="Blender Clash:")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.set_blender_clash_set_a")
|
||||
row.operator("bim.set_blender_clash_set_b")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.execute_blender_clash")
|
||||
|
||||
layout.label(text="IFC Clash:")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.add_clash_set")
|
||||
row.operator("bim.import_clash_sets", text="", icon="IMPORT")
|
||||
|
||||
@@ -29,6 +29,7 @@ class BIM_PT_classifications(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_project_setup"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -86,6 +87,7 @@ class BIM_PT_classification_references(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "object"
|
||||
bl_parent_id = "BIM_PT_object_metadata"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
|
||||
@@ -27,6 +27,7 @@ class BIM_PT_cobie(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_collaboration"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
@@ -29,6 +29,7 @@ class BIM_PT_constraints(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_project_setup"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -73,6 +74,7 @@ class BIM_PT_object_constraints(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "object"
|
||||
bl_parent_id = "BIM_PT_misc_object"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
|
||||
@@ -29,6 +29,7 @@ class BIM_PT_context(bpy.types.Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_geometry"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
|
||||
@@ -31,6 +31,7 @@ class BIM_PT_cost_schedules(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_4D5D"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
|
||||
@@ -26,6 +26,7 @@ class BIM_PT_covetool(bpy.types.Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_integrations"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
@@ -170,6 +170,8 @@ class ImportIfcCsv(bpy.types.Operator):
|
||||
else:
|
||||
ifc_csv.delimiter = props.csv_delimiter
|
||||
ifc_csv.Import(ifc_file)
|
||||
if not props.should_load_from_memory:
|
||||
ifc_file.write(props.csv_ifc_file)
|
||||
purge_module_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ class BIM_PT_ifccsv(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_collaboration"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
@@ -20,17 +20,21 @@ import bpy
|
||||
from . import ui, prop, operator
|
||||
|
||||
classes = (
|
||||
operator.PrintIfcFile,
|
||||
operator.PurgeIfcLinks,
|
||||
operator.PrintObjectPlacement,
|
||||
operator.ValidateIfcFile,
|
||||
operator.ProfileImportIFC,
|
||||
operator.CreateAllShapes,
|
||||
operator.CreateShapeFromStepId,
|
||||
operator.SelectHighPolygonMeshes,
|
||||
operator.InspectFromStepId,
|
||||
operator.InspectFromObject,
|
||||
operator.InspectFromStepId,
|
||||
operator.ParseExpress,
|
||||
operator.PrintIfcFile,
|
||||
operator.PrintObjectPlacement,
|
||||
operator.ProfileImportIFC,
|
||||
operator.PurgeHdf5Cache,
|
||||
operator.PurgeIfcLinks,
|
||||
operator.RewindInspector,
|
||||
operator.SelectExpressFile,
|
||||
operator.SelectHighPolygonMeshes,
|
||||
operator.SelectHighestPolygonMeshes,
|
||||
operator.ValidateIfcFile,
|
||||
prop.BIMDebugProperties,
|
||||
ui.BIM_PT_debug,
|
||||
)
|
||||
|
||||
@@ -16,20 +16,25 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import bpy
|
||||
import time
|
||||
import logging
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.representation
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.debug as core
|
||||
import blenderbim.bim.handler
|
||||
import blenderbim.bim.import_ifc as import_ifc
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
|
||||
|
||||
class PrintIfcFile(bpy.types.Operator):
|
||||
bl_idname = "bim.print_ifc_file"
|
||||
bl_label = "Print IFC File"
|
||||
bl_description = "Prints the file contents in the system console.\nAccess it with Window > Toggle System Console"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -43,6 +48,7 @@ class PrintIfcFile(bpy.types.Operator):
|
||||
class PurgeIfcLinks(bpy.types.Operator):
|
||||
bl_idname = "bim.purge_ifc_links"
|
||||
bl_label = "Purge IFC Links"
|
||||
bl_description = "Purge all definitions and references from the file.\nWarning : Cannot be undone."
|
||||
|
||||
def execute(self, context):
|
||||
for obj in bpy.data.objects:
|
||||
@@ -96,7 +102,8 @@ class ProfileImportIFC(bpy.types.Operator):
|
||||
|
||||
class CreateAllShapes(bpy.types.Operator):
|
||||
bl_idname = "bim.create_all_shapes"
|
||||
bl_label = "Create All Shapes"
|
||||
bl_label = "Test All Shapes"
|
||||
bl_description = "Look for errors in all the shapes contained in the file"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -126,7 +133,7 @@ class CreateAllShapes(bpy.types.Operator):
|
||||
except:
|
||||
failures.append(element)
|
||||
print("***** FAILURE *****")
|
||||
print("Failures:")
|
||||
print(f"Failures: {len(failures)}")
|
||||
for failure in failures:
|
||||
print(failure)
|
||||
return {"FINISHED"}
|
||||
@@ -135,6 +142,7 @@ class CreateAllShapes(bpy.types.Operator):
|
||||
class CreateShapeFromStepId(bpy.types.Operator):
|
||||
bl_idname = "bim.create_shape_from_step_id"
|
||||
bl_label = "Create Shape From STEP ID"
|
||||
bl_description = "Recreate a mesh object from a STEP ID"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
should_include_curves: bpy.props.BoolProperty()
|
||||
|
||||
@@ -154,9 +162,12 @@ class CreateShapeFromStepId(bpy.types.Operator):
|
||||
if self.should_include_curves:
|
||||
settings.set(settings.INCLUDE_CURVES, True)
|
||||
shape = ifcopenshell.geom.create_shape(settings, element)
|
||||
ifc_importer = import_ifc.IfcImporter(self.ifc_import_settings)
|
||||
ifc_importer.file = self.file
|
||||
mesh = ifc_importer.create_mesh(element, shape)
|
||||
if shape:
|
||||
ifc_importer = import_ifc.IfcImporter(self.ifc_import_settings)
|
||||
ifc_importer.file = self.file
|
||||
mesh = ifc_importer.create_mesh(element, shape)
|
||||
else:
|
||||
mesh = None
|
||||
obj = bpy.data.objects.new("Debug", mesh)
|
||||
context.scene.collection.objects.link(obj)
|
||||
return {"FINISHED"}
|
||||
@@ -165,20 +176,37 @@ class CreateShapeFromStepId(bpy.types.Operator):
|
||||
class SelectHighPolygonMeshes(bpy.types.Operator):
|
||||
bl_idname = "bim.select_high_polygon_meshes"
|
||||
bl_label = "Select High Polygon Meshes"
|
||||
bl_description = "Select objects containing more polygons than the specified number"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
threshold: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
[
|
||||
o.select_set(True)
|
||||
for o in context.view_layer.objects
|
||||
if o.type == "MESH" and len(o.data.polygons) > context.scene.BIMDebugProperties.number_of_polygons
|
||||
]
|
||||
for obj in context.view_layer.objects:
|
||||
if obj.type == "MESH" and len(obj.data.polygons) > self.threshold:
|
||||
obj.select_set(True)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectHighestPolygonMeshes(bpy.types.Operator):
|
||||
bl_idname = "bim.select_highest_polygon_meshes"
|
||||
bl_label = "Select Highest Polygon Meshes"
|
||||
bl_description = "Select objects with a number of polygons superior to the specified percentile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
percentile: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
objects = [obj for obj in context.view_layer.objects if obj.type == "MESH"]
|
||||
if objects:
|
||||
percentile = len(max(objects, key=lambda o: len(o.data.polygons)).data.polygons) * self.percentile / 100
|
||||
print(f"Selected all Meshes with more than {int(percentile)} polygons")
|
||||
[obj.select_set(True) for obj in objects if len(obj.data.polygons) > percentile]
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RewindInspector(bpy.types.Operator):
|
||||
bl_idname = "bim.rewind_inspector"
|
||||
bl_label = "Rewind Inspector"
|
||||
bl_description = "Rewind the Inspector to the previously inspected element"
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMDebugProperties
|
||||
@@ -195,6 +223,7 @@ class RewindInspector(bpy.types.Operator):
|
||||
class InspectFromStepId(bpy.types.Operator):
|
||||
bl_idname = "bim.inspect_from_step_id"
|
||||
bl_label = "Inspect From STEP ID"
|
||||
bl_description = "Inspect the attributes and references by looking up the specified STEP ID"
|
||||
step_id: bpy.props.IntProperty()
|
||||
|
||||
@classmethod
|
||||
@@ -245,12 +274,21 @@ class InspectFromStepId(bpy.types.Operator):
|
||||
class InspectFromObject(bpy.types.Operator):
|
||||
bl_idname = "bim.inspect_from_object"
|
||||
bl_label = "Inspect From Object"
|
||||
bl_description = "Inspect the Active Object's attributes and references"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
if bpy.app.version >= (3, 0, 0):
|
||||
cls.poll_message_set("No Active Object")
|
||||
elif not context.active_object.BIMObjectProperties.ifc_definition_id:
|
||||
if bpy.app.version >= (3, 0, 0):
|
||||
cls.poll_message_set("Active Object doesn't have an IFC definition")
|
||||
else:
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
ifc_definition_id = context.active_object.BIMObjectProperties.ifc_definition_id
|
||||
if not ifc_definition_id:
|
||||
return {"FINISHED"}
|
||||
bpy.ops.bim.inspect_from_step_id(step_id=ifc_definition_id)
|
||||
bpy.ops.bim.inspect_from_step_id(step_id=context.active_object.BIMObjectProperties.ifc_definition_id)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -262,3 +300,40 @@ class PrintObjectPlacement(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
print(ifcopenshell.util.placement.get_local_placement(IfcStore.get_file().by_id(self.step_id)))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ParseExpress(bpy.types.Operator):
|
||||
bl_idname = "bim.parse_express"
|
||||
bl_label = "Parse Express"
|
||||
|
||||
def execute(self, context):
|
||||
core.parse_express(tool.Debug, context.scene.BIMDebugProperties.express_file)
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectExpressFile(bpy.types.Operator):
|
||||
bl_idname = "bim.select_express_file"
|
||||
bl_label = "Select Express File"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Select an IFC EXPRESS definition"
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.exp", options={"HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
if os.path.exists(self.filepath) and "exp" in os.path.splitext(self.filepath)[1]:
|
||||
context.scene.BIMDebugProperties.express_file = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class PurgeHdf5Cache(bpy.types.Operator):
|
||||
bl_idname = "bim.purge_hdf5_cache"
|
||||
bl_label = "Purge HDF5 Cache"
|
||||
|
||||
def execute(self, context):
|
||||
core.purge_hdf5_cache(tool.Debug)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -33,8 +33,10 @@ from bpy.props import (
|
||||
class BIMDebugProperties(PropertyGroup):
|
||||
step_id: IntProperty(name="STEP ID")
|
||||
number_of_polygons: IntProperty(name="Number of Polygons", min=0)
|
||||
active_step_id: IntProperty(name="STEP ID")
|
||||
percentile_of_polygons: IntProperty(name="Percentile of Polygons", min=0, max=100, default=90, subtype="PERCENTAGE")
|
||||
active_step_id: IntProperty(name="STEP ID", default=1, soft_min=1)
|
||||
step_id_breadcrumb: CollectionProperty(name="STEP ID Breadcrumb", type=StrProperty)
|
||||
attributes: CollectionProperty(name="Attributes", type=Attribute)
|
||||
inverse_attributes: CollectionProperty(name="Inverse Attributes", type=Attribute)
|
||||
inverse_references: CollectionProperty(name="Inverse References", type=Attribute)
|
||||
express_file: StringProperty(name="Express File")
|
||||
|
||||
@@ -27,6 +27,7 @@ class BIM_PT_debug(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_quality_control"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
@@ -38,9 +39,17 @@ class BIM_PT_debug(Panel):
|
||||
row.operator("bim.validate_ifc_file", icon="CHECKMARK", text="")
|
||||
row.operator("bim.select_ifc_file", icon="FILE_FOLDER", text="")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "express_file", text="")
|
||||
row.operator("bim.parse_express", icon="IMPORT", text="")
|
||||
row.operator("bim.select_express_file", icon="FILE_FOLDER", text="")
|
||||
|
||||
row = layout.row()
|
||||
row.operator("bim.print_ifc_file")
|
||||
|
||||
row = layout.row()
|
||||
row.operator("bim.purge_hdf5_cache")
|
||||
|
||||
row = layout.row()
|
||||
row.operator("bim.purge_ifc_links")
|
||||
|
||||
@@ -56,8 +65,13 @@ class BIM_PT_debug(Panel):
|
||||
row.prop(props, "step_id", text="")
|
||||
|
||||
row = layout.split(factor=0.7, align=True)
|
||||
row.operator("bim.select_high_polygon_meshes")
|
||||
row.operator("bim.select_high_polygon_meshes").threshold = context.scene.BIMDebugProperties.number_of_polygons
|
||||
row.prop(props, "number_of_polygons", text="")
|
||||
row = layout.split(factor=0.7, align=True)
|
||||
row.operator(
|
||||
"bim.select_highest_polygon_meshes"
|
||||
).percentile = context.scene.BIMDebugProperties.percentile_of_polygons
|
||||
row.prop(props, "percentile_of_polygons", text="")
|
||||
|
||||
layout.label(text="Inspector:")
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ############################################################################ #
|
||||
|
||||
# Hey there! Welcome to the BlenderBIM Add-on code. Please feel free to reach
|
||||
# out if you have any questions or need further guidance. Happy hacking!
|
||||
|
||||
# ############################################################################ #
|
||||
|
||||
# Every module has a __init__.py file to load all of its classes. Every
|
||||
# operation, property, and interface needs to be registered with the Blender
|
||||
# system when the add-on loads. This is where it happens.
|
||||
|
||||
import bpy
|
||||
from . import ui, prop, operator
|
||||
|
||||
# You'll need to provide a list of every one of your classes here. If you forget
|
||||
# to specify your class, it won't load and you won't be able to use that
|
||||
# operator, property, or interface.
|
||||
classes = (
|
||||
operator.DemonstrateHelloWorld,
|
||||
operator.DemonstrateRenameProject,
|
||||
prop.BIMDemoProperties,
|
||||
ui.BIM_PT_demo,
|
||||
)
|
||||
|
||||
|
||||
# When the add-on loads, this register function is called. This allows you to
|
||||
# perform additional tasks during startup. If you need to store custom
|
||||
# properties, this is where you tell Blender where they are going to be stored.
|
||||
# You might see more advanced registrations happening in other modules.
|
||||
def register():
|
||||
# Properties are usually stored on bpy.types.Scene when they are something
|
||||
# that affects everything in the project, or bpy.types.Object when they
|
||||
# affect a single BIM element.
|
||||
bpy.types.Scene.BIMDemoProperties = bpy.props.PointerProperty(type=prop.BIMDemoProperties)
|
||||
|
||||
|
||||
# When someone disables the add-on, we need to unload everything we loaded. This
|
||||
# does the reverse of the register function.
|
||||
def unregister():
|
||||
del bpy.types.Scene.BIMDemoProperties
|
||||
@@ -0,0 +1,78 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ############################################################################ #
|
||||
|
||||
# Hey there! Welcome to the BlenderBIM Add-on code. Please feel free to reach
|
||||
# out if you have any questions or need further guidance. Happy hacking!
|
||||
|
||||
# ############################################################################ #
|
||||
|
||||
# Every module has a data.py file to load data for its interface panels to
|
||||
# display. When a panel needs to show information that doesn't come from a
|
||||
# Blender property, it gets that data from one of these classes. This separation
|
||||
# of "fetching data" and "displaying data" is a common concept that makes UI
|
||||
# code much simpler, more efficient, and less error-prone to refresh data.
|
||||
|
||||
import blenderbim.tool as tool
|
||||
|
||||
|
||||
# All data must have a refresh function. The refresh function simply sets all
|
||||
# data class to be not yet loaded. So the next time the interface is drawn to
|
||||
# the user, it will force the data to be reloaded.
|
||||
def refresh():
|
||||
# When you define your own data classes, just add to this list!
|
||||
DemoData.is_loaded = False
|
||||
|
||||
|
||||
# This is a sample data class. It correlates to a single interface panel. Panels
|
||||
# should not share data classes. This makes it easy to write your interface
|
||||
# without having your code mixed in with other parts of the interface. As a
|
||||
# convention, the class is named the same name as the panel.
|
||||
class DemoData:
|
||||
# All data classes must have two variables. One to store all the data it has
|
||||
# loaded and another to store the load state.
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
# Every data class must have a load function. This lets us load data in a
|
||||
# predictable manner.
|
||||
@classmethod
|
||||
def load(cls):
|
||||
# The load function always has two responsibilities: populate the data,
|
||||
# and set is_loaded to true.
|
||||
cls.data = {
|
||||
"has_project": cls.has_project(),
|
||||
"project_name": cls.project_name(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def has_project(cls):
|
||||
# Here, we check whether or not there is an active IFC project in our
|
||||
# Blender session.
|
||||
return bool(tool.Ifc.get())
|
||||
|
||||
@classmethod
|
||||
def project_name(cls):
|
||||
# Imagine how messy our UI code would be if all of this was mixed in
|
||||
# with our layout code. Here, it's isolated and testable, and the UX
|
||||
# becomes easier to maintain and change.
|
||||
ifc = tool.Ifc.get()
|
||||
if ifc:
|
||||
return ifc.by_type("IfcProject")[0].Name or "Unnamed"
|
||||
@@ -0,0 +1,79 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ############################################################################ #
|
||||
|
||||
# Hey there! Welcome to the BlenderBIM Add-on code. Please feel free to reach
|
||||
# out if you have any questions or need further guidance. Happy hacking!
|
||||
|
||||
# ############################################################################ #
|
||||
|
||||
# Every module has an operator.py file to define all of the buttons that a user
|
||||
# can click on from the Blender interface. Blender calls these buttons
|
||||
# "Operators", since they correlate to a single user operation.
|
||||
|
||||
import bpy
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.demo as core
|
||||
import blenderbim.bim.handler
|
||||
|
||||
|
||||
# Each button correlates to a class like the one below. In this case, we're
|
||||
# creating a new button that will execute a hello world feature.
|
||||
class DemonstrateHelloWorld(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# Every operator has a unique ID. If you enable Python tooltips and hover
|
||||
# over any button in the interface, you will see each button will run a
|
||||
# function that uses this ID. For example, hovering over this button will
|
||||
# show that the code it executes is "bpy.ops.bim.demonstrate_hello_world()"
|
||||
bl_idname = "bim.demonstrate_hello_world"
|
||||
|
||||
# In the interface, this button will have the text "Print Hello World".
|
||||
bl_label = "Print Hello World"
|
||||
|
||||
# This code means that the user can undo or redo after pressing the button.
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
# When hovering over the button, this helpful description will be shown.
|
||||
bl_description = "Prints the text 'Hello World'"
|
||||
|
||||
# When the button is pressed, this _execute() function will run.
|
||||
def _execute(self, context):
|
||||
# Every operator should do one thing only: execute a core function. In
|
||||
# order to execute a core function, the operator's responsibility is to
|
||||
# pass in all of the inputs the core needs to do its job.
|
||||
|
||||
# A core function simply tells tools what to do, so a core function will
|
||||
# always need at least one tool as an input.
|
||||
core.demonstrate_hello_world(tool.Demo)
|
||||
|
||||
|
||||
# This class, just like the one above, will correlate to another button in the
|
||||
# interface. It has a different ID and a different label.
|
||||
class DemonstrateRenameProject(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.demonstrate_rename_project"
|
||||
bl_label = "Rename IFC Project"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Renames your IFC project name and prints the name"
|
||||
|
||||
def _execute(self, context):
|
||||
# This core function requires three inputs: two tools, and a name
|
||||
# string. In this case, the name is taken from some custom Blender
|
||||
# properties. Generally, the inputs to the core function will come from
|
||||
# properties (such as an input field) or data from the scene (like the
|
||||
# actively selected object).
|
||||
core.demonstrate_rename_project(tool.Ifc, tool.Demo, name=bpy.context.scene.BIMDemoProperties.name)
|
||||
@@ -0,0 +1,61 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ############################################################################ #
|
||||
|
||||
# Hey there! Welcome to the BlenderBIM Add-on code. Please feel free to reach
|
||||
# out if you have any questions or need further guidance. Happy hacking!
|
||||
|
||||
# ############################################################################ #
|
||||
|
||||
# Every module has a prop.py file to define Blender properties. Any time you
|
||||
# want an interface widget like an input field, dropdown, checkbox, or number
|
||||
# slider, you need a Blender property to store that widget's data. If you want
|
||||
# to store data that will affect the Blender interface, you also need a
|
||||
# property. Properties are stored in the .blend file, so when your user closes
|
||||
# their Blender session, and reopens it, things are how they left it.
|
||||
|
||||
import bpy
|
||||
from bpy.types import PropertyGroup
|
||||
|
||||
# Properties have many different data types. We won't use all of them in this
|
||||
# demo module, but this is a list for your reference.
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
StringProperty,
|
||||
EnumProperty,
|
||||
BoolProperty,
|
||||
IntProperty,
|
||||
FloatProperty,
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
|
||||
|
||||
# All properties must belong in a property group. Usually, you'd have a group
|
||||
# named after your module.
|
||||
class BIMDemoProperties(PropertyGroup):
|
||||
# This first property is a string. This means that in the interface, it will
|
||||
# represent a text input field. We can give it a name and a default value.
|
||||
# The name will be the label shown next to the input field in the interface.
|
||||
name: StringProperty(name="Name", default="New Project Name")
|
||||
# Not all properties need to be shown using their equivalent input widget.
|
||||
# In this case, we can store a message string, but we will never show it as
|
||||
# an input text field in the ui.py.
|
||||
message: StringProperty(name="Message")
|
||||
show_hints: BoolProperty(name="Show Hints", default=False)
|
||||
@@ -0,0 +1,131 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ############################################################################ #
|
||||
|
||||
# Hey there! Welcome to the BlenderBIM Add-on code. Please feel free to reach
|
||||
# out if you have any questions or need further guidance. Happy hacking!
|
||||
|
||||
# ############################################################################ #
|
||||
|
||||
# Every module has a ui.py file to define its interface. Interfaces describe how
|
||||
# panels, buttons, labels, and input fields are laid out.
|
||||
|
||||
import bpy
|
||||
from blenderbim.bim.module.demo.data import DemoData
|
||||
|
||||
# Every panel in the interface correlates to one of these classes. If you enable
|
||||
# the "Developer Extras" option in Blender, then you can actually right click on
|
||||
# any panel in Blender, and click "Edit Source". This will bring you to a ui.py
|
||||
# file like this one where you can see this code. Pretty neat!
|
||||
class BIM_PT_demo(bpy.types.Panel):
|
||||
# Every panel has a title.
|
||||
bl_label = "BlenderBIM Demo"
|
||||
|
||||
# Every panel must have an ID. It must be unique. For example, you may want
|
||||
# to later reference the panel (such as if you want to create a nested
|
||||
# subpanel).
|
||||
bl_idname = "BIM_PT_demo"
|
||||
|
||||
# This tells the panel to appear in the properties section (in the bottom
|
||||
# right by default) of the Blender interface.
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
|
||||
# This tells the panel to appear in the "scene" tab of the properties panel.
|
||||
bl_context = "scene"
|
||||
|
||||
# Every panel has a draw function. This draws all the layout of the panel,
|
||||
# including all of its labels, buttons, and so on. Note that if the panel is
|
||||
# hidden, this draw function will not be called.
|
||||
def draw(self, context):
|
||||
# Before drawing any content, the panel must load any dynamic variables
|
||||
# that it will show. Most panels have dynamic content, which display
|
||||
# changing data from your BIM model. You must load that data first. The
|
||||
# two lines are always the same, "if not loaded, then load".
|
||||
if not DemoData.is_loaded:
|
||||
# Each panel should have its own data class that it loads data from.
|
||||
# Note that we only load data if it hasn't already been loaded.
|
||||
# This is because interface panels are drawn continuously. This
|
||||
# draw() function will be called every time you scroll or move your
|
||||
# mouse over it. Loading data is slow, so we only refresh data when
|
||||
# we have to.
|
||||
DemoData.load()
|
||||
|
||||
# Interface panels often show properties. For convenience, define where
|
||||
# the properties are stored for the module.
|
||||
self.props = context.scene.BIMDemoProperties
|
||||
|
||||
# This defines a new "row" in our layout. When a new row is defined, the
|
||||
# things on that row, like buttons, labels, and input fields, show on a
|
||||
# new line.
|
||||
row = self.layout.row()
|
||||
# This is the simplest interface element - a label placed in our row.
|
||||
# If you enable the "Icon Viewer" add-on, you can view a list of
|
||||
# Blender's built-in icons to choose from in the Blender text editor's
|
||||
# side panel.
|
||||
row.label(text="This is a demo panel", icon="INFO")
|
||||
|
||||
# Our interface can contain simple logic. For example, if we don't have
|
||||
# an IFC project, show an error message and don't draw anything else.
|
||||
# The ui.py should always have very simple logic. Details like how to
|
||||
# determine whether we have a project is delegated to the data loader.
|
||||
if not DemoData.data["has_project"]:
|
||||
row = self.layout.row()
|
||||
row.label(text="Load or create an IFC project first", icon="ERROR")
|
||||
return
|
||||
|
||||
row = self.layout.row()
|
||||
# When you want to show a button in the interface, you have to specify
|
||||
# an operator. Operators are defined in operator.py. You reference an
|
||||
# operator using its unique ID.
|
||||
row.operator("bim.demonstrate_hello_world")
|
||||
|
||||
# Blender properties can be used to affect what your panel shows. Notice
|
||||
# how the logic is very simple. Only use simple boolean predicates and
|
||||
# loops in your layout. No complex logic should ever been seen in the
|
||||
# interface code.
|
||||
if self.props.message:
|
||||
row = self.layout.row()
|
||||
row.label(text=self.props.message)
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Project Name")
|
||||
# Sometimes you need data that doesn't come from Blender properties,
|
||||
# like from your IFC model. In this case, just get some data from your
|
||||
# data loader. For those familiar with how templating languages work,
|
||||
# this is exactly the same.
|
||||
row.label(text=DemoData.data["project_name"])
|
||||
|
||||
row = self.layout.row()
|
||||
# We've seen how to show text labels and buttons using operators. What
|
||||
# about text input fields, number sliders, dropdowns, checkboxes, and
|
||||
# scrollable lists? The way Blender works is that you simply show a
|
||||
# Blender property in your interface. The data type of that property
|
||||
# determines which UI widget is shown. A text property will show a text
|
||||
# field. A number property will show a number slider. An enum property
|
||||
# will show a drop down. And so on. In this case, our name property is a
|
||||
# text data type, so expect to see a text input field show up here.
|
||||
row.prop(self.props, "name")
|
||||
row = self.layout.row()
|
||||
# Here's another button, referencing another operator.
|
||||
row.operator("bim.demonstrate_rename_project")
|
||||
|
||||
if self.props.show_hints:
|
||||
row = self.layout.row()
|
||||
row.label(text="Name cannot be blank!")
|
||||
@@ -27,6 +27,7 @@ class BIM_PT_diff(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_quality_control"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
@@ -20,35 +20,29 @@ import bpy
|
||||
from . import ui, prop, operator
|
||||
|
||||
classes = (
|
||||
operator.LoadInformation,
|
||||
operator.LoadDocumentReferences,
|
||||
operator.DisableDocumentEditingUI,
|
||||
operator.EnableEditingDocument,
|
||||
operator.DisableEditingDocument,
|
||||
operator.AddInformation,
|
||||
operator.AddDocumentReference,
|
||||
operator.EditInformation,
|
||||
operator.EditDocumentReference,
|
||||
operator.RemoveDocument,
|
||||
operator.EnableAssigningDocument,
|
||||
operator.DisableAssigningDocument,
|
||||
operator.AddInformation,
|
||||
operator.AssignDocument,
|
||||
operator.DisableDocumentEditingUI,
|
||||
operator.DisableEditingDocument,
|
||||
operator.EditDocument,
|
||||
operator.EnableEditingDocument,
|
||||
operator.LoadDocument,
|
||||
operator.LoadParentDocument,
|
||||
operator.LoadProjectDocuments,
|
||||
operator.RemoveDocument,
|
||||
operator.UnassignDocument,
|
||||
prop.Document,
|
||||
prop.BIMDocumentProperties,
|
||||
prop.BIMObjectDocumentProperties,
|
||||
ui.BIM_PT_documents,
|
||||
ui.BIM_PT_object_documents,
|
||||
ui.BIM_UL_documents,
|
||||
ui.BIM_UL_object_documents,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.Scene.BIMDocumentProperties = bpy.props.PointerProperty(type=prop.BIMDocumentProperties)
|
||||
bpy.types.Object.BIMObjectDocumentProperties = bpy.props.PointerProperty(type=prop.BIMObjectDocumentProperties)
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.Scene.BIMDocumentProperties
|
||||
del bpy.types.Object.BIMObjectDocumentProperties
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.schema
|
||||
import blenderbim.tool as tool
|
||||
|
||||
|
||||
def refresh():
|
||||
DocumentData.is_loaded = False
|
||||
ObjectDocumentData.is_loaded = False
|
||||
|
||||
|
||||
class DocumentData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {
|
||||
"total_information": cls.total_information(),
|
||||
"parent_document": cls.parent_document(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def total_information(cls):
|
||||
return len(
|
||||
[
|
||||
rel
|
||||
for rel in tool.Ifc.get().by_type("IfcProject")[0].HasAssociations or []
|
||||
if rel.is_a("IfcRelAssociatesDocument") and rel.RelatingDocument.is_a("IfcDocumentInformation")
|
||||
]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def parent_document(cls):
|
||||
props = bpy.context.scene.BIMDocumentProperties
|
||||
if len(props.breadcrumbs):
|
||||
parent = tool.Ifc.get().by_id(int(props.breadcrumbs[-1].name))
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
return str(parent.DocumentId)
|
||||
return str(parent.Identification)
|
||||
return ""
|
||||
|
||||
|
||||
class ObjectDocumentData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {
|
||||
"documents": cls.documents(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def documents(cls):
|
||||
results = []
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
if not element:
|
||||
return results
|
||||
for rel in getattr(element, "HasAssociations", []):
|
||||
if rel.is_a("IfcRelAssociatesDocument"):
|
||||
if not rel.RelatingDocument.is_a("IfcDocumentReference"):
|
||||
continue
|
||||
|
||||
name = rel.RelatingDocument.Name
|
||||
if not name and rel.RelatingDocument.ReferencedDocument:
|
||||
name = rel.RelatingDocument.ReferencedDocument.Name
|
||||
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
identification = rel.RelatingDocument.ItemReference
|
||||
if not identification and rel.RelatingDocument.ReferencedDocument:
|
||||
identification = rel.RelatingDocument.ReferencedDocument.DocumentId
|
||||
else:
|
||||
identification = rel.RelatingDocument.Identification
|
||||
if not identification and rel.RelatingDocument.ReferencedDocument:
|
||||
identification = rel.RelatingDocument.ReferencedDocument.Identification
|
||||
|
||||
location = rel.RelatingDocument.Location
|
||||
if location is None and rel.RelatingDocument.ReferencedDocument:
|
||||
location = rel.RelatingDocument.ReferencedDocument.Location
|
||||
if location:
|
||||
if not "://" in location:
|
||||
if not os.path.isabs(location):
|
||||
location = os.path.abspath(os.path.join(os.path.dirname(tool.Ifc.get_path()), location))
|
||||
location = "file://" + location
|
||||
|
||||
results.append(
|
||||
{
|
||||
"id": rel.RelatingDocument.id(),
|
||||
"identification": identification,
|
||||
"name": name,
|
||||
"location": location,
|
||||
}
|
||||
)
|
||||
return results
|
||||
@@ -1,5 +1,5 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
# Copyright (C) 2020, 2021, 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
@@ -20,303 +20,133 @@ import bpy
|
||||
import json
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.attribute
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.document as core
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from ifcopenshell.api.document.data import Data
|
||||
|
||||
|
||||
class LoadInformation(bpy.types.Operator):
|
||||
bl_idname = "bim.load_information"
|
||||
bl_label = "Load Information"
|
||||
class LoadProjectDocuments(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.load_project_documents"
|
||||
bl_label = "Load Project Documents"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
props = context.scene.BIMDocumentProperties
|
||||
props.documents.clear()
|
||||
for information_id, information in Data.information.items():
|
||||
new = props.documents.add()
|
||||
new.name = information["Name"] or "Unnamed"
|
||||
if self.file.schema == "IFC2X3":
|
||||
new.identification = information["DocumentId"] or "*"
|
||||
else:
|
||||
new.identification = information["Identification"] or "*"
|
||||
new.ifc_definition_id = information_id
|
||||
props.is_editing = "information"
|
||||
bpy.ops.bim.disable_editing_document()
|
||||
return {"FINISHED"}
|
||||
def _execute(self, context):
|
||||
core.load_project_documents(tool.Document)
|
||||
|
||||
|
||||
class LoadDocumentReferences(bpy.types.Operator):
|
||||
bl_idname = "bim.load_document_references"
|
||||
bl_label = "Load Document References"
|
||||
class LoadDocument(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.load_document"
|
||||
bl_label = "Load Document"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
document: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
core.load_document(tool.Document, document=tool.Ifc.get().by_id(self.document))
|
||||
|
||||
|
||||
class LoadParentDocument(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.load_parent_document"
|
||||
bl_label = "Load Parent Document"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
props = context.scene.BIMDocumentProperties
|
||||
props.documents.clear()
|
||||
for reference_id, reference in Data.references.items():
|
||||
new = props.documents.add()
|
||||
new.name = reference["Name"] or "Unnamed"
|
||||
if self.file.schema == "IFC2X3":
|
||||
new.identification = reference["ItemReference"] or "*"
|
||||
else:
|
||||
new.identification = reference["Identification"] or "*"
|
||||
new.ifc_definition_id = reference_id
|
||||
props.is_editing = "reference"
|
||||
bpy.ops.bim.disable_editing_document()
|
||||
return {"FINISHED"}
|
||||
def _execute(self, context):
|
||||
core.load_parent_document(tool.Document)
|
||||
|
||||
|
||||
class DisableDocumentEditingUI(bpy.types.Operator):
|
||||
class DisableDocumentEditingUI(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.disable_document_editing_ui"
|
||||
bl_label = "Disable Document Editing UI"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.BIMDocumentProperties.is_editing = ""
|
||||
bpy.ops.bim.disable_editing_document()
|
||||
return {"FINISHED"}
|
||||
def _execute(self, context):
|
||||
core.disable_document_editing_ui(tool.Document)
|
||||
|
||||
|
||||
class EnableEditingDocument(bpy.types.Operator):
|
||||
class EnableEditingDocument(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.enable_editing_document"
|
||||
bl_label = "Enable Editing Document"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
document: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMDocumentProperties
|
||||
props.document_attributes.clear()
|
||||
|
||||
if props.is_editing == "information":
|
||||
data = Data.information[self.document]
|
||||
ifc_class = "IfcDocumentInformation"
|
||||
elif props.is_editing == "reference":
|
||||
data = Data.references[self.document]
|
||||
ifc_class = "IfcDocumentReference"
|
||||
|
||||
for attribute in IfcStore.get_schema().declaration_by_name(ifc_class).all_attributes():
|
||||
data_type = ifcopenshell.util.attribute.get_primitive_type(attribute)
|
||||
if data_type == "entity":
|
||||
continue
|
||||
new = props.document_attributes.add()
|
||||
new.name = attribute.name()
|
||||
new.is_null = data[attribute.name()] is None
|
||||
new.is_optional = attribute.optional()
|
||||
new.data_type = data_type
|
||||
if data_type == "string":
|
||||
new.string_value = "" if new.is_null else data[attribute.name()]
|
||||
elif data_type == "enum":
|
||||
new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute))
|
||||
if data[attribute.name()]:
|
||||
new.enum_value = data[attribute.name()]
|
||||
props.active_document_id = self.document
|
||||
return {"FINISHED"}
|
||||
def _execute(self, context):
|
||||
core.enable_editing_document(tool.Document, document=tool.Ifc.get().by_id(self.document))
|
||||
|
||||
|
||||
class DisableEditingDocument(bpy.types.Operator):
|
||||
class DisableEditingDocument(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.disable_editing_document"
|
||||
bl_label = "Disable Editing Document"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.BIMDocumentProperties.active_document_id = 0
|
||||
return {"FINISHED"}
|
||||
def _execute(self, context):
|
||||
core.disable_editing_document(tool.Document)
|
||||
|
||||
|
||||
class AddInformation(bpy.types.Operator):
|
||||
class AddInformation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_information"
|
||||
bl_label = "Add Information"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
result = ifcopenshell.api.run("document.add_information", IfcStore.get_file())
|
||||
Data.load(IfcStore.get_file())
|
||||
bpy.ops.bim.load_information()
|
||||
bpy.ops.bim.enable_editing_document(document=result.id())
|
||||
return {"FINISHED"}
|
||||
core.add_information(tool.Ifc, tool.Document)
|
||||
|
||||
|
||||
class AddDocumentReference(bpy.types.Operator):
|
||||
class AddDocumentReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_document_reference"
|
||||
bl_label = "Add Document Reference"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
result = ifcopenshell.api.run("document.add_reference", IfcStore.get_file())
|
||||
Data.load(IfcStore.get_file())
|
||||
bpy.ops.bim.load_document_references()
|
||||
bpy.ops.bim.enable_editing_document(document=result.id())
|
||||
return {"FINISHED"}
|
||||
core.add_reference(tool.Ifc, tool.Document)
|
||||
|
||||
|
||||
class EditInformation(bpy.types.Operator):
|
||||
bl_idname = "bim.edit_information"
|
||||
class EditDocument(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.edit_document"
|
||||
bl_label = "Edit Information"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMDocumentProperties
|
||||
attributes = {}
|
||||
for attribute in props.document_attributes:
|
||||
if attribute.is_null:
|
||||
attributes[attribute.name] = None
|
||||
elif attribute.enum_items:
|
||||
attributes[attribute.name] = attribute.enum_value
|
||||
else:
|
||||
attributes[attribute.name] = attribute.string_value
|
||||
self.file = IfcStore.get_file()
|
||||
ifcopenshell.api.run(
|
||||
"document.edit_information",
|
||||
self.file,
|
||||
**{"information": self.file.by_id(props.active_document_id), "attributes": attributes}
|
||||
)
|
||||
Data.load(IfcStore.get_file())
|
||||
bpy.ops.bim.load_information()
|
||||
return {"FINISHED"}
|
||||
core.edit_document(tool.Ifc, tool.Document, document=tool.Ifc.get().by_id(props.active_document_id))
|
||||
|
||||
|
||||
class EditDocumentReference(bpy.types.Operator):
|
||||
bl_idname = "bim.edit_document_reference"
|
||||
bl_label = "Edit Document Reference"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMDocumentProperties
|
||||
attributes = {}
|
||||
for attribute in props.document_attributes:
|
||||
if attribute.is_null:
|
||||
attributes[attribute.name] = None
|
||||
else:
|
||||
attributes[attribute.name] = attribute.string_value
|
||||
self.file = IfcStore.get_file()
|
||||
ifcopenshell.api.run(
|
||||
"document.edit_reference",
|
||||
self.file,
|
||||
**{"reference": self.file.by_id(props.active_document_id), "attributes": attributes}
|
||||
)
|
||||
Data.load(IfcStore.get_file())
|
||||
bpy.ops.bim.load_document_references()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemoveDocument(bpy.types.Operator):
|
||||
class RemoveDocument(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_document"
|
||||
bl_label = "Remove Document"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
document: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMDocumentProperties
|
||||
self.file = IfcStore.get_file()
|
||||
ifcopenshell.api.run("document.remove_document", self.file, **{"document": self.file.by_id(self.document)})
|
||||
Data.load(IfcStore.get_file())
|
||||
if props.is_editing == "information":
|
||||
bpy.ops.bim.load_information()
|
||||
elif props.is_editing == "reference":
|
||||
bpy.ops.bim.load_document_references()
|
||||
return {"FINISHED"}
|
||||
core.remove_document(tool.Ifc, tool.Document, document=tool.Ifc.get().by_id(self.document))
|
||||
|
||||
|
||||
class EnableAssigningDocument(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_assigning_document"
|
||||
bl_label = "Enable Assigning Document"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
props = obj.BIMObjectDocumentProperties
|
||||
if props.available_document_types == "IfcDocumentInformation":
|
||||
bpy.ops.bim.load_information()
|
||||
elif props.available_document_types == "IfcDocumentReference":
|
||||
bpy.ops.bim.load_document_references()
|
||||
props.is_adding = props.available_document_types
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisableAssigningDocument(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_assigning_document"
|
||||
bl_label = "Disable Assigning Document"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
props = obj.BIMObjectDocumentProperties
|
||||
props.is_adding = ""
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AssignDocument(bpy.types.Operator):
|
||||
class AssignDocument(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.assign_document"
|
||||
bl_label = "Assign Document"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
document: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
document = tool.Ifc.get().by_id(self.document)
|
||||
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
|
||||
for obj in objs:
|
||||
obj_id = obj.BIMObjectProperties.ifc_definition_id
|
||||
if not obj_id:
|
||||
continue
|
||||
ifcopenshell.api.run(
|
||||
"document.assign_document",
|
||||
self.file,
|
||||
**{
|
||||
"product": self.file.by_id(obj_id),
|
||||
"document": self.file.by_id(self.document),
|
||||
}
|
||||
)
|
||||
Data.load(self.file, obj_id)
|
||||
return {"FINISHED"}
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element:
|
||||
core.assign_document(tool.Ifc, product=element, document=document)
|
||||
|
||||
|
||||
class UnassignDocument(bpy.types.Operator):
|
||||
class UnassignDocument(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.unassign_document"
|
||||
bl_label = "Unassign Document"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
document: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
document = tool.Ifc.get().by_id(self.document)
|
||||
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
|
||||
for obj in objs:
|
||||
obj_id = obj.BIMObjectProperties.ifc_definition_id
|
||||
if not obj_id:
|
||||
continue
|
||||
ifcopenshell.api.run(
|
||||
"document.unassign_document",
|
||||
self.file,
|
||||
**{
|
||||
"product": self.file.by_id(obj_id),
|
||||
"document": self.file.by_id(self.document),
|
||||
}
|
||||
)
|
||||
Data.load(self.file, obj_id)
|
||||
return {"FINISHED"}
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element:
|
||||
core.unassign_document(tool.Ifc, product=element, document=document)
|
||||
|
||||
@@ -34,6 +34,7 @@ from bpy.props import (
|
||||
class Document(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
identification: StringProperty(name="Identification")
|
||||
is_information: BoolProperty(name="Is Information")
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
|
||||
|
||||
@@ -41,12 +42,6 @@ class BIMDocumentProperties(PropertyGroup):
|
||||
document_attributes: CollectionProperty(name="Document Attributes", type=Attribute)
|
||||
active_document_id: IntProperty(name="Active Document Id")
|
||||
documents: CollectionProperty(name="Documents", type=Document)
|
||||
breadcrumbs: CollectionProperty(name="Breadcrumbs", type=StrProperty)
|
||||
active_document_index: IntProperty(name="Active Document Index")
|
||||
is_editing: StringProperty(name="Is Editing")
|
||||
|
||||
|
||||
class BIMObjectDocumentProperties(PropertyGroup):
|
||||
is_adding: StringProperty(name="Is Adding")
|
||||
available_document_types: EnumProperty(
|
||||
items=[(d, d, "") for d in ["IfcDocumentInformation", "IfcDocumentReference"]], name="Available Document Types"
|
||||
)
|
||||
is_editing: BoolProperty(name="Is Editing", default=False)
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
from bpy.types import Panel, UIList
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.helper import draw_attributes
|
||||
from ifcopenshell.api.document.data import Data
|
||||
from blenderbim.bim.module.document.data import DocumentData, ObjectDocumentData
|
||||
|
||||
|
||||
class BIM_PT_documents(Panel):
|
||||
@@ -29,45 +29,50 @@ class BIM_PT_documents(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_project_setup"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return IfcStore.get_file()
|
||||
|
||||
def draw(self, context):
|
||||
if not Data.is_loaded:
|
||||
Data.load(IfcStore.get_file())
|
||||
if not DocumentData.is_loaded:
|
||||
DocumentData.load()
|
||||
|
||||
self.props = context.scene.BIMDocumentProperties
|
||||
|
||||
if not self.props.is_editing or self.props.is_editing == "information":
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="{} Documents Found".format(len(Data.information)), icon="FILE")
|
||||
if self.props.is_editing == "information":
|
||||
row.operator("bim.add_information", text="", icon="ADD")
|
||||
row.operator("bim.disable_document_editing_ui", text="", icon="CANCEL")
|
||||
else:
|
||||
row.operator("bim.load_information", text="", icon="IMPORT")
|
||||
|
||||
if not self.props.is_editing or self.props.is_editing == "reference":
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="{} References Found".format(len(Data.references)), icon="FILE_HIDDEN")
|
||||
if self.props.is_editing == "reference":
|
||||
row.operator("bim.add_document_reference", text="", icon="ADD")
|
||||
row.operator("bim.disable_document_editing_ui", text="", icon="CANCEL")
|
||||
else:
|
||||
row.operator("bim.load_document_references", text="", icon="IMPORT")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="{} Documents Found".format(DocumentData.data["total_information"]), icon="FILE")
|
||||
if self.props.is_editing:
|
||||
self.layout.template_list(
|
||||
"BIM_UL_documents", "", self.props, "documents", self.props, "active_document_index"
|
||||
)
|
||||
row.operator("bim.disable_document_editing_ui", text="", icon="CANCEL")
|
||||
else:
|
||||
row.operator("bim.load_project_documents", text="", icon="IMPORT")
|
||||
|
||||
if not self.props.is_editing:
|
||||
return
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
if self.props.breadcrumbs:
|
||||
row.operator("bim.load_parent_document", text="", icon="FRAME_PREV")
|
||||
row.label(text=DocumentData.data["parent_document"])
|
||||
else:
|
||||
row.alignment = "RIGHT"
|
||||
row.operator("bim.add_information", text="", icon="ADD")
|
||||
if self.props.breadcrumbs:
|
||||
row.operator("bim.add_document_reference", text="", icon="FILE_HIDDEN")
|
||||
|
||||
if self.props.active_document_id:
|
||||
self.draw_editable_ui(context)
|
||||
row.operator("bim.edit_document", text="", icon="CHECKMARK")
|
||||
row.operator("bim.disable_editing_document", text="", icon="CANCEL")
|
||||
elif self.props.documents and self.props.active_document_index < len(self.props.documents):
|
||||
ifc_definition_id = self.props.documents[self.props.active_document_index].ifc_definition_id
|
||||
row.operator("bim.enable_editing_document", text="", icon="GREASEPENCIL").document = ifc_definition_id
|
||||
row.operator("bim.remove_document", text="", icon="X").document = ifc_definition_id
|
||||
|
||||
def draw_editable_ui(self, context):
|
||||
draw_attributes(self.props.document_attributes, self.layout)
|
||||
self.layout.template_list("BIM_UL_documents", "", self.props, "documents", self.props, "active_document_index")
|
||||
|
||||
if self.props.active_document_id:
|
||||
draw_attributes(self.props.document_attributes, self.layout)
|
||||
|
||||
|
||||
class BIM_PT_object_documents(Panel):
|
||||
@@ -77,6 +82,7 @@ class BIM_PT_object_documents(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "object"
|
||||
bl_parent_id = "BIM_PT_misc_object"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -87,88 +93,64 @@ class BIM_PT_object_documents(Panel):
|
||||
return bool(context.active_object.BIMObjectProperties.ifc_definition_id)
|
||||
|
||||
def draw(self, context):
|
||||
if not ObjectDocumentData.is_loaded:
|
||||
ObjectDocumentData.load()
|
||||
|
||||
obj = context.active_object
|
||||
self.oprops = obj.BIMObjectProperties
|
||||
self.sprops = context.scene.BIMDocumentProperties
|
||||
self.props = obj.BIMObjectDocumentProperties
|
||||
self.props = context.scene.BIMDocumentProperties
|
||||
self.file = IfcStore.get_file()
|
||||
if not Data.is_loaded:
|
||||
Data.load(IfcStore.get_file())
|
||||
if self.oprops.ifc_definition_id not in Data.products:
|
||||
Data.load(IfcStore.get_file(), self.oprops.ifc_definition_id)
|
||||
|
||||
self.draw_add_ui()
|
||||
|
||||
document_ids = Data.products[self.oprops.ifc_definition_id]
|
||||
|
||||
if not document_ids:
|
||||
if not ObjectDocumentData.data["documents"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="No Documents", icon="FILE")
|
||||
|
||||
for document_id in document_ids:
|
||||
try:
|
||||
document = Data.information[document_id]
|
||||
document_type = "IfcDocumentInformation"
|
||||
icon = "FILE"
|
||||
except:
|
||||
document = Data.references[document_id]
|
||||
document_type = "IfcDocumentReference"
|
||||
icon = "FILE_HIDDEN"
|
||||
for document in ObjectDocumentData.data["documents"]:
|
||||
row = self.layout.row(align=True)
|
||||
if self.file.schema == "IFC2X3":
|
||||
if document_type == "IfcDocumentInformation":
|
||||
row.label(text=document.get("DocumentId") or "*", icon=icon)
|
||||
elif document_type == "IfcDocumentReference":
|
||||
row.label(text=document.get("ItemReference") or "*", icon=icon)
|
||||
else:
|
||||
row.label(text=document.get("Identification") or "*", icon=icon)
|
||||
row.label(text=document.get("Name") or "Unnamed")
|
||||
row.operator("bim.unassign_document", text="", icon="X").document = document_id
|
||||
row.label(text=document["identification"] or "*", icon="FILE")
|
||||
row.label(text=document["name"] or "Unnamed")
|
||||
if document["location"]:
|
||||
row.operator("bim.open_uri", icon="URL", text="").uri = document["location"]
|
||||
row.operator("bim.unassign_document", text="", icon="X").document = document["id"]
|
||||
|
||||
def draw_add_ui(self):
|
||||
if self.props.is_adding:
|
||||
if not self.props.is_editing:
|
||||
row = self.layout.row(align=True)
|
||||
icon = "FILE" if self.props.is_adding == "IfcDocumentInformation" else "FILE_HIDDEN"
|
||||
row.label(text="Adding {}".format(self.props.is_adding), icon=icon)
|
||||
row.operator("bim.disable_assigning_document", text="", icon="CANCEL")
|
||||
self.layout.template_list(
|
||||
"BIM_UL_object_documents",
|
||||
"",
|
||||
self.sprops,
|
||||
"documents",
|
||||
self.sprops,
|
||||
"active_document_index",
|
||||
)
|
||||
row.operator("bim.load_project_documents", text="Assign Document References", icon="ADD")
|
||||
return
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
if self.props.breadcrumbs:
|
||||
row.operator("bim.load_parent_document", text="", icon="FRAME_PREV")
|
||||
row.label(text=DocumentData.data["parent_document"])
|
||||
else:
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "available_document_types", text="")
|
||||
row.operator("bim.enable_assigning_document", text="", icon="ADD")
|
||||
row.alignment = "RIGHT"
|
||||
|
||||
if self.props.documents and self.props.active_document_index < len(self.props.documents):
|
||||
document = self.props.documents[self.props.active_document_index]
|
||||
if not document.is_information:
|
||||
row.operator("bim.assign_document", text="", icon="ADD").document = document.ifc_definition_id
|
||||
row.operator("bim.disable_document_editing_ui", text="", icon="CANCEL")
|
||||
|
||||
self.layout.template_list("BIM_UL_documents", "", self.props, "documents", self.props, "active_document_index")
|
||||
|
||||
|
||||
class BIM_UL_documents(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
row.label(text=item.identification)
|
||||
row.label(text=item.name)
|
||||
if context.scene.BIMDocumentProperties.active_document_id == item.ifc_definition_id:
|
||||
if context.scene.BIMDocumentProperties.is_editing == "information":
|
||||
row.operator("bim.edit_information", text="", icon="CHECKMARK")
|
||||
elif context.scene.BIMDocumentProperties.is_editing == "reference":
|
||||
row.operator("bim.edit_document_reference", text="", icon="CHECKMARK")
|
||||
row.operator("bim.disable_editing_document", text="", icon="CANCEL")
|
||||
elif context.scene.BIMDocumentProperties.active_document_id:
|
||||
row.operator("bim.remove_document", text="", icon="X").document = item.ifc_definition_id
|
||||
else:
|
||||
op = row.operator("bim.enable_editing_document", text="", icon="GREASEPENCIL")
|
||||
|
||||
if item.is_information:
|
||||
op = row.operator("bim.load_document", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT")
|
||||
op.document = item.ifc_definition_id
|
||||
row.operator("bim.remove_document", text="", icon="X").document = item.ifc_definition_id
|
||||
row.label(text="", icon="FILE")
|
||||
else:
|
||||
row.label(text="", icon="BLANK1")
|
||||
row.label(text="", icon="FILE_HIDDEN")
|
||||
|
||||
|
||||
class BIM_UL_object_documents(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
row.label(text=item.identification)
|
||||
row.label(text=item.name)
|
||||
row.operator("bim.assign_document", text="", icon="ADD").document = item.ifc_definition_id
|
||||
split1 = row.split(factor=0.1)
|
||||
split1.label(text=item.identification)
|
||||
split2 = split1.split(factor=0.9)
|
||||
split2.label(text=item.name)
|
||||
|
||||
@@ -20,40 +20,48 @@ import bpy
|
||||
from . import ui, prop, operator, handler, gizmos
|
||||
|
||||
classes = (
|
||||
operator.AddDrawing,
|
||||
operator.CreateDrawing,
|
||||
operator.AddAnnotation,
|
||||
operator.AddSheet,
|
||||
operator.OpenSheet,
|
||||
operator.AddDrawingToSheet,
|
||||
operator.CreateSheets,
|
||||
operator.OpenView,
|
||||
operator.OpenViewCamera,
|
||||
operator.ActivateView,
|
||||
operator.SelectDocIfcFile,
|
||||
operator.GenerateReferences,
|
||||
operator.ResizeText,
|
||||
operator.AddVariable,
|
||||
operator.RemoveVariable,
|
||||
operator.PropagateTextData,
|
||||
operator.RemoveDrawing,
|
||||
operator.AddDrawingStyle,
|
||||
operator.RemoveDrawingStyle,
|
||||
operator.SaveDrawingStyle,
|
||||
operator.ActivateDrawingStyle,
|
||||
operator.EditVectorStyle,
|
||||
operator.RemoveSheet,
|
||||
operator.AddSchedule,
|
||||
operator.RemoveSchedule,
|
||||
operator.SelectScheduleFile,
|
||||
operator.BuildSchedule,
|
||||
operator.AddScheduleToSheet,
|
||||
operator.ActivateView,
|
||||
operator.AddAnnotation,
|
||||
operator.AddDrawing,
|
||||
operator.AddDrawingStyle,
|
||||
operator.AddDrawingStyleAttribute,
|
||||
operator.RemoveDrawingStyleAttribute,
|
||||
operator.RefreshDrawingList,
|
||||
operator.CleanWireframes,
|
||||
operator.CopyGrid,
|
||||
operator.AddDrawingToSheet,
|
||||
operator.AddSchedule,
|
||||
operator.AddScheduleToSheet,
|
||||
operator.AddSectionsAnnotations,
|
||||
operator.AddSheet,
|
||||
operator.BuildSchedule,
|
||||
operator.CleanWireframes,
|
||||
operator.ContractSheet,
|
||||
operator.CopyGrid,
|
||||
operator.CreateDrawing,
|
||||
operator.CreateSheets,
|
||||
operator.DisableEditingDrawings,
|
||||
operator.DisableEditingSheets,
|
||||
operator.DisableEditingText,
|
||||
operator.DisableEditingTextProduct,
|
||||
operator.EditText,
|
||||
operator.EditTextProduct,
|
||||
operator.EditVectorStyle,
|
||||
operator.EnableEditingText,
|
||||
operator.EnableEditingTextProduct,
|
||||
operator.ExpandSheet,
|
||||
operator.GenerateReferences,
|
||||
operator.LoadDrawings,
|
||||
operator.LoadSheets,
|
||||
operator.OpenSheet,
|
||||
operator.OpenView,
|
||||
operator.RemoveDrawing,
|
||||
operator.RemoveDrawingFromSheet,
|
||||
operator.RemoveDrawingStyle,
|
||||
operator.RemoveDrawingStyleAttribute,
|
||||
operator.RemoveSchedule,
|
||||
operator.RemoveSheet,
|
||||
operator.ResizeText,
|
||||
operator.SaveDrawingStyle,
|
||||
operator.SelectDocIfcFile,
|
||||
operator.SelectScheduleFile,
|
||||
prop.Variable,
|
||||
prop.Drawing,
|
||||
prop.Schedule,
|
||||
@@ -64,12 +72,13 @@ classes = (
|
||||
prop.BIMTextProperties,
|
||||
ui.BIM_PT_camera,
|
||||
ui.BIM_PT_drawing_underlay,
|
||||
ui.BIM_PT_annotation_utilities,
|
||||
ui.BIM_PT_sheets,
|
||||
ui.BIM_PT_drawings,
|
||||
ui.BIM_PT_schedules,
|
||||
ui.BIM_PT_sheets,
|
||||
ui.BIM_PT_text,
|
||||
ui.BIM_PT_annotation_utilities,
|
||||
ui.BIM_UL_drawinglist,
|
||||
ui.BIM_UL_sheets,
|
||||
gizmos.UglyDotGizmo,
|
||||
gizmos.DotGizmo,
|
||||
gizmos.DimensionLabelGizmo,
|
||||
@@ -81,6 +90,7 @@ classes = (
|
||||
def register():
|
||||
bpy.types.Scene.DocProperties = bpy.props.PointerProperty(type=prop.DocProperties)
|
||||
bpy.types.Camera.BIMCameraProperties = bpy.props.PointerProperty(type=prop.BIMCameraProperties)
|
||||
bpy.types.Object.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties)
|
||||
bpy.types.TextCurve.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties)
|
||||
bpy.app.handlers.load_post.append(handler.toggleDecorationsOnLoad)
|
||||
bpy.app.handlers.depsgraph_update_pre.append(handler.depsgraph_update_pre_handler)
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import bpy
|
||||
import os
|
||||
import blenderbim.tool as tool
|
||||
from mathutils import Vector
|
||||
|
||||
|
||||
@@ -34,13 +35,13 @@ class Annotator:
|
||||
return float(sizes[str(size)])
|
||||
|
||||
@staticmethod
|
||||
def add_text(context, related_element=None):
|
||||
def add_text(related_element=None):
|
||||
curve = bpy.data.curves.new(type="FONT", name="Text")
|
||||
curve.body = "TEXT"
|
||||
obj = bpy.data.objects.new("Text", curve)
|
||||
obj.matrix_world = context.scene.camera.matrix_world
|
||||
obj.matrix_world = bpy.context.scene.camera.matrix_world
|
||||
if related_element is None:
|
||||
location, _, _, _ = Annotator.get_placeholder_coords(context)
|
||||
location, _, _, _ = Annotator.get_placeholder_coords(bpy.context)
|
||||
else:
|
||||
obj.data.BIMTextProperties.related_element = related_element
|
||||
location = related_element.location
|
||||
@@ -49,12 +50,12 @@ class Annotator:
|
||||
font = bpy.data.fonts.get("OpenGost TypeB TT")
|
||||
if not font:
|
||||
font = bpy.data.fonts.load(
|
||||
os.path.join(context.scene.BIMProperties.data_dir, "fonts", "OpenGost Type B TT.ttf")
|
||||
os.path.join(bpy.context.scene.BIMProperties.data_dir, "fonts", "OpenGost Type B TT.ttf")
|
||||
)
|
||||
font.name = "OpenGost Type B TT"
|
||||
obj.data.font = font
|
||||
obj.data.BIMTextProperties.font_size = "2.5"
|
||||
collection = context.scene.camera.users_collection[0]
|
||||
collection = bpy.context.scene.camera.users_collection[0]
|
||||
collection.objects.link(obj)
|
||||
Annotator.resize_text(obj)
|
||||
return obj
|
||||
@@ -82,9 +83,9 @@ class Annotator:
|
||||
text_obj.data.size = font_size
|
||||
|
||||
@staticmethod
|
||||
def add_line_to_annotation(obj, context, co1=None, co2=None):
|
||||
def add_line_to_annotation(obj, co1=None, co2=None):
|
||||
if co1 is None:
|
||||
co1, co2, _, _ = Annotator.get_placeholder_coords(context)
|
||||
co1, co2, _, _ = Annotator.get_placeholder_coords()
|
||||
co1 = obj.matrix_world.inverted() @ co1
|
||||
co2 = obj.matrix_world.inverted() @ co2
|
||||
if isinstance(obj.data, bpy.types.Mesh):
|
||||
@@ -101,8 +102,8 @@ class Annotator:
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def add_plane_to_annotation(obj, context):
|
||||
co1, co2, co3, co4 = Annotator.get_placeholder_coords(context)
|
||||
def add_plane_to_annotation(obj):
|
||||
co1, co2, co3, co4 = Annotator.get_placeholder_coords()
|
||||
co1 = obj.matrix_world.inverted() @ co1 # bot left
|
||||
co2 = obj.matrix_world.inverted() @ co2 # top left
|
||||
co3 = obj.matrix_world.inverted() @ co3 # bot right
|
||||
@@ -150,27 +151,43 @@ class Annotator:
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def get_annotation_obj(name, data_type, context):
|
||||
collection = context.scene.camera.users_collection[0]
|
||||
for obj in collection.objects:
|
||||
if name in obj.name:
|
||||
return obj
|
||||
if data_type == "mesh":
|
||||
data = bpy.data.meshes.new(name)
|
||||
elif data_type == "curve":
|
||||
data = bpy.data.curves.new(name, type="CURVE")
|
||||
def get_annotation_obj(object_type, data_type):
|
||||
collection = bpy.context.scene.camera.users_collection[0]
|
||||
if object_type == "TEXT":
|
||||
obj = bpy.data.objects.new(object_type, None)
|
||||
collection.objects.link(obj)
|
||||
return obj
|
||||
elif object_type == "TEXT_LEADER":
|
||||
data = bpy.data.curves.new(object_type, type="CURVE")
|
||||
data.dimensions = "3D"
|
||||
data.resolution_u = 2
|
||||
obj = bpy.data.objects.new(name, data)
|
||||
obj = bpy.data.objects.new(object_type, data)
|
||||
collection.objects.link(obj)
|
||||
return obj
|
||||
for obj in collection.objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element and element.ObjectType == object_type:
|
||||
return obj
|
||||
if data_type == "mesh":
|
||||
data = bpy.data.meshes.new(object_type)
|
||||
elif data_type == "curve":
|
||||
data = bpy.data.curves.new(object_type, type="CURVE")
|
||||
data.dimensions = "3D"
|
||||
data.resolution_u = 2
|
||||
obj = bpy.data.objects.new(object_type, data)
|
||||
collection.objects.link(obj)
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def get_placeholder_coords(context):
|
||||
camera = context.scene.camera
|
||||
def get_placeholder_coords():
|
||||
camera = bpy.context.scene.camera
|
||||
z_offset = camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
|
||||
if context.scene.render.resolution_x > context.scene.render.resolution_y:
|
||||
y = camera.data.ortho_scale * (context.scene.render.resolution_y / context.scene.render.resolution_x) / 4
|
||||
if bpy.context.scene.render.resolution_x > bpy.context.scene.render.resolution_y:
|
||||
y = (
|
||||
camera.data.ortho_scale
|
||||
* (bpy.context.scene.render.resolution_y / bpy.context.scene.render.resolution_x)
|
||||
/ 4
|
||||
)
|
||||
else:
|
||||
y = camera.data.ortho_scale / 4
|
||||
y_offset = camera.matrix_world.to_quaternion() @ Vector((0, y, 0))
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import ifcopenshell.util.representation
|
||||
import blenderbim.tool as tool
|
||||
|
||||
|
||||
def refresh():
|
||||
TextData.is_loaded = False
|
||||
SheetsData.is_loaded = False
|
||||
DrawingsData.is_loaded = False
|
||||
|
||||
|
||||
class TextData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {"attributes": cls.attributes(), "relating_product": cls.relating_product()}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def attributes(cls):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
if not element or not element.is_a("IfcAnnotation") or element.ObjectType not in ["TEXT", "TEXT_LEADER"]:
|
||||
return []
|
||||
rep = ifcopenshell.util.representation.get_representation(element, "Plan", "Annotation")
|
||||
text_literal = [i for i in rep.Items if i.is_a("IfcTextLiteral")][0]
|
||||
return [
|
||||
{"name": "Literal", "value": text_literal.Literal},
|
||||
{"name": "BoxAlignment", "value": text_literal.BoxAlignment},
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def relating_product(cls):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
if not element or not element.is_a("IfcAnnotation") or element.ObjectType not in ["TEXT", "TEXT_LEADER"]:
|
||||
return
|
||||
for rel in element.HasAssignments:
|
||||
if rel.is_a("IfcRelAssignsToProduct"):
|
||||
return rel.RelatingProduct.Name or "Unnamed"
|
||||
|
||||
|
||||
class SheetsData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {"total_sheets": cls.total_sheets()}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def total_sheets(cls):
|
||||
return len([d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "DOCUMENTATION"])
|
||||
|
||||
|
||||
class DrawingsData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {"total_drawings": cls.total_drawings(), "location_hint": cls.location_hint()}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def total_drawings(cls):
|
||||
return len([e for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"])
|
||||
|
||||
@classmethod
|
||||
def location_hint(cls):
|
||||
if bpy.context.scene.DocProperties.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]:
|
||||
results = [("0", "Origin", "")]
|
||||
results.extend(
|
||||
[(str(s.id()), s.Name or "Unnamed", "") for s in tool.Ifc.get().by_type("IfcBuildingStorey")]
|
||||
)
|
||||
return results
|
||||
return [(h.upper(), h, "") for h in ["North", "South", "East", "West"]]
|
||||
@@ -16,32 +16,32 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Viewport decorations"""
|
||||
import os
|
||||
import gpu
|
||||
import bpy
|
||||
import blf
|
||||
import bgl
|
||||
import math
|
||||
import bmesh
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.bim.module.drawing.helper as helper
|
||||
from functools import reduce
|
||||
from itertools import chain
|
||||
|
||||
from bpy.types import SpaceView3D
|
||||
from mathutils import Vector, Matrix
|
||||
import bpy
|
||||
import bmesh
|
||||
import blf
|
||||
from bpy_extras.view3d_utils import location_3d_to_region_2d
|
||||
import gpu
|
||||
import bgl
|
||||
from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
import blenderbim.bim.module.drawing.helper as helper
|
||||
|
||||
|
||||
class BaseDecorator:
|
||||
# base name of objects to decorate
|
||||
basename = "IfcAnnotation/Something"
|
||||
objecttype = "NOTDEFINED"
|
||||
|
||||
DEF_GLSL = """
|
||||
#define PI 3.141592653589793
|
||||
#define MAX_POINTS 64
|
||||
#define CIRCLE_SEGS 24
|
||||
#define CIRCLE_SEGS 12
|
||||
"""
|
||||
|
||||
LIB_GLSL = """
|
||||
@@ -90,6 +90,7 @@ class BaseDecorator:
|
||||
|
||||
GEOM_GLSL = """
|
||||
uniform vec2 winsize;
|
||||
uniform float viewportDrawingScale;
|
||||
|
||||
layout(lines) in;
|
||||
layout(line_strip, max_vertices=2) out;
|
||||
@@ -134,13 +135,48 @@ class BaseDecorator:
|
||||
defines=self.DEF_GLSL,
|
||||
)
|
||||
|
||||
# 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
|
||||
|
||||
def get_scale(camera):
|
||||
if camera.data.BIMCameraProperties.diagram_scale == "CUSTOM":
|
||||
human_scale, fraction = camera.data.BIMCameraProperties.custom_diagram_scale.split("|")
|
||||
else:
|
||||
human_scale, fraction = camera.data.BIMCameraProperties.diagram_scale.split("|")
|
||||
numerator, denominator = fraction.split("/")
|
||||
return float(numerator) / float(denominator)
|
||||
|
||||
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
|
||||
self.camera_width_mm = get_scale(camera) * camera_width_model
|
||||
|
||||
self.font_id = blf.load(
|
||||
os.path.join(bpy.context.scene.BIMProperties.data_dir, "fonts", "OpenGost Type B TT.ttf")
|
||||
)
|
||||
|
||||
def camera_zoom_to_factor(self, zoom):
|
||||
return math.pow(((zoom / 50) + math.sqrt(2)) / 2, 2)
|
||||
|
||||
def get_objects(self, collection):
|
||||
"""find relevant objects
|
||||
using class.basename
|
||||
using class.objecttype
|
||||
|
||||
returns: iterable of blender objects
|
||||
"""
|
||||
return filter(lambda o: self.basename in o.name, collection.all_objects)
|
||||
results = []
|
||||
for obj in collection.all_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
if element.is_a("IfcAnnotation") and element.ObjectType == self.objecttype:
|
||||
results.append(obj)
|
||||
return results
|
||||
|
||||
def get_path_geom(self, obj, topo=True):
|
||||
"""Parses path geometry into line segments
|
||||
@@ -208,7 +244,7 @@ class BaseDecorator:
|
||||
return vertices, indices
|
||||
|
||||
def decorate(self, context, object):
|
||||
"""perform actuall drawing stuff"""
|
||||
"""perform actual drawing stuff"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def draw_lines(self, context, obj, vertices, indices, topology=None):
|
||||
@@ -241,6 +277,15 @@ class BaseDecorator:
|
||||
self.shader.uniform_float("winsize", (region.width, region.height))
|
||||
self.shader.uniform_float("color", color)
|
||||
|
||||
# Horrific prototype code
|
||||
factor = self.camera_zoom_to_factor(context.space_data.region_3d.view_camera_zoom)
|
||||
camera_width_px = factor * context.region.width
|
||||
mm_to_px = camera_width_px / self.camera_width_mm
|
||||
# 0.00025 is a magic constant number I visually discovered to get the right number.
|
||||
# It probably should be dynamically calculated using system.dpi or something.
|
||||
viewport_drawing_scale = 0.00025 * mm_to_px
|
||||
self.shader.uniform_float("viewportDrawingScale", viewport_drawing_scale)
|
||||
|
||||
batch.draw(self.shader)
|
||||
|
||||
def draw_label(self, context, text, pos, dir, gap=4, center=True, vcenter=False):
|
||||
@@ -251,8 +296,9 @@ class BaseDecorator:
|
||||
|
||||
aligned and centered at segment middle
|
||||
"""
|
||||
font_id = 0
|
||||
font_size = 16
|
||||
# 0 is the default font, but we're fancier than that
|
||||
font_id = self.font_id
|
||||
|
||||
dpi = context.preferences.system.dpi
|
||||
|
||||
color = context.scene.DocProperties.decorations_colour
|
||||
@@ -261,6 +307,16 @@ class BaseDecorator:
|
||||
cos = math.cos(ang)
|
||||
sin = math.sin(ang)
|
||||
|
||||
# Horrific prototype code
|
||||
factor = self.camera_zoom_to_factor(context.space_data.region_3d.view_camera_zoom)
|
||||
camera_width_px = factor * context.region.width
|
||||
mm_to_px = camera_width_px / self.camera_width_mm
|
||||
# 0.004118616 is a magic constant number I visually discovered to get the right number.
|
||||
# In particular it works only for the OpenGOST font and produces a 2.5mm font size.
|
||||
# It probably should be dynamically calculated using system.dpi or something.
|
||||
# font_size = 16 <-- this is a good default
|
||||
font_size = int(0.004118616 * mm_to_px)
|
||||
|
||||
blf.size(font_id, font_size, dpi)
|
||||
|
||||
w, h = 0, 0
|
||||
@@ -284,6 +340,9 @@ class BaseDecorator:
|
||||
|
||||
blf.rotation(font_id, ang)
|
||||
blf.color(font_id, *color)
|
||||
# blf.enable(font_id, blf.SHADOW)
|
||||
# blf.shadow(font_id, 5, 0, 0, 0, 1)
|
||||
# blf.shadow_offset(font_id, 1, -1)
|
||||
blf.draw(font_id, text)
|
||||
blf.disable(font_id, blf.ROTATION)
|
||||
|
||||
@@ -302,7 +361,7 @@ class BaseDecorator:
|
||||
else:
|
||||
return
|
||||
|
||||
return bpy.utils.units.to_string(unit_system, "LENGTH", value, precision, split_unit=True)
|
||||
return bpy.utils.units.to_string(unit_system, "LENGTH", value, precision=precision, split_unit=True)
|
||||
|
||||
|
||||
class DimensionDecorator(BaseDecorator):
|
||||
@@ -311,7 +370,7 @@ class DimensionDecorator(BaseDecorator):
|
||||
- puts metric text next to each segment
|
||||
"""
|
||||
|
||||
basename = "IfcAnnotation/Dimension"
|
||||
objecttype = "DIMENSION"
|
||||
|
||||
DEF_GLSL = (
|
||||
BaseDecorator.DEF_GLSL
|
||||
@@ -323,6 +382,7 @@ class DimensionDecorator(BaseDecorator):
|
||||
|
||||
GEOM_GLSL = """
|
||||
uniform vec2 winsize;
|
||||
uniform float viewportDrawingScale;
|
||||
|
||||
layout(lines) in;
|
||||
layout(line_strip, max_vertices=MAX_POINTS) out;
|
||||
@@ -339,7 +399,7 @@ class DimensionDecorator(BaseDecorator):
|
||||
vec4 p;
|
||||
|
||||
vec4 head[3];
|
||||
arrow_head(dir, ARROW_SIZE, ARROW_ANGLE, head);
|
||||
arrow_head(dir, viewportDrawingScale * ARROW_SIZE, ARROW_ANGLE, head);
|
||||
|
||||
// start edge arrow
|
||||
gl_Position = p0;
|
||||
@@ -406,7 +466,7 @@ class EqualityDecorator(DimensionDecorator):
|
||||
- puts 'EQ' label
|
||||
"""
|
||||
|
||||
basename = "IfcAnnotation/Equal"
|
||||
objecttype = "EQUAL_DIMENSION"
|
||||
|
||||
def draw_labels(self, context, obj, vertices, indices):
|
||||
region = context.region
|
||||
@@ -423,12 +483,11 @@ class EqualityDecorator(DimensionDecorator):
|
||||
|
||||
|
||||
class LeaderDecorator(BaseDecorator):
|
||||
"""Decorating stairs
|
||||
"""Decorating text with arrows
|
||||
- head point with arrow
|
||||
- middle points w/out decorations
|
||||
"""
|
||||
|
||||
basename = "IfcAnnotation/Leader"
|
||||
objecttype = "TEXT_LEADER"
|
||||
|
||||
DEF_GLSL = (
|
||||
BaseDecorator.DEF_GLSL
|
||||
@@ -440,6 +499,7 @@ class LeaderDecorator(BaseDecorator):
|
||||
|
||||
GEOM_GLSL = """
|
||||
uniform vec2 winsize;
|
||||
uniform float viewportDrawingScale;
|
||||
|
||||
layout(lines) in;
|
||||
layout(line_strip, max_vertices=MAX_POINTS) out;
|
||||
@@ -461,7 +521,7 @@ class LeaderDecorator(BaseDecorator):
|
||||
// end edge arrow for last segment
|
||||
if (t1 == 2u) {
|
||||
vec4 head[3];
|
||||
arrow_head(dir, ARROW_SIZE, ARROW_ANGLE, head);
|
||||
arrow_head(dir, viewportDrawingScale * ARROW_SIZE, ARROW_ANGLE, head);
|
||||
|
||||
gl_Position = p1;
|
||||
EmitVertex();
|
||||
@@ -475,7 +535,7 @@ class LeaderDecorator(BaseDecorator):
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
|
||||
gap1 = dir * ARROW_SIZE;
|
||||
gap1 = dir * viewportDrawingScale * ARROW_SIZE;
|
||||
}
|
||||
|
||||
// stem, adjusted for and arrow
|
||||
@@ -488,9 +548,24 @@ class LeaderDecorator(BaseDecorator):
|
||||
}
|
||||
"""
|
||||
|
||||
def get_spline_end(self, obj):
|
||||
spline = obj.data.splines[0]
|
||||
spline_points = spline.bezier_points if spline.bezier_points else spline.points
|
||||
if not spline_points:
|
||||
return Vector((0, 0, 0))
|
||||
return obj.matrix_world @ spline_points[0].co
|
||||
|
||||
def decorate(self, context, obj):
|
||||
verts, idxs, topo = self.get_path_geom(obj)
|
||||
self.draw_lines(context, obj, verts, idxs, topo)
|
||||
self.draw_labels(context, obj)
|
||||
|
||||
def draw_labels(self, context, obj):
|
||||
region = context.region
|
||||
region3d = context.region_data
|
||||
dir = Vector((1, 0))
|
||||
pos = location_3d_to_region_2d(region, region3d, self.get_spline_end(obj))
|
||||
self.draw_label(context, obj.BIMTextProperties.value, pos, dir, gap=0, center=False, vcenter=False)
|
||||
|
||||
|
||||
class StairDecorator(BaseDecorator):
|
||||
@@ -500,12 +575,12 @@ class StairDecorator(BaseDecorator):
|
||||
- middle points w/out decorations
|
||||
"""
|
||||
|
||||
basename = "IfcAnnotation/Stair"
|
||||
objecttype = "STAIR_ARROW"
|
||||
|
||||
DEF_GLSL = (
|
||||
BaseDecorator.DEF_GLSL
|
||||
+ """
|
||||
#define CIRCLE_SIZE 8.0
|
||||
#define CIRCLE_SIZE 6.0
|
||||
#define ARROW_ANGLE PI / 3.0
|
||||
#define ARROW_SIZE 24.0
|
||||
"""
|
||||
@@ -513,6 +588,7 @@ class StairDecorator(BaseDecorator):
|
||||
|
||||
GEOM_GLSL = """
|
||||
uniform vec2 winsize;
|
||||
uniform float viewportDrawingScale;
|
||||
|
||||
layout(lines) in;
|
||||
layout(line_strip, max_vertices=MAX_POINTS) out;
|
||||
@@ -534,7 +610,7 @@ class StairDecorator(BaseDecorator):
|
||||
// start edge circle for first segment
|
||||
if (t0 == 1u) {
|
||||
vec4 head[CIRCLE_SEGS];
|
||||
circle_head(CIRCLE_SIZE, head);
|
||||
circle_head(viewportDrawingScale * CIRCLE_SIZE, head);
|
||||
|
||||
for(int i=0; i<CIRCLE_SEGS; i++) {
|
||||
p = p0w + head[i];
|
||||
@@ -546,13 +622,13 @@ class StairDecorator(BaseDecorator):
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
|
||||
gap0 = dir * CIRCLE_SIZE;
|
||||
gap0 = dir * viewportDrawingScale * CIRCLE_SIZE;
|
||||
}
|
||||
|
||||
// end edge arrow for last segment
|
||||
if (t1 == 2u) {
|
||||
vec4 head[3];
|
||||
arrow_head(dir, ARROW_SIZE, ARROW_ANGLE, head);
|
||||
arrow_head(dir, viewportDrawingScale * ARROW_SIZE, ARROW_ANGLE, head);
|
||||
|
||||
p = p1w - head[1];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
@@ -583,7 +659,7 @@ class StairDecorator(BaseDecorator):
|
||||
|
||||
|
||||
class HiddenDecorator(BaseDecorator):
|
||||
basename = "IfcAnnotation/Hidden"
|
||||
objecttype = "HIDDEN_LINE"
|
||||
|
||||
DEF_GLSL = (
|
||||
BaseDecorator.DEF_GLSL
|
||||
@@ -628,13 +704,14 @@ class HiddenDecorator(BaseDecorator):
|
||||
"""
|
||||
|
||||
FRAG_GLSL = """
|
||||
uniform float viewportDrawingScale;
|
||||
uniform vec4 color;
|
||||
in vec2 gl_FragCoord;
|
||||
in float dist;
|
||||
out vec4 fragColor;
|
||||
|
||||
void main() {
|
||||
uint bit = uint(fract(dist / DASH_SIZE) * 32);
|
||||
uint bit = uint(fract(dist / (viewportDrawingScale * DASH_SIZE)) * 32);
|
||||
if ((DASH_PATTERN & (1U<<bit)) == 0U) discard;
|
||||
fragColor = color;
|
||||
}
|
||||
@@ -649,7 +726,7 @@ class HiddenDecorator(BaseDecorator):
|
||||
|
||||
|
||||
class MiscDecorator(HiddenDecorator):
|
||||
basename = "IfcAnnotation/Misc"
|
||||
objecttype = "MISC"
|
||||
|
||||
FRAG_GLSL = BaseDecorator.FRAG_GLSL
|
||||
|
||||
@@ -677,7 +754,7 @@ class LevelDecorator(BaseDecorator):
|
||||
|
||||
|
||||
class PlanLevelDecorator(LevelDecorator):
|
||||
basename = "IfcAnnotation/Plan Level"
|
||||
objecttype = "PLAN_LEVEL"
|
||||
|
||||
DEF_GLSL = (
|
||||
BaseDecorator.DEF_GLSL
|
||||
@@ -689,6 +766,7 @@ class PlanLevelDecorator(LevelDecorator):
|
||||
|
||||
GEOM_GLSL = """
|
||||
uniform vec2 winsize;
|
||||
uniform float viewportDrawingScale;
|
||||
|
||||
layout(lines) in;
|
||||
layout(line_strip, max_vertices=MAX_POINTS) out;
|
||||
@@ -710,7 +788,7 @@ class PlanLevelDecorator(LevelDecorator):
|
||||
// end edge cross+circle for last segment
|
||||
if (t1 == 2u) {
|
||||
vec4 head_o[CIRCLE_SEGS];
|
||||
circle_head(CIRCLE_SIZE, head_o);
|
||||
circle_head(viewportDrawingScale * CIRCLE_SIZE, head_o);
|
||||
|
||||
for(int i=0; i<CIRCLE_SEGS; i++) {
|
||||
p = p1w + head_o[i];
|
||||
@@ -723,7 +801,7 @@ class PlanLevelDecorator(LevelDecorator):
|
||||
EndPrimitive();
|
||||
|
||||
vec4 head_x[3];
|
||||
cross_head(dir, CROSS_SIZE, head_x);
|
||||
cross_head(dir, viewportDrawingScale * CROSS_SIZE, head_x);
|
||||
|
||||
p = p1w + head_x[1];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
@@ -762,7 +840,7 @@ class PlanLevelDecorator(LevelDecorator):
|
||||
|
||||
|
||||
class SectionLevelDecorator(LevelDecorator):
|
||||
basename = "IfcAnnotation/Section Level"
|
||||
objecttype = "SECTION_LEVEL"
|
||||
|
||||
DEF_GLSL = (
|
||||
BaseDecorator.DEF_GLSL
|
||||
@@ -774,6 +852,7 @@ class SectionLevelDecorator(LevelDecorator):
|
||||
|
||||
GEOM_GLSL = """
|
||||
uniform vec2 winsize;
|
||||
uniform float viewportDrawingScale;
|
||||
|
||||
layout(lines) in;
|
||||
layout(line_strip, max_vertices=MAX_POINTS) out;
|
||||
@@ -781,8 +860,8 @@ class SectionLevelDecorator(LevelDecorator):
|
||||
out float dist; // distance from starging point along segment
|
||||
|
||||
void callout_head(in vec4 dir, out vec4 head[4]) {
|
||||
vec2 gap = dir.xy * CALLOUT_GAP;
|
||||
vec2 tail = dir.xy * -CALLOUT_SIZE;
|
||||
vec2 gap = dir.xy * viewportDrawingScale * CALLOUT_GAP;
|
||||
vec2 tail = dir.xy * viewportDrawingScale * -CALLOUT_SIZE;
|
||||
vec2 side = cross(vec3(dir.xy, 0), vec3(0, 0, 1)).xy * CALLOUT_GAP;
|
||||
|
||||
head[0] = vec4(tail + side, 0, 0);
|
||||
@@ -850,7 +929,7 @@ class BreakDecorator(BaseDecorator):
|
||||
Uses first two vertices in verts list.
|
||||
"""
|
||||
|
||||
basename = "IfcAnnotation/Break"
|
||||
objecttype = "BREAKLINE"
|
||||
|
||||
DEF_GLSL = (
|
||||
BaseDecorator.DEF_GLSL
|
||||
@@ -862,6 +941,7 @@ class BreakDecorator(BaseDecorator):
|
||||
|
||||
GEOM_GLSL = """
|
||||
uniform vec2 winsize;
|
||||
uniform float viewportDrawingScale;
|
||||
|
||||
layout(lines) in;
|
||||
layout(line_strip, max_vertices=MAX_POINTS) out;
|
||||
@@ -878,8 +958,8 @@ class BreakDecorator(BaseDecorator):
|
||||
|
||||
vec4 p;
|
||||
|
||||
vec4 quart = dir * BREAK_LENGTH * .25;
|
||||
vec4 side = vec4(cross(vec3(dir.xy, 0), vec3(0, 0, 1)).xy, 0, 0) * BREAK_WIDTH;
|
||||
vec4 quart = dir * viewportDrawingScale * BREAK_LENGTH * .25;
|
||||
vec4 side = vec4(cross(vec3(dir.xy, 0), vec3(0, 0, 1)).xy, 0, 0) * viewportDrawingScale * BREAK_WIDTH;
|
||||
|
||||
// TODO: check if there's enough length for zigzag
|
||||
|
||||
@@ -928,7 +1008,7 @@ class BreakDecorator(BaseDecorator):
|
||||
|
||||
|
||||
class GridDecorator(BaseDecorator):
|
||||
basename = "IfcGridAxis/"
|
||||
objecttype = "GRID"
|
||||
|
||||
DEF_GLSL = (
|
||||
BaseDecorator.DEF_GLSL
|
||||
@@ -941,6 +1021,7 @@ class GridDecorator(BaseDecorator):
|
||||
|
||||
GEOM_GLSL = """
|
||||
uniform vec2 winsize;
|
||||
uniform float viewportDrawingScale;
|
||||
|
||||
layout(lines) in;
|
||||
layout(line_strip, max_vertices=MAX_POINTS) out;
|
||||
@@ -955,12 +1036,12 @@ class GridDecorator(BaseDecorator):
|
||||
|
||||
vec4 p0w = CLIP2WIN(p0), p1w = CLIP2WIN(p1);
|
||||
vec4 edge = p1w - p0w, dir = normalize(edge);
|
||||
vec4 gap = dir * CIRCLE_SIZE;
|
||||
vec4 gap = dir * CIRCLE_SIZE * viewportDrawingScale;
|
||||
|
||||
vec4 p;
|
||||
|
||||
vec4 head[CIRCLE_SEGS];
|
||||
circle_head(CIRCLE_SIZE, head);
|
||||
circle_head(CIRCLE_SIZE * viewportDrawingScale, head);
|
||||
|
||||
dist = 0;
|
||||
|
||||
@@ -1044,30 +1125,33 @@ class GridDecorator(BaseDecorator):
|
||||
self.draw_label(context, text, p1, dir, vcenter=True, gap=0)
|
||||
|
||||
|
||||
class SectionViewDecorator(LevelDecorator):
|
||||
basename = "IfcAnnotation/Section"
|
||||
class ElevationDecorator(LevelDecorator):
|
||||
objecttype = "ELEVATION"
|
||||
|
||||
DEF_GLSL = (
|
||||
BaseDecorator.DEF_GLSL
|
||||
+ """
|
||||
#define CIRCLE_SIZE 8.0
|
||||
#define TRIANGLE_L 32.0
|
||||
#define TRIANGLE_W 16.0
|
||||
#define TRIANGLE_L 22.63
|
||||
#define TRIANGLE_W 11.31
|
||||
"""
|
||||
)
|
||||
|
||||
GEOM_GLSL = """
|
||||
uniform vec2 winsize;
|
||||
uniform float viewportDrawingScale;
|
||||
|
||||
layout(lines) in;
|
||||
layout(line_strip, max_vertices=MAX_POINTS) out;
|
||||
|
||||
void triangle_head(in vec4 dir, in vec4 side, in float length, in float width, out vec4 head[3]) {
|
||||
vec4 nose = dir * length;
|
||||
vec4 ear = side * width;
|
||||
head[0] = vec4(0);
|
||||
head[1] = nose * .5 + ear;
|
||||
head[2] = nose;
|
||||
void triangle_head(in vec4 side, in vec4 dir, in float length, in float width, in float radius, out vec4 head[5]) {
|
||||
vec4 nose = side * length;
|
||||
vec4 ear = dir * width;
|
||||
head[0] = side * -radius;
|
||||
head[1] = nose * -.5;
|
||||
head[2] = vec4(0) + ear;
|
||||
head[3] = nose * .5;
|
||||
head[4] = side * radius;
|
||||
}
|
||||
|
||||
void main() {
|
||||
@@ -1078,75 +1162,290 @@ class SectionViewDecorator(LevelDecorator):
|
||||
|
||||
vec4 p0w = CLIP2WIN(p0), p1w = CLIP2WIN(p1);
|
||||
vec4 edge = p1w - p0w, dir = normalize(edge);
|
||||
vec4 gap = dir * TRIANGLE_L * .5;
|
||||
vec4 side = vec4(cross(vec3(dir.xy, 0), vec3(0, 0, 1)).xy, 0, 0);
|
||||
vec4 p;
|
||||
|
||||
vec4 head[CIRCLE_SEGS];
|
||||
circle_head(CIRCLE_SIZE, head);
|
||||
circle_head(viewportDrawingScale * CIRCLE_SIZE, head);
|
||||
|
||||
vec4 head3[3];
|
||||
vec4 head5[5];
|
||||
|
||||
// start edge circle
|
||||
for(int i=0; i<CIRCLE_SEGS; i++) {
|
||||
p = p0w + gap + head[i];
|
||||
p = p0w + head[i];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
}
|
||||
p = p0w + gap + head[0];
|
||||
p = p0w + head[0];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
|
||||
// start edge triangle
|
||||
triangle_head(dir, -side, TRIANGLE_L, TRIANGLE_W, head3);
|
||||
p = p0w + head3[0];
|
||||
// edge triangle
|
||||
triangle_head(side, dir, viewportDrawingScale * TRIANGLE_L, viewportDrawingScale * TRIANGLE_W, viewportDrawingScale * CIRCLE_SIZE, head5);
|
||||
p = p0w + head5[0];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
p = p0w + head3[1];
|
||||
p = p0w + head5[1];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
p = p0w + head3[2];
|
||||
p = p0w + head5[2];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
p = p0w + head5[3];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
p = p0w + head5[4];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
|
||||
// end edge circle
|
||||
for(int i=0; i<CIRCLE_SEGS; i++) {
|
||||
p = p1w - gap + head[i];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
}
|
||||
p = p1w - gap + head[0];
|
||||
// reference divider
|
||||
p = p0w + normalize(vec4(-1, 0, 0, 0)) * viewportDrawingScale * CIRCLE_SIZE;
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
|
||||
// end edge triangle
|
||||
triangle_head(-dir, -side, TRIANGLE_L, TRIANGLE_W, head3);
|
||||
p = p1w + head3[0];
|
||||
p = p0w + normalize(vec4(1, 0, 0, 0)) * viewportDrawingScale * CIRCLE_SIZE;
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
p = p1w + head3[1];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
p = p1w + head3[2];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
|
||||
// stem
|
||||
gl_Position = p0;
|
||||
EmitVertex();
|
||||
gl_Position = p1;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
"""
|
||||
|
||||
def decorate(self, context, obj):
|
||||
verts, _, _ = self.get_path_geom(obj, topo=False)
|
||||
self.draw_lines(context, obj, verts, [(0, 1)])
|
||||
center = obj.matrix_world.translation
|
||||
v1 = obj.matrix_world @ Vector((0, 0, 0))
|
||||
v2 = obj.matrix_world @ Vector((0, 0, -1))
|
||||
self.draw_lines(context, obj, [v1, v2], [(0, 1)])
|
||||
|
||||
|
||||
class SectionDecorator(LevelDecorator):
|
||||
objecttype = "SECTION"
|
||||
|
||||
DEF_GLSL = (
|
||||
BaseDecorator.DEF_GLSL
|
||||
+ """
|
||||
#define CIRCLE_SIZE 8.0
|
||||
#define TRIANGLE_L 22.63
|
||||
#define TRIANGLE_W 11.31
|
||||
"""
|
||||
)
|
||||
|
||||
GEOM_GLSL = """
|
||||
uniform vec2 winsize;
|
||||
uniform float viewportDrawingScale;
|
||||
|
||||
layout(lines) in;
|
||||
layout(line_strip, max_vertices=MAX_POINTS) out;
|
||||
|
||||
void triangle_head(in vec4 side, in vec4 dir, in float length, in float width, in float radius, out vec4 head[5]) {
|
||||
vec4 nose = side * length;
|
||||
vec4 ear = dir * width;
|
||||
head[0] = side * -radius;
|
||||
head[1] = nose * -.5;
|
||||
head[2] = vec4(0) + ear;
|
||||
head[3] = nose * .5;
|
||||
head[4] = side * radius;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec4 clip2win = matCLIP2WIN();
|
||||
vec4 win2clip = matWIN2CLIP();
|
||||
|
||||
vec4 p0 = gl_in[0].gl_Position, p1 = gl_in[1].gl_Position;
|
||||
|
||||
vec4 p0w = CLIP2WIN(p0), p1w = CLIP2WIN(p1);
|
||||
vec4 edge = p1w - p0w, side = normalize(edge);
|
||||
vec4 gap = side * viewportDrawingScale * CIRCLE_SIZE;
|
||||
vec4 dir = vec4(cross(vec3(side.xy, 0), vec3(0, 0, 1)).xy, 0, 0);
|
||||
vec4 p;
|
||||
|
||||
vec4 head[CIRCLE_SEGS];
|
||||
circle_head(viewportDrawingScale * CIRCLE_SIZE, head);
|
||||
|
||||
vec4 head5[5];
|
||||
|
||||
// start edge circle
|
||||
for(int i=0; i<CIRCLE_SEGS; i++) {
|
||||
p = p0w + head[i];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
}
|
||||
p = p0w + head[0];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
|
||||
// start edge triangle
|
||||
triangle_head(side, dir, viewportDrawingScale * TRIANGLE_L, viewportDrawingScale * TRIANGLE_W, viewportDrawingScale * CIRCLE_SIZE, head5);
|
||||
p = p0w + head5[0];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
p = p0w + head5[1];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
p = p0w + head5[2];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
p = p0w + head5[3];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
p = p0w + head5[4];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
|
||||
// start reference divider
|
||||
p = p0w + normalize(vec4(-1, 0, 0, 0)) * viewportDrawingScale * CIRCLE_SIZE;
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
p = p0w + normalize(vec4(1, 0, 0, 0)) * viewportDrawingScale * CIRCLE_SIZE;
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
|
||||
// end edge circle
|
||||
for(int i=0; i<CIRCLE_SEGS; i++) {
|
||||
p = p1w - head[i];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
}
|
||||
p = p1w - head[0];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
|
||||
// end edge triangle
|
||||
triangle_head(side, dir, viewportDrawingScale * TRIANGLE_L, viewportDrawingScale * TRIANGLE_W, viewportDrawingScale * CIRCLE_SIZE, head5);
|
||||
p = p1w + head5[0];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
p = p1w + head5[1];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
p = p1w + head5[2];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
p = p1w + head5[3];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
p = p1w + head5[4];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
|
||||
// end reference divider
|
||||
p = p1w + normalize(vec4(-1, 0, 0, 0)) * viewportDrawingScale * CIRCLE_SIZE;
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
p = p1w + normalize(vec4(1, 0, 0, 0)) * viewportDrawingScale * CIRCLE_SIZE;
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
|
||||
// stem
|
||||
p = p0w + gap;
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
p = p1w - gap;
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
"""
|
||||
|
||||
def decorate(self, context, obj):
|
||||
if obj.data.is_editmode:
|
||||
verts, idxs = self.get_editmesh_geom(obj)
|
||||
else:
|
||||
verts, idxs = self.get_mesh_geom(obj)
|
||||
vert_map = {v.freeze(): (obj.matrix_world.inverted() @ v).x for v in verts}
|
||||
verts = sorted(verts, key=lambda v: vert_map[v], reverse=True)
|
||||
self.draw_lines(context, obj, verts, idxs)
|
||||
|
||||
|
||||
|
||||
class TextDecorator(BaseDecorator):
|
||||
"""Decorator for text objects
|
||||
- draws the text next to the origin
|
||||
"""
|
||||
|
||||
objecttype = "TEXT"
|
||||
|
||||
DEF_GLSL = (
|
||||
BaseDecorator.DEF_GLSL
|
||||
+ """
|
||||
#define ARROW_ANGLE PI / 12.0
|
||||
#define ARROW_SIZE 16.0
|
||||
"""
|
||||
)
|
||||
|
||||
GEOM_GLSL = """
|
||||
uniform vec2 winsize;
|
||||
uniform float viewportDrawingScale;
|
||||
|
||||
layout(lines) in;
|
||||
layout(line_strip, max_vertices=MAX_POINTS) out;
|
||||
|
||||
void main() {
|
||||
vec4 clip2win = matCLIP2WIN();
|
||||
vec4 win2clip = matWIN2CLIP();
|
||||
|
||||
vec4 p0 = gl_in[0].gl_Position, p1 = gl_in[1].gl_Position;
|
||||
|
||||
vec4 p0w = CLIP2WIN(p0), p1w = CLIP2WIN(p1);
|
||||
vec4 edge = p1w - p0w, dir = normalize(edge);
|
||||
|
||||
vec4 p;
|
||||
|
||||
vec4 head[3];
|
||||
arrow_head(dir, viewportDrawingScale * ARROW_SIZE, ARROW_ANGLE, head);
|
||||
|
||||
// start edge arrow
|
||||
gl_Position = p0;
|
||||
EmitVertex();
|
||||
p = p0w + head[1];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
p = p0w + head[2];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
gl_Position = p0;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
|
||||
// end edge arrow
|
||||
gl_Position = p1;
|
||||
EmitVertex();
|
||||
p = p1w - head[1];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
p = p1w - head[2];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
gl_Position = p1;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
|
||||
// stem, with gaps for arrows
|
||||
p = p0w + head[0];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
p = p1w - head[0];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
"""
|
||||
|
||||
def decorate(self, context, obj):
|
||||
self.draw_labels(context, obj)
|
||||
|
||||
def draw_labels(self, context, obj):
|
||||
region = context.region
|
||||
region3d = context.region_data
|
||||
dir = Vector((1, 0))
|
||||
pos = location_3d_to_region_2d(region, region3d, obj.matrix_world.translation)
|
||||
self.draw_label(context, obj.BIMTextProperties.value, pos, dir, gap=0, center=False, vcenter=False)
|
||||
|
||||
|
||||
class DecorationsHandler:
|
||||
@@ -1161,7 +1460,9 @@ class DecorationsHandler:
|
||||
SectionLevelDecorator,
|
||||
StairDecorator,
|
||||
BreakDecorator,
|
||||
SectionViewDecorator,
|
||||
SectionDecorator,
|
||||
ElevationDecorator,
|
||||
TextDecorator,
|
||||
]
|
||||
|
||||
installed = None
|
||||
|
||||
@@ -45,6 +45,3 @@ def set_active_camera_resolution(scene):
|
||||
scene.render.resolution_x = scene.camera.data.BIMCameraProperties.raster_x
|
||||
scene.render.resolution_y = scene.camera.data.BIMCameraProperties.raster_y
|
||||
current_drawing = scene.DocProperties.drawings[scene.DocProperties.current_drawing_index]
|
||||
if scene.camera != current_drawing.camera:
|
||||
scene.DocProperties.current_drawing_index = scene.DocProperties.drawings.find(scene.camera.name.split("/")[1])
|
||||
bpy.ops.bim.activate_view(drawing_index=scene.DocProperties.current_drawing_index)
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import bpy
|
||||
import math
|
||||
import mathutils.geometry
|
||||
import blenderbim.tool as tool
|
||||
from mathutils import Vector
|
||||
|
||||
# Code taken and updated from https://blenderartists.org/t/detecting-intersection-of-bounding-boxes/457520/2
|
||||
@@ -259,13 +260,11 @@ def format_distance(value, isArea=False, hide_units=True):
|
||||
def get_active_drawing(scene):
|
||||
"""Get active drawing collection and camera"""
|
||||
props = scene.DocProperties
|
||||
if props.active_drawing_index is None or len(props.drawings) == 0:
|
||||
return None, None
|
||||
try:
|
||||
drawing = props.active_drawing
|
||||
return scene.collection.children["Views"].children[f"IfcGroup/{drawing.name}"], drawing.camera
|
||||
except (KeyError, IndexError):
|
||||
raise RuntimeError("missing drawing collection")
|
||||
camera = tool.Ifc.get_object(tool.Ifc.get().by_id(props.active_drawing_id))
|
||||
return camera.users_collection[0], camera
|
||||
except:
|
||||
return None, None
|
||||
|
||||
|
||||
def get_project_collection(scene):
|
||||
|
||||
@@ -18,11 +18,14 @@
|
||||
|
||||
import os
|
||||
import bpy
|
||||
import json
|
||||
import enum
|
||||
import ifcopenshell
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.drawing as core
|
||||
import blenderbim.bim.module.drawing.annotation as annotation
|
||||
import blenderbim.bim.module.drawing.decoration as decoration
|
||||
import enum
|
||||
from blenderbim.bim.module.drawing.data import DrawingsData
|
||||
from pathlib import Path
|
||||
from blenderbim.bim.prop import Attribute, StrProperty
|
||||
from bpy.types import PropertyGroup
|
||||
@@ -55,12 +58,24 @@ def purge():
|
||||
vector_styles_enum = []
|
||||
|
||||
|
||||
def update_target_view(self, context):
|
||||
DrawingsData.data["location_hint"] = DrawingsData.location_hint()
|
||||
|
||||
|
||||
def get_location_hint(self, context):
|
||||
if not DrawingsData.is_loaded:
|
||||
DrawingsData.load()
|
||||
return DrawingsData.data["location_hint"]
|
||||
|
||||
|
||||
def update_diagram_scale(self, context):
|
||||
scale = self.diagram_scale
|
||||
if scale == "CUSTOM":
|
||||
scale = self.custom_diagram_scale
|
||||
scale = scale.split("|")[1]
|
||||
element = tool.Ifc.get_entity(context.scene.camera)
|
||||
if "|" not in scale:
|
||||
return
|
||||
human_scale, scale = scale.split("|")
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
if not element:
|
||||
return
|
||||
pset = ifcopenshell.util.element.get_psets(element).get("EPset_Drawing")
|
||||
@@ -68,7 +83,9 @@ def update_diagram_scale(self, context):
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
else:
|
||||
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="EPset_Drawing")
|
||||
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Scale": scale})
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Scale": scale, "HumanScale": human_scale}
|
||||
)
|
||||
|
||||
|
||||
def get_diagram_scales(self, context):
|
||||
@@ -131,31 +148,42 @@ def get_diagram_scales(self, context):
|
||||
return diagram_scales_enum
|
||||
|
||||
|
||||
def updateDrawingName(self, context):
|
||||
if not self.camera:
|
||||
return
|
||||
if self.camera.name == self.name:
|
||||
return
|
||||
self.camera.name = "IfcAnnotation/{}".format(self.name)
|
||||
unique_name = "/".join(self.camera.name.split("/")[1:])
|
||||
self.camera.users_collection[0].name = "IfcGroup/{}".format(unique_name)
|
||||
if self.name != unique_name:
|
||||
self.name = unique_name
|
||||
def update_drawing_name(self, context):
|
||||
drawing = tool.Ifc.get().by_id(self.ifc_definition_id)
|
||||
core.update_drawing_name(tool.Ifc, tool.Drawing, drawing=drawing, name=self.name)
|
||||
|
||||
|
||||
def refreshActiveDrawingIndex(self, context):
|
||||
bpy.ops.bim.activate_view(drawing_index=context.scene.DocProperties.active_drawing_index)
|
||||
def update_has_underlay(self, context):
|
||||
update_layer(self, context, "HasUnderlay", self.has_underlay)
|
||||
|
||||
|
||||
def update_has_linework(self, context):
|
||||
update_layer(self, context, "HasLinework", self.has_linework)
|
||||
|
||||
|
||||
def update_has_annotation(self, context):
|
||||
update_layer(self, context, "HasAnnotation", self.has_annotation)
|
||||
|
||||
|
||||
def update_layer(self, context, name, value):
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
if not element:
|
||||
return
|
||||
pset = ifcopenshell.util.element.get_psets(element).get("EPset_Drawing")
|
||||
if pset:
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
else:
|
||||
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="EPset_Drawing")
|
||||
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={name: value})
|
||||
|
||||
|
||||
def getTitleblocks(self, context):
|
||||
global titleblocks_enum
|
||||
if len(titleblocks_enum) < 1:
|
||||
titleblocks_enum.clear()
|
||||
for filename in Path(os.path.join(context.scene.BIMProperties.data_dir, "templates", "titleblocks")).glob(
|
||||
"*.svg"
|
||||
):
|
||||
f = str(filename.stem)
|
||||
titleblocks_enum.append((f, f, ""))
|
||||
files = Path(os.path.join(context.scene.BIMProperties.data_dir, "templates", "titleblocks")).glob("*.svg")
|
||||
files = sorted([str(f.stem) for f in files])
|
||||
titleblocks_enum.extend([(f, f, "") for f in files])
|
||||
return titleblocks_enum
|
||||
|
||||
|
||||
@@ -168,6 +196,10 @@ def refreshTitleblocks(self, context):
|
||||
def toggleDecorations(self, context):
|
||||
toggle = self.should_draw_decorations
|
||||
if toggle:
|
||||
# TODO: design a proper text variable templating renderer
|
||||
collection = context.scene.camera.users_collection[0]
|
||||
for obj in collection.objects:
|
||||
tool.Drawing.update_text_value(obj)
|
||||
decoration.DecorationsHandler.install(context)
|
||||
else:
|
||||
decoration.DecorationsHandler.uninstall()
|
||||
@@ -193,8 +225,9 @@ class Variable(PropertyGroup):
|
||||
|
||||
|
||||
class Drawing(PropertyGroup):
|
||||
name: StringProperty(name="Name", update=updateDrawingName)
|
||||
camera: PointerProperty(name="Camera", type=bpy.types.Object)
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
name: StringProperty(name="Name", update=update_drawing_name)
|
||||
target_view: StringProperty(name="Target View")
|
||||
|
||||
|
||||
class Schedule(PropertyGroup):
|
||||
@@ -213,9 +246,12 @@ class Sheet(PropertyGroup):
|
||||
def get_name(self):
|
||||
return self.get("name")
|
||||
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
identification: StringProperty(name="Identification")
|
||||
name: StringProperty(name="Name", get=get_name, set=set_name)
|
||||
drawings: CollectionProperty(name="Drawings", type=Drawing)
|
||||
active_drawing_index: IntProperty(name="Active Drawing Index")
|
||||
is_sheet: BoolProperty(name="Is Sheet", default=False)
|
||||
reference_type: StringProperty(name="Reference Type")
|
||||
is_expanded: BoolProperty(name="Is Expanded", default=False)
|
||||
|
||||
|
||||
class DrawingStyle(PropertyGroup):
|
||||
@@ -264,54 +300,52 @@ class RasterStyleProperty(enum.Enum):
|
||||
|
||||
|
||||
class DocProperties(PropertyGroup):
|
||||
has_underlay: BoolProperty(name="Underlay", default=False)
|
||||
has_linework: BoolProperty(name="Linework", default=True)
|
||||
has_annotation: BoolProperty(name="Annotation", default=True)
|
||||
should_use_underlay_cache: BoolProperty(name="Use Underlay Cache", default=False)
|
||||
should_use_linework_cache: BoolProperty(name="Use Linework Cache", default=False)
|
||||
should_use_annotation_cache: BoolProperty(name="Use Annotation Cache", default=False)
|
||||
should_extract: BoolProperty(name="Should Extract", default=True)
|
||||
is_editing_drawings: BoolProperty(name="Is Editing Drawings", default=False)
|
||||
target_view: EnumProperty(
|
||||
items=[
|
||||
("PLAN_VIEW", "Plan", ""),
|
||||
("ELEVATION_VIEW", "Elevation", ""),
|
||||
("SECTION_VIEW", "Section", ""),
|
||||
("REFLECTED_PLAN_VIEW", "RCP", ""),
|
||||
("MODEL_VIEW", "Model", ""),
|
||||
],
|
||||
name="Target View",
|
||||
default="PLAN_VIEW",
|
||||
update=update_target_view,
|
||||
)
|
||||
location_hint: EnumProperty(items=get_location_hint, name="Location Hint")
|
||||
drawings: CollectionProperty(name="Drawings", type=Drawing)
|
||||
active_drawing_index: IntProperty(name="Active Drawing Index", update=refreshActiveDrawingIndex)
|
||||
active_drawing_id: IntProperty(name="Active Drawing Id")
|
||||
active_drawing_index: IntProperty(name="Active Drawing Index")
|
||||
current_drawing_index: IntProperty(name="Current Drawing Index")
|
||||
schedules: CollectionProperty(name="Schedules", type=Schedule)
|
||||
active_schedule_index: IntProperty(name="Active Schedule Index")
|
||||
titleblock: EnumProperty(items=getTitleblocks, name="Titleblock", update=refreshTitleblocks)
|
||||
is_editing_sheets: BoolProperty(name="Is Editing Sheets", default=False)
|
||||
sheets: CollectionProperty(name="Sheets", type=Sheet)
|
||||
active_sheet_index: IntProperty(name="Active Sheet Index")
|
||||
ifc_files: CollectionProperty(name="IFCs", type=StrProperty)
|
||||
drawing_styles: CollectionProperty(name="Drawing Styles", type=DrawingStyle)
|
||||
should_draw_decorations: BoolProperty(name="Should Draw Decorations", update=toggleDecorations)
|
||||
decorations_colour: FloatVectorProperty(
|
||||
name="Decorations Colour", subtype="COLOR", default=(1, 0, 0, 1), min=0.0, max=1.0, size=4
|
||||
name="Decorations Colour", subtype="COLOR", default=(1, 1, 1, 1), min=0.0, max=1.0, size=4
|
||||
)
|
||||
|
||||
@property
|
||||
def active_schedule(self):
|
||||
return self.schedules[self.active_schedule_index]
|
||||
|
||||
@property
|
||||
def active_drawing(self):
|
||||
return self.drawings[self.active_drawing_index]
|
||||
|
||||
@property
|
||||
def active_sheet(self):
|
||||
return self.sheets[self.active_sheet_index]
|
||||
|
||||
|
||||
class BIMCameraProperties(PropertyGroup):
|
||||
has_underlay: BoolProperty(name="Underlay", default=False, update=update_has_underlay)
|
||||
has_linework: BoolProperty(name="Linework", default=True, update=update_has_linework)
|
||||
has_annotation: BoolProperty(name="Annotation", default=True, update=update_has_annotation)
|
||||
representation: StringProperty(name="Representation")
|
||||
view_name: StringProperty(name="View Name")
|
||||
target_view: EnumProperty(
|
||||
items=[
|
||||
("PLAN_VIEW", "PLAN_VIEW", ""),
|
||||
("ELEVATION_VIEW", "ELEVATION_VIEW", ""),
|
||||
("SECTION_VIEW", "SECTION_VIEW", ""),
|
||||
("REFLECTED_PLAN_VIEW", "REFLECTED_PLAN_VIEW", ""),
|
||||
("MODEL_VIEW", "MODEL_VIEW", ""),
|
||||
],
|
||||
name="Target View",
|
||||
default="PLAN_VIEW",
|
||||
)
|
||||
diagram_scale: EnumProperty(items=get_diagram_scales, name="Drawing Scale", update=update_diagram_scale)
|
||||
custom_diagram_scale: StringProperty(name="Custom Scale", update=update_diagram_scale)
|
||||
raster_x: IntProperty(name="Raster X", default=1000)
|
||||
@@ -332,8 +366,29 @@ class BIMCameraProperties(PropertyGroup):
|
||||
cut_objects_custom: StringProperty(name="Custom Cut")
|
||||
active_drawing_style_index: IntProperty(name="Active Drawing Style Index")
|
||||
|
||||
# For now, this JSON dump are all the parameters that determine a camera's "Block representation"
|
||||
# By checking this, you will know whether or not the camera IFC representation needs to be refreshed
|
||||
def update_representation(self, obj):
|
||||
representation = json.dumps(
|
||||
{
|
||||
"matrix": [list(x) for x in obj.matrix_world],
|
||||
"raster_x": self.raster_x,
|
||||
"raster_y": self.raster_y,
|
||||
"ortho_scale": obj.data.ortho_scale,
|
||||
"clip_end": obj.data.clip_end,
|
||||
}
|
||||
)
|
||||
if self.representation != representation:
|
||||
self.representation = representation
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class BIMTextProperties(PropertyGroup):
|
||||
is_editing: BoolProperty(name="Is Editing", default=False)
|
||||
is_editing_product: BoolProperty(name="Is Editing Product", default=False)
|
||||
attributes: CollectionProperty(name="Attributes", type=Attribute)
|
||||
value: StringProperty(name="Value", default="TEXT")
|
||||
font_size: EnumProperty(
|
||||
items=[
|
||||
("1.8", "1.8 - Small", ""),
|
||||
@@ -342,6 +397,7 @@ class BIMTextProperties(PropertyGroup):
|
||||
("5.0", "5.0 - Header", ""),
|
||||
("7.0", "7.0 - Title", ""),
|
||||
],
|
||||
default="2.5",
|
||||
update=refreshFontSize,
|
||||
name="Font Size",
|
||||
)
|
||||
@@ -354,5 +410,4 @@ class BIMTextProperties(PropertyGroup):
|
||||
update=refreshFontSize,
|
||||
name="Symbol",
|
||||
)
|
||||
related_element: PointerProperty(name="Related Element", type=bpy.types.Object)
|
||||
variables: CollectionProperty(name="Variables", type=Variable)
|
||||
relating_product: PointerProperty(name="Relating Product", type=bpy.types.Object)
|
||||
|
||||
@@ -16,11 +16,12 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
import urllib.parse
|
||||
import pystache
|
||||
import ntpath
|
||||
import os
|
||||
import ntpath
|
||||
import pystache
|
||||
import urllib.parse
|
||||
import xml.etree.ElementTree as ET
|
||||
import blenderbim.tool as tool
|
||||
from shutil import copy
|
||||
from xml.dom import minidom
|
||||
|
||||
@@ -31,7 +32,7 @@ class SheetBuilder:
|
||||
self.scale = "NTS"
|
||||
|
||||
def create(self, name, titleblock_name):
|
||||
sheet_path = os.path.join(self.data_dir, f"{name}.svg")
|
||||
sheet_path = os.path.join(self.data_dir, "sheets", f"{name}.svg")
|
||||
root = ET.Element("svg")
|
||||
root.attrib["xmlns"] = "http://www.w3.org/2000/svg"
|
||||
root.attrib["xmlns:xlink"] = "http://www.w3.org/1999/xlink"
|
||||
@@ -59,11 +60,16 @@ class SheetBuilder:
|
||||
with open(sheet_path, "w") as f:
|
||||
f.write(minidom.parseString(ET.tostring(root)).toprettyxml(indent=" "))
|
||||
|
||||
def add_drawing(self, view_name, sheet_name):
|
||||
sheet_path = os.path.join(self.data_dir, "sheets", sheet_name + ".svg")
|
||||
view_path = os.path.join(self.data_dir, "diagrams", view_name + ".svg")
|
||||
def add_drawing(self, drawing, sheet):
|
||||
filename = drawing.Name
|
||||
sheet_name = tool.Drawing.get_sheet_filename(sheet)
|
||||
sheet_dir = os.path.join(self.data_dir, "sheets")
|
||||
drawing_dir = os.path.join(self.data_dir, "diagrams")
|
||||
sheet_path = os.path.join(sheet_dir, sheet_name + ".svg")
|
||||
drawing_path = os.path.join(drawing_dir, filename + ".svg")
|
||||
underlay_path = os.path.join(drawing_dir, filename + "-underlay.png")
|
||||
|
||||
if not os.path.isfile(view_path):
|
||||
if not os.path.isfile(sheet_path):
|
||||
raise FileNotFoundError
|
||||
|
||||
ET.register_namespace("", "http://www.w3.org/2000/svg")
|
||||
@@ -72,7 +78,7 @@ class SheetBuilder:
|
||||
sheet_tree = ET.parse(sheet_path)
|
||||
sheet_root = sheet_tree.getroot()
|
||||
|
||||
view_tree = ET.parse(view_path)
|
||||
view_tree = ET.parse(drawing_path)
|
||||
view_root = view_tree.getroot()
|
||||
|
||||
# The view is placed into a group with a background image element.
|
||||
@@ -80,26 +86,48 @@ class SheetBuilder:
|
||||
# here to accommodate browsers which do not nest images.
|
||||
view = ET.SubElement(sheet_root, "g")
|
||||
view.attrib["data-type"] = "drawing"
|
||||
view.attrib["data-guid"] = drawing.GlobalId
|
||||
view_width = self.convert_to_mm(view_root.attrib.get("width"))
|
||||
view_height = self.convert_to_mm(view_root.attrib.get("height"))
|
||||
|
||||
background = ET.SubElement(view, "image")
|
||||
background.attrib["xlink:href"] = "../diagrams/{}.png".format(view_name)
|
||||
background.attrib["x"] = "30"
|
||||
background.attrib["y"] = "30"
|
||||
background.attrib["width"] = str(view_width)
|
||||
background.attrib["height"] = str(view_height)
|
||||
if os.path.isfile(underlay_path):
|
||||
background = ET.SubElement(view, "image")
|
||||
background.attrib["data-type"] = "background"
|
||||
background.attrib["xlink:href"] = os.path.relpath(underlay_path, sheet_dir)
|
||||
background.attrib["x"] = "30"
|
||||
background.attrib["y"] = "30"
|
||||
background.attrib["width"] = str(view_width)
|
||||
background.attrib["height"] = str(view_height)
|
||||
|
||||
foreground = ET.SubElement(view, "image")
|
||||
foreground.attrib["xlink:href"] = "../diagrams/{}.svg".format(view_name)
|
||||
foreground.attrib["x"] = "30"
|
||||
foreground.attrib["y"] = "30"
|
||||
foreground.attrib["width"] = str(view_width)
|
||||
foreground.attrib["height"] = str(view_height)
|
||||
if os.path.isfile(drawing_path):
|
||||
foreground = ET.SubElement(view, "image")
|
||||
foreground.attrib["data-type"] = "foreground"
|
||||
foreground.attrib["xlink:href"] = os.path.relpath(drawing_path, sheet_dir)
|
||||
foreground.attrib["x"] = "30"
|
||||
foreground.attrib["y"] = "30"
|
||||
foreground.attrib["width"] = str(view_width)
|
||||
foreground.attrib["height"] = str(view_height)
|
||||
|
||||
self.add_view_title(30, view_height + 35, view)
|
||||
sheet_tree.write(sheet_path)
|
||||
|
||||
def remove_drawing(self, drawing, sheet):
|
||||
sheet_name = tool.Drawing.get_sheet_filename(sheet)
|
||||
sheet_dir = os.path.join(self.data_dir, "sheets")
|
||||
sheet_path = os.path.join(sheet_dir, sheet_name + ".svg")
|
||||
|
||||
ET.register_namespace("", "http://www.w3.org/2000/svg")
|
||||
|
||||
sheet_tree = ET.parse(sheet_path)
|
||||
sheet_root = sheet_tree.getroot()
|
||||
|
||||
for g in sheet_root.findall("{http://www.w3.org/2000/svg}g"):
|
||||
if g.attrib["data-type"] == "drawing" and g.attrib["data-guid"] == drawing.GlobalId:
|
||||
sheet_root.remove(g)
|
||||
break
|
||||
|
||||
sheet_tree.write(sheet_path)
|
||||
|
||||
def add_schedule(self, schedule_name, sheet_name):
|
||||
sheet_path = os.path.join(self.data_dir, "sheets", sheet_name + ".svg")
|
||||
view_path = os.path.join(self.data_dir, "schedules", schedule_name + ".svg")
|
||||
@@ -132,13 +160,15 @@ class SheetBuilder:
|
||||
title_tree = ET.parse(os.path.join(self.data_dir, "templates", "view-title.svg"))
|
||||
title_root = title_tree.getroot()
|
||||
title = ET.SubElement(parent, "image")
|
||||
title.attrib["data-type"] = "view-title"
|
||||
title.attrib["xlink:href"] = "../templates/view-title.svg"
|
||||
title.attrib["x"] = str(x)
|
||||
title.attrib["y"] = str(y)
|
||||
title.attrib["width"] = str(self.convert_to_mm(title_root.attrib.get("width")))
|
||||
title.attrib["height"] = str(self.convert_to_mm(title_root.attrib.get("height")))
|
||||
|
||||
def build(self, sheet_name):
|
||||
def build(self, sheet):
|
||||
sheet_name = tool.Drawing.get_sheet_filename(sheet)
|
||||
os.makedirs(os.path.join(self.data_dir, "build", sheet_name), exist_ok=True)
|
||||
|
||||
sheet_path = os.path.join(self.data_dir, "sheets", f"{sheet_name}.svg")
|
||||
@@ -149,48 +179,60 @@ class SheetBuilder:
|
||||
tree = ET.parse(sheet_path)
|
||||
root = tree.getroot()
|
||||
|
||||
titleblock = root.findall('{http://www.w3.org/2000/svg}g[@data-type="titleblock"]')[0]
|
||||
image = titleblock.findall("{http://www.w3.org/2000/svg}image")[0]
|
||||
titleblock.append(self.parse_embedded_svg(image, {"number": sheet_name, "revision": "A"}))
|
||||
titleblock.remove(image)
|
||||
|
||||
self.group_number = 1
|
||||
self.build_drawings(root.findall('{http://www.w3.org/2000/svg}g[@data-type="drawing"]'), sheet_name)
|
||||
self.build_titleblock(root, sheet)
|
||||
self.build_drawings(root, sheet)
|
||||
self.build_schedules(root.findall('{http://www.w3.org/2000/svg}g[@data-type="schedule"]'))
|
||||
|
||||
with open(os.path.join(self.data_dir, "build", sheet_name, f"{sheet_name}.svg"), "wb") as output:
|
||||
tree.write(output)
|
||||
|
||||
def build_drawings(self, drawings, sheet_name):
|
||||
for view in drawings:
|
||||
def build_titleblock(self, root, sheet):
|
||||
titleblock = root.findall('{http://www.w3.org/2000/svg}g[@data-type="titleblock"]')[0]
|
||||
image = titleblock.findall("{http://www.w3.org/2000/svg}image")[0]
|
||||
titleblock.append(self.parse_embedded_svg(image, sheet.get_info()))
|
||||
titleblock.remove(image)
|
||||
|
||||
def build_drawings(self, root, sheet):
|
||||
sheet_name = tool.Drawing.get_sheet_filename(sheet)
|
||||
references = tool.Drawing.get_document_references(sheet)
|
||||
drawing_references = {tool.Drawing.get_reference_element(r): r for r in references}
|
||||
|
||||
for view in root.findall('{http://www.w3.org/2000/svg}g[@data-type="drawing"]'):
|
||||
drawing = [d for d in drawing_references.keys() if d and d.GlobalId == view.attrib["data-guid"]][0]
|
||||
reference = drawing_references[drawing]
|
||||
|
||||
images = view.findall("{http://www.w3.org/2000/svg}image")
|
||||
background = images[0]
|
||||
foreground = images[1]
|
||||
view_title = images[2]
|
||||
self.scale = "NTS"
|
||||
|
||||
# Add foreground
|
||||
view.append(self.parse_embedded_svg(foreground, {}))
|
||||
background = None
|
||||
foreground = None
|
||||
view_title = None
|
||||
|
||||
# Add background
|
||||
background_path = os.path.join(self.data_dir, "sheets", self.get_href(background))
|
||||
for image in images:
|
||||
if image.attrib["data-type"] == "background":
|
||||
background = image
|
||||
elif image.attrib["data-type"] == "foreground":
|
||||
foreground = image
|
||||
elif image.attrib["data-type"] == "view-title":
|
||||
view_title = image
|
||||
|
||||
copy(background_path, os.path.join(self.data_dir, "build", sheet_name))
|
||||
if foreground is not None:
|
||||
view.append(self.parse_embedded_svg(foreground, {}))
|
||||
|
||||
# Add view title
|
||||
foreground_path = self.get_href(foreground)
|
||||
view.append(
|
||||
self.parse_embedded_svg(
|
||||
view_title,
|
||||
{"no": self.group_number, "name": ntpath.basename(foreground_path)[0:-4], "scale": self.scale},
|
||||
)
|
||||
)
|
||||
if background is not None:
|
||||
background_path = os.path.join(self.data_dir, "sheets", self.get_href(background))
|
||||
copy(background_path, os.path.join(self.data_dir, "build", sheet_name))
|
||||
|
||||
if view_title is not None:
|
||||
foreground_path = self.get_href(foreground)
|
||||
data = reference.get_info()
|
||||
if not data["Name"]:
|
||||
data["Name"] = ntpath.basename(foreground_path)[0:-4]
|
||||
data["Scale"] = tool.Drawing.get_drawing_human_scale(drawing)
|
||||
view.append(self.parse_embedded_svg(view_title, data))
|
||||
|
||||
for image in images:
|
||||
view.remove(image)
|
||||
|
||||
self.group_number += 1
|
||||
|
||||
def build_schedules(self, schedules):
|
||||
for group in schedules:
|
||||
images = group.findall("{http://www.w3.org/2000/svg}image")
|
||||
@@ -203,15 +245,13 @@ class SheetBuilder:
|
||||
path = self.get_href(schedule)
|
||||
group.append(
|
||||
self.parse_embedded_svg(
|
||||
group_title, {"no": self.group_number, "name": ntpath.basename(path)[0:-4], "scale": self.scale}
|
||||
group_title, {"no": 1, "name": ntpath.basename(path)[0:-4], "scale": self.scale}
|
||||
)
|
||||
)
|
||||
|
||||
for image in images:
|
||||
group.remove(image)
|
||||
|
||||
self.group_number += 1
|
||||
|
||||
def get_href(self, element):
|
||||
return urllib.parse.unquote(element.attrib.get("{http://www.w3.org/1999/xlink}href"))
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@ import pystache
|
||||
import xml.etree.ElementTree as ET
|
||||
import svgwrite
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.representation
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.bim.module.drawing.helper as helper
|
||||
import blenderbim.bim.module.drawing.annotation as annotation
|
||||
from mathutils import Vector
|
||||
@@ -146,13 +149,10 @@ class SvgWriter:
|
||||
self.draw_dimension_annotations(obj)
|
||||
self.draw_measureit_arch_dimension_annotations()
|
||||
|
||||
if self.annotations.get("break_obj"):
|
||||
self.draw_break_annotations(self.annotations["break_obj"])
|
||||
|
||||
for grid_obj in self.annotations.get("grid_objs", []):
|
||||
matrix_world = grid_obj.matrix_world
|
||||
classes = self.get_attribute_classes(grid_obj)
|
||||
for edge in grid_obj.data.edges:
|
||||
classes = ["annotation", "grid"]
|
||||
v0_global = matrix_world @ grid_obj.data.vertices[edge.vertices[0]].co.xyz
|
||||
v1_global = matrix_world @ grid_obj.data.vertices[edge.vertices[1]].co.xyz
|
||||
v0 = self.project_point_onto_camera(v0_global)
|
||||
@@ -165,10 +165,8 @@ class SvgWriter:
|
||||
start=tuple(start * self.scale), end=tuple(end * self.scale), class_=" ".join(classes)
|
||||
)
|
||||
)
|
||||
line["marker-start"] = "url(#grid-marker)"
|
||||
line["marker-end"] = "url(#grid-marker)"
|
||||
line["stroke-dasharray"] = "12.5, 3, 3, 3"
|
||||
axis_tag = IfcStore.get_file().by_id(grid_obj.BIMObjectProperties.ifc_definition_id).AxisTag
|
||||
axis_tag = tool.Ifc.get_entity(grid_obj).Name
|
||||
self.svg.add(
|
||||
self.svg.text(
|
||||
axis_tag,
|
||||
@@ -199,66 +197,23 @@ class SvgWriter:
|
||||
self.draw_ifc_annotation()
|
||||
|
||||
for obj in self.annotations.get("misc_objs", []):
|
||||
self.draw_misc_annotation(obj, ["IfcAnnotation"])
|
||||
self.draw_misc_annotation(obj)
|
||||
|
||||
for obj_data in self.annotations.get("hidden_objs", []):
|
||||
self.draw_line_annotation(obj_data, ["hidden"])
|
||||
self.draw_line_annotation(obj_data)
|
||||
|
||||
for obj_data in self.annotations.get("solid_objs", []):
|
||||
self.draw_line_annotation(obj_data, ["solid"])
|
||||
self.draw_line_annotation(obj_data)
|
||||
|
||||
if self.annotations.get("leader_obj"):
|
||||
self.draw_line_annotation(self.annotations["leader_obj"], ["leader"])
|
||||
self.draw_leader_annotations()
|
||||
|
||||
if self.annotations.get("plan_level_obj"):
|
||||
matrix_world = self.annotations["plan_level_obj"].matrix_world
|
||||
for spline in self.annotations["plan_level_obj"].data.splines:
|
||||
classes = ["annotation", "plan-level"]
|
||||
points = self.get_spline_points(spline)
|
||||
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
|
||||
d = " ".join(
|
||||
[
|
||||
"L {} {}".format((x_offset + p.x) * self.scale, (y_offset - p.y) * self.scale)
|
||||
for p in projected_points
|
||||
]
|
||||
)
|
||||
d = "M{}".format(d[1:])
|
||||
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
|
||||
path["marker-end"] = "url(#plan-level-marker)"
|
||||
text_position = Vector(
|
||||
(
|
||||
(x_offset + projected_points[0].x) * self.scale,
|
||||
((y_offset - projected_points[0].y) * self.scale) - 2.5,
|
||||
)
|
||||
)
|
||||
# TODO: allow metric to be configurable
|
||||
rl = ((matrix_world @ points[0].co).xyz + self.annotations["plan_level_obj"].location).z
|
||||
if bpy.context.scene.unit_settings.system == "IMPERIAL":
|
||||
rl = helper.format_distance(rl)
|
||||
else:
|
||||
rl = "{:.3f}m".format(rl)
|
||||
if projected_points[0].x > projected_points[-1].x:
|
||||
text_anchor = "end"
|
||||
else:
|
||||
text_anchor = "start"
|
||||
self.svg.add(
|
||||
self.svg.text(
|
||||
"RL +{}".format(rl),
|
||||
insert=tuple(text_position),
|
||||
**{
|
||||
"font-size": annotation.Annotator.get_svg_text_size(2.5),
|
||||
"font-family": "OpenGost Type B TT",
|
||||
"text-anchor": text_anchor,
|
||||
"alignment-baseline": "baseline",
|
||||
"dominant-baseline": "baseline",
|
||||
},
|
||||
)
|
||||
)
|
||||
for obj in self.annotations.get("plan_level_objs", []):
|
||||
self.draw_plan_level_annotation(obj)
|
||||
|
||||
if self.annotations.get("section_level_obj"):
|
||||
matrix_world = self.annotations["section_level_obj"].matrix_world
|
||||
classes = self.get_attribute_classes(self.annotations["section_level_obj"])
|
||||
for spline in self.annotations["section_level_obj"].data.splines:
|
||||
classes = ["annotation", "section-level"]
|
||||
points = self.get_spline_points(spline)
|
||||
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
|
||||
d = " ".join(
|
||||
@@ -269,8 +224,6 @@ class SvgWriter:
|
||||
)
|
||||
d = "M{}".format(d[1:])
|
||||
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
|
||||
path["marker-start"] = "url(#section-level-marker)"
|
||||
path["stroke-dasharray"] = "12.5, 3, 3, 3"
|
||||
text_position = Vector(
|
||||
(
|
||||
(x_offset + projected_points[0].x) * self.scale,
|
||||
@@ -299,8 +252,8 @@ class SvgWriter:
|
||||
|
||||
if self.annotations.get("stair_obj"):
|
||||
matrix_world = self.annotations["stair_obj"].matrix_world
|
||||
classes = self.get_attribute_classes(self.annotations["stair_obj"])
|
||||
for spline in self.annotations["stair_obj"].data.splines:
|
||||
classes = ["annotation", "stair"]
|
||||
points = self.get_spline_points(spline)
|
||||
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
|
||||
d = " ".join(
|
||||
@@ -327,7 +280,18 @@ class SvgWriter:
|
||||
},
|
||||
)
|
||||
)
|
||||
self.draw_text_annotations()
|
||||
|
||||
for obj in self.annotations.get("elevation_objs", []):
|
||||
self.draw_elevation_annotation(obj)
|
||||
|
||||
for obj in self.annotations.get("section_objs", []):
|
||||
self.draw_section_annotation(obj)
|
||||
|
||||
for text_obj in self.annotations.get("text_objs", []):
|
||||
self.draw_text_annotation(text_obj, text_obj.location)
|
||||
|
||||
if self.annotations.get("break_obj"):
|
||||
self.draw_break_annotations(self.annotations["break_obj"])
|
||||
|
||||
def draw_ifc_annotation(self):
|
||||
x_offset = self.raw_width / 2
|
||||
@@ -349,13 +313,13 @@ class SvgWriter:
|
||||
)
|
||||
)
|
||||
|
||||
def draw_misc_annotation(self, obj, classes):
|
||||
def draw_misc_annotation(self, obj):
|
||||
# We have to decide whether this should come from Blender or from IFC.
|
||||
# For the moment, for convenience of experimenting with ideas, it comes
|
||||
# from Blender. In the future, it should probably come from IFC.
|
||||
if not isinstance(obj.data, bpy.types.Mesh):
|
||||
return
|
||||
classes.extend(self.get_attribute_classes(obj))
|
||||
classes = self.get_attribute_classes(obj)
|
||||
if len(obj.data.polygons) == 0:
|
||||
self.draw_edge_annotation(obj, classes)
|
||||
return
|
||||
@@ -365,6 +329,7 @@ class SvgWriter:
|
||||
for polygon in obj.data.polygons:
|
||||
points = [obj.data.vertices[v] for v in polygon.vertices]
|
||||
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
|
||||
projected_points.append(projected_points[0])
|
||||
d = " ".join(
|
||||
[
|
||||
"L {} {}".format((x_offset + p.x) * self.scale, (y_offset - p.y) * self.scale)
|
||||
@@ -375,12 +340,12 @@ class SvgWriter:
|
||||
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
|
||||
|
||||
def get_attribute_classes(self, obj):
|
||||
classes = [obj.name.split("/")[0]]
|
||||
for slot in obj.material_slots:
|
||||
if slot.material:
|
||||
classes.append("material-{}".format(re.sub("[^0-9a-zA-Z]+", "", slot.material.name)))
|
||||
global_id = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id).GlobalId
|
||||
classes.append("globalid-{}".format(global_id))
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
global_id = "GlobalId-{}".format(element.GlobalId)
|
||||
predefined_type = "PredefinedType-" + self.canonicalise_class_name(
|
||||
str(ifcopenshell.util.element.get_predefined_type(element))
|
||||
)
|
||||
classes = [global_id, element.is_a(), predefined_type]
|
||||
for attribute in self.annotations.get("attributes", []):
|
||||
result = self.get_obj_value(obj, attribute)
|
||||
if result:
|
||||
@@ -389,6 +354,9 @@ class SvgWriter:
|
||||
)
|
||||
return classes
|
||||
|
||||
def canonicalise_class_name(self, name):
|
||||
return re.sub("[^0-9a-zA-Z]+", "", name)
|
||||
|
||||
def get_obj_value(self, obj, key):
|
||||
# This is a duplicate implementation of the IFC selector key in Blender
|
||||
# In the future if all this becomes purely IFC based this can be deleted
|
||||
@@ -414,14 +382,14 @@ class SvgWriter:
|
||||
if result:
|
||||
return result.string_value
|
||||
|
||||
def draw_line_annotation(self, obj_data, classes):
|
||||
def draw_line_annotation(self, obj_data):
|
||||
# TODO: properly scope these offsets
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
|
||||
obj, data = obj_data
|
||||
|
||||
classes.extend(["annotation"])
|
||||
classes = self.get_attribute_classes(obj)
|
||||
matrix_world = obj.matrix_world
|
||||
|
||||
if isinstance(data, bpy.types.Curve):
|
||||
@@ -455,71 +423,183 @@ class SvgWriter:
|
||||
self.svg.line(start=tuple(start * self.scale), end=tuple(end * self.scale), class_=" ".join(classes))
|
||||
)
|
||||
|
||||
def draw_text_annotations(self):
|
||||
def draw_leader_annotations(self):
|
||||
for obj in self.annotations.get("leader_objs", []):
|
||||
self.draw_line_annotation((obj, obj.data))
|
||||
spline = obj.data.splines[0]
|
||||
spline_points = spline.bezier_points if spline.bezier_points else spline.points
|
||||
if spline_points:
|
||||
position = obj.matrix_world @ spline_points[0].co.xyz
|
||||
else:
|
||||
position = Vector((0, 0, 0))
|
||||
self.draw_text_annotation(obj, position)
|
||||
|
||||
def draw_section_annotation(self, obj):
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
|
||||
for text_obj in self.annotations.get("text_objs", []):
|
||||
text_position = self.project_point_onto_camera(text_obj.location)
|
||||
text_position = Vector(((x_offset + text_position.x), (y_offset - text_position.y)))
|
||||
self.draw_line_annotation((obj, obj.data))
|
||||
|
||||
local_x_axis = text_obj.matrix_world.to_quaternion() @ Vector((1, 0, 0))
|
||||
projected_x_axis = self.project_point_onto_camera(text_obj.location + local_x_axis)
|
||||
angle = math.degrees(
|
||||
(Vector((x_offset + projected_x_axis.x, y_offset - projected_x_axis.y)) - text_position).angle_signed(
|
||||
Vector((1, 0))
|
||||
)
|
||||
)
|
||||
v1 = self.project_point_onto_camera(obj.matrix_world @ Vector((0, 0, 0)))
|
||||
v2 = self.project_point_onto_camera(obj.matrix_world @ Vector((0, 0, -1)))
|
||||
angle = -math.degrees((v2 - v1).xy.angle_signed(Vector((0, 1))))
|
||||
|
||||
for vert in obj.data.vertices:
|
||||
point = obj.matrix_world @ vert.co
|
||||
symbol_position = self.project_point_onto_camera(point)
|
||||
symbol_position = Vector(((x_offset + symbol_position.x), (y_offset - symbol_position.y)))
|
||||
transform = "rotate({}, {}, {})".format(
|
||||
angle,
|
||||
(text_position * self.scale)[0],
|
||||
(text_position * self.scale)[1],
|
||||
(symbol_position * self.scale)[0],
|
||||
(symbol_position * self.scale)[1],
|
||||
)
|
||||
|
||||
if text_obj.data.BIMTextProperties.symbol != "None":
|
||||
self.svg.add(
|
||||
self.svg.use(
|
||||
"#{}".format(text_obj.data.BIMTextProperties.symbol), insert=tuple(text_position * self.scale)
|
||||
)
|
||||
)
|
||||
|
||||
if text_obj.data.align_x == "CENTER":
|
||||
text_anchor = "middle"
|
||||
elif text_obj.data.align_x == "RIGHT":
|
||||
text_anchor = "end"
|
||||
else:
|
||||
text_anchor = "start"
|
||||
|
||||
if text_obj.data.align_y == "CENTER":
|
||||
alignment_baseline = "middle"
|
||||
elif text_obj.data.align_y == "TOP":
|
||||
alignment_baseline = "hanging"
|
||||
else:
|
||||
alignment_baseline = "baseline"
|
||||
|
||||
text_body = text_obj.data.body
|
||||
if text_obj.name in self.annotations.get("template_variables", {}):
|
||||
text_body = pystache.render(text_body, self.annotations["template_variables"][text_obj.name])
|
||||
|
||||
for line_number, text_line in enumerate(text_body.split("\n")):
|
||||
self.svg.add(
|
||||
self.svg.text(
|
||||
text_line,
|
||||
insert=tuple((text_position * self.scale) + Vector((0, 3.5 * line_number))),
|
||||
class_=" ".join(self.get_attribute_classes(text_obj)),
|
||||
**{
|
||||
"font-size": annotation.Annotator.get_svg_text_size(
|
||||
text_obj.data.BIMTextProperties.font_size
|
||||
),
|
||||
"font-family": "OpenGost Type B TT",
|
||||
"text-anchor": text_anchor,
|
||||
"alignment-baseline": alignment_baseline,
|
||||
"dominant-baseline": alignment_baseline,
|
||||
"transform": transform,
|
||||
},
|
||||
)
|
||||
self.svg.add(
|
||||
self.svg.use("#section-arrow", insert=tuple(symbol_position * self.scale), transform=transform)
|
||||
)
|
||||
self.svg.add(self.svg.use("#section-tag", insert=tuple(symbol_position * self.scale)))
|
||||
|
||||
reference_id, sheet_id = self.get_reference_and_sheet_id_from_annotation(tool.Ifc.get_entity(obj))
|
||||
text_position = list(symbol_position * self.scale)
|
||||
text_style = {
|
||||
"font-size": annotation.Annotator.get_svg_text_size(2.5),
|
||||
"font-family": "OpenGost Type B TT",
|
||||
"text-anchor": "middle",
|
||||
"alignment-baseline": "middle",
|
||||
"dominant-baseline": "middle",
|
||||
}
|
||||
self.svg.add(self.svg.text(reference_id, insert=(text_position[0], text_position[1] - 2.5), **text_style))
|
||||
self.svg.add(self.svg.text(sheet_id, insert=(text_position[0], text_position[1] + 2.5), **text_style))
|
||||
|
||||
def draw_elevation_annotation(self, obj):
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
symbol_position = self.project_point_onto_camera(obj.location)
|
||||
symbol_position = Vector(((x_offset + symbol_position.x), (y_offset - symbol_position.y)))
|
||||
|
||||
v1 = self.project_point_onto_camera(obj.matrix_world @ Vector((0, 0, 0)))
|
||||
v2 = self.project_point_onto_camera(obj.matrix_world @ Vector((0, 0, -1)))
|
||||
angle = -math.degrees((v2 - v1).xy.angle_signed(Vector((0, 1))))
|
||||
|
||||
transform = "rotate({}, {}, {})".format(
|
||||
angle,
|
||||
(symbol_position * self.scale)[0],
|
||||
(symbol_position * self.scale)[1],
|
||||
)
|
||||
|
||||
self.svg.add(self.svg.use("#elevation-arrow", insert=tuple(symbol_position * self.scale), transform=transform))
|
||||
self.svg.add(self.svg.use("#elevation-tag", insert=tuple(symbol_position * self.scale)))
|
||||
|
||||
reference_id, sheet_id = self.get_reference_and_sheet_id_from_annotation(tool.Ifc.get_entity(obj))
|
||||
text_position = list(symbol_position * self.scale)
|
||||
text_style = {
|
||||
"font-size": annotation.Annotator.get_svg_text_size(2.5),
|
||||
"font-family": "OpenGost Type B TT",
|
||||
"text-anchor": "middle",
|
||||
"alignment-baseline": "middle",
|
||||
"dominant-baseline": "middle",
|
||||
}
|
||||
self.svg.add(self.svg.text(reference_id, insert=(text_position[0], text_position[1] - 2.5), **text_style))
|
||||
self.svg.add(self.svg.text(sheet_id, insert=(text_position[0], text_position[1] + 2.5), **text_style))
|
||||
|
||||
def get_reference_and_sheet_id_from_annotation(self, element):
|
||||
reference_id = "-"
|
||||
sheet_id = "-"
|
||||
drawing = tool.Drawing.get_annotation_element(element)
|
||||
reference = tool.Drawing.get_drawing_reference(drawing)
|
||||
if reference:
|
||||
sheet = tool.Drawing.get_reference_sheet(reference)
|
||||
if sheet:
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
reference_id = reference.ItemReference or "-"
|
||||
sheet_id = sheet.DocumentId or "-"
|
||||
else:
|
||||
reference_id = reference.Identification or "-"
|
||||
sheet_id = sheet.Identification or "-"
|
||||
return (reference_id, sheet_id)
|
||||
return ("-", "-")
|
||||
|
||||
def draw_text_annotation(self, text_obj, position):
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
element = tool.Ifc.get_entity(text_obj)
|
||||
rep = ifcopenshell.util.representation.get_representation(element, "Plan", "Annotation")
|
||||
text_literal = [i for i in rep.Items if i.is_a("IfcTextLiteral")][0]
|
||||
|
||||
text_position = self.project_point_onto_camera(position)
|
||||
text_position = Vector(((x_offset + text_position.x), (y_offset - text_position.y)))
|
||||
|
||||
local_x_axis = text_obj.matrix_world.to_quaternion() @ Vector((1, 0, 0))
|
||||
projected_x_axis = self.project_point_onto_camera(position + local_x_axis)
|
||||
angle = math.degrees(
|
||||
(Vector((x_offset + projected_x_axis.x, y_offset - projected_x_axis.y)) - text_position).angle_signed(
|
||||
Vector((1, 0))
|
||||
)
|
||||
)
|
||||
|
||||
transform = "rotate({}, {}, {})".format(
|
||||
angle,
|
||||
(text_position * self.scale)[0],
|
||||
(text_position * self.scale)[1],
|
||||
)
|
||||
|
||||
if text_obj.BIMTextProperties.symbol != "None":
|
||||
self.svg.add(
|
||||
self.svg.use(f"#{text_obj.BIMTextProperties.symbol}", insert=tuple(text_position * self.scale))
|
||||
)
|
||||
|
||||
if text_literal.BoxAlignment == "top-left":
|
||||
alignment_baseline = "hanging"
|
||||
text_anchor = "start"
|
||||
elif text_literal.BoxAlignment == "top-middle":
|
||||
alignment_baseline = "hanging"
|
||||
text_anchor = "middle"
|
||||
elif text_literal.BoxAlignment == "top-right":
|
||||
alignment_baseline = "hanging"
|
||||
text_anchor = "end"
|
||||
elif text_literal.BoxAlignment == "middle-left":
|
||||
alignment_baseline = "middle"
|
||||
text_anchor = "start"
|
||||
elif text_literal.BoxAlignment == "center":
|
||||
alignment_baseline = "middle"
|
||||
text_anchor = "middle"
|
||||
elif text_literal.BoxAlignment == "middle-right":
|
||||
alignment_baseline = "middle"
|
||||
text_anchor = "end"
|
||||
elif text_literal.BoxAlignment == "bottom-left":
|
||||
alignment_baseline = "baseline"
|
||||
text_anchor = "start"
|
||||
elif text_literal.BoxAlignment == "bottom-middle":
|
||||
alignment_baseline = "baseline"
|
||||
text_anchor = "middle"
|
||||
elif text_literal.BoxAlignment == "bottom-right":
|
||||
alignment_baseline = "baseline"
|
||||
text_anchor = "end"
|
||||
|
||||
literal = text_literal.Literal
|
||||
|
||||
product = tool.Drawing.get_text_product(element)
|
||||
selector = ifcopenshell.util.selector.Selector
|
||||
variables = {}
|
||||
for variable in re.findall("{{.*?}}", literal):
|
||||
literal = literal.replace(variable, selector.get_element_value(product, variable[2:-2]) or "")
|
||||
|
||||
for line_number, text_line in enumerate(literal.replace("\\n", "\n").split("\n")):
|
||||
self.svg.add(
|
||||
self.svg.text(
|
||||
text_line,
|
||||
insert=tuple((text_position * self.scale) + Vector((0, 3.5 * line_number))),
|
||||
class_=" ".join(self.get_attribute_classes(text_obj)),
|
||||
**{
|
||||
"font-size": annotation.Annotator.get_svg_text_size(text_obj.BIMTextProperties.font_size),
|
||||
"font-family": "OpenGost Type B TT",
|
||||
"text-anchor": text_anchor,
|
||||
"alignment-baseline": alignment_baseline,
|
||||
"dominant-baseline": alignment_baseline,
|
||||
"transform": transform,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
def draw_break_annotations(self, break_obj):
|
||||
x_offset = self.raw_width / 2
|
||||
@@ -549,7 +629,54 @@ class SvgWriter:
|
||||
d = "M{}".format(d[1:])
|
||||
path = self.svg.add(self.svg.path(d=d, class_=" ".join(["breakline"])))
|
||||
|
||||
def draw_plan_level_annotation(self, obj):
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
matrix_world = obj.matrix_world
|
||||
classes = self.get_attribute_classes(obj)
|
||||
for spline in obj.data.splines:
|
||||
points = self.get_spline_points(spline)
|
||||
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
|
||||
d = " ".join(
|
||||
[
|
||||
"L {} {}".format((x_offset + p.x) * self.scale, (y_offset - p.y) * self.scale)
|
||||
for p in projected_points
|
||||
]
|
||||
)
|
||||
d = "M{}".format(d[1:])
|
||||
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
|
||||
text_position = Vector(
|
||||
(
|
||||
(x_offset + projected_points[0].x) * self.scale,
|
||||
((y_offset - projected_points[0].y) * self.scale) - 2.5,
|
||||
)
|
||||
)
|
||||
# TODO: allow metric to be configurable
|
||||
rl = ((matrix_world @ points[0].co).xyz + obj.location).z
|
||||
if bpy.context.scene.unit_settings.system == "IMPERIAL":
|
||||
rl = helper.format_distance(rl)
|
||||
else:
|
||||
rl = "{:.3f}m".format(rl)
|
||||
if projected_points[0].x > projected_points[-1].x:
|
||||
text_anchor = "end"
|
||||
else:
|
||||
text_anchor = "start"
|
||||
self.svg.add(
|
||||
self.svg.text(
|
||||
"RL +{}".format(rl),
|
||||
insert=tuple(text_position),
|
||||
**{
|
||||
"font-size": annotation.Annotator.get_svg_text_size(2.5),
|
||||
"font-family": "OpenGost Type B TT",
|
||||
"text-anchor": text_anchor,
|
||||
"alignment-baseline": "baseline",
|
||||
"dominant-baseline": "baseline",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
def draw_dimension_annotations(self, dimension_obj, text_override=None):
|
||||
classes = self.get_attribute_classes(dimension_obj)
|
||||
matrix_world = dimension_obj.matrix_world
|
||||
for spline in dimension_obj.data.splines:
|
||||
points = self.get_spline_points(spline)
|
||||
@@ -558,7 +685,7 @@ class SvgWriter:
|
||||
continue
|
||||
v0_global = matrix_world @ points[i].co.xyz
|
||||
v1_global = matrix_world @ points[i + 1].co.xyz
|
||||
self.draw_dimension_annotation(v0_global, v1_global, text_override)
|
||||
self.draw_dimension_annotation(v0_global, v1_global, classes, text_override)
|
||||
|
||||
def draw_measureit_arch_dimension_annotations(self):
|
||||
try:
|
||||
@@ -568,10 +695,11 @@ class SvgWriter:
|
||||
except:
|
||||
return
|
||||
for coord in coords:
|
||||
self.draw_dimension_annotation(Vector(coord[0]), Vector(coord[1]))
|
||||
self.draw_dimension_annotation(
|
||||
Vector(coord[0]), Vector(coord[1]), ["IfcAnnotation", "PredefinedType-DIMENSION"]
|
||||
)
|
||||
|
||||
def draw_dimension_annotation(self, v0_global, v1_global, text_override=None):
|
||||
classes = ["annotation", "dimension"]
|
||||
def draw_dimension_annotation(self, v0_global, v1_global, classes, text_override=None):
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
v0 = self.project_point_onto_camera(v0_global)
|
||||
@@ -593,8 +721,6 @@ class SvgWriter:
|
||||
line = self.svg.add(
|
||||
self.svg.line(start=tuple(start * self.scale), end=tuple(end * self.scale), class_=" ".join(classes))
|
||||
)
|
||||
line["marker-start"] = "url(#dimension-marker-start)"
|
||||
line["marker-end"] = "url(#dimension-marker-end)"
|
||||
if text_override is not None:
|
||||
text = text_override
|
||||
else:
|
||||
@@ -613,6 +739,7 @@ class SvgWriter:
|
||||
)
|
||||
|
||||
def project_point_onto_camera(self, point):
|
||||
# TODO is this needlessly complex?
|
||||
return self.camera.matrix_world.inverted() @ geometry.intersect_line_plane(
|
||||
point.xyz,
|
||||
point.xyz - Vector(self.camera_projection),
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import blenderbim.bim.helper
|
||||
import blenderbim.tool as tool
|
||||
from bpy.types import Panel
|
||||
from blenderbim.bim.module.drawing.data import TextData, SheetsData, DrawingsData
|
||||
|
||||
|
||||
class BIM_PT_camera(Panel):
|
||||
@@ -44,13 +47,13 @@ class BIM_PT_camera(Panel):
|
||||
|
||||
col = layout.column(align=True)
|
||||
row = col.row(align=True)
|
||||
row.prop(dprops, "has_underlay", icon="OUTLINER_OB_IMAGE")
|
||||
row.prop(props, "has_underlay", icon="OUTLINER_OB_IMAGE")
|
||||
row.prop(dprops, "should_use_underlay_cache", text="", icon="FILE_REFRESH")
|
||||
row = col.row(align=True)
|
||||
row.prop(dprops, "has_linework", icon="IMAGE_DATA")
|
||||
row.prop(props, "has_linework", icon="IMAGE_DATA")
|
||||
row.prop(dprops, "should_use_linework_cache", text="", icon="FILE_REFRESH")
|
||||
row = col.row(align=True)
|
||||
row.prop(dprops, "has_annotation", icon="MOD_EDGESPLIT")
|
||||
row.prop(props, "has_annotation", icon="MOD_EDGESPLIT")
|
||||
row.prop(dprops, "should_use_annotation_cache", text="", icon="FILE_REFRESH")
|
||||
|
||||
row = layout.row()
|
||||
@@ -64,9 +67,6 @@ class BIM_PT_camera(Panel):
|
||||
row = layout.row()
|
||||
row.operator("bim.resize_text")
|
||||
|
||||
row = layout.row()
|
||||
row.prop(props, "target_view")
|
||||
|
||||
row = layout.row()
|
||||
row.prop(props, "cut_objects")
|
||||
if props.cut_objects == "CUSTOM":
|
||||
@@ -146,44 +146,62 @@ class BIM_PT_drawing_underlay(Panel):
|
||||
|
||||
|
||||
class BIM_PT_drawings(Panel):
|
||||
bl_label = "SVG Drawings"
|
||||
bl_label = "Drawings"
|
||||
bl_idname = "BIM_PT_drawings"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "output"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "BIM Documentation"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
props = context.scene.DocProperties
|
||||
if not DrawingsData.is_loaded:
|
||||
DrawingsData.load()
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.add_drawing")
|
||||
row.operator("bim.refresh_drawing_list", icon="FILE_REFRESH", text="")
|
||||
self.props = context.scene.DocProperties
|
||||
|
||||
if props.drawings:
|
||||
if props.active_drawing_index < len(props.drawings):
|
||||
if not self.props.is_editing_drawings:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=f"{DrawingsData.data['total_drawings']} Drawings Found", icon="IMAGE_DATA")
|
||||
row.operator("bim.load_drawings", text="", icon="IMPORT")
|
||||
return
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "target_view", text="")
|
||||
row.prop(self.props, "location_hint", text="")
|
||||
row.operator("bim.add_drawing", text="", icon="ADD")
|
||||
row.operator("bim.disable_editing_drawings", text="", icon="CANCEL")
|
||||
|
||||
if self.props.drawings:
|
||||
if self.props.active_drawing_index < len(self.props.drawings):
|
||||
active_drawing = self.props.drawings[self.props.active_drawing_index]
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
op = row.operator("bim.open_view", icon="URL", text="")
|
||||
op.view = props.active_drawing.name
|
||||
row.operator("bim.remove_drawing", icon="X", text="").index = props.active_drawing_index
|
||||
layout.template_list("BIM_UL_drawinglist", "", props, "drawings", props, "active_drawing_index")
|
||||
op.view = active_drawing.name
|
||||
op = row.operator("bim.activate_view", icon="OUTLINER_OB_CAMERA", text="")
|
||||
op.drawing = active_drawing.ifc_definition_id
|
||||
row.operator("bim.create_drawing", text="", icon="OUTPUT")
|
||||
row.operator("bim.remove_drawing", icon="X", text="").drawing = active_drawing.ifc_definition_id
|
||||
self.layout.template_list(
|
||||
"BIM_UL_drawinglist", "", self.props, "drawings", self.props, "active_drawing_index"
|
||||
)
|
||||
|
||||
row = layout.row()
|
||||
row.operator("bim.add_ifc_file")
|
||||
# Commented out until federated drawing generation is rebuilt
|
||||
# row = self.layout.row()
|
||||
# row.operator("bim.add_ifc_file")
|
||||
|
||||
for index, ifc_file in enumerate(props.ifc_files):
|
||||
row = layout.row(align=True)
|
||||
row.prop(ifc_file, "name", text="IFC #{}".format(index + 1))
|
||||
row.operator("bim.select_doc_ifc_file", icon="FILE_FOLDER", text="").index = index
|
||||
row.operator("bim.remove_ifc_file", icon="X", text="").index = index
|
||||
# for index, ifc_file in enumerate(self.props.ifc_files):
|
||||
# row = self.layout.row(align=True)
|
||||
# row.prop(ifc_file, "name", text="IFC #{}".format(index + 1))
|
||||
# row.operator("bim.select_doc_ifc_file", icon="FILE_FOLDER", text="").index = index
|
||||
# row.operator("bim.remove_ifc_file", icon="X", text="").index = index
|
||||
|
||||
|
||||
class BIM_PT_schedules(Panel):
|
||||
bl_label = "ODS Schedules"
|
||||
bl_label = "Schedules"
|
||||
bl_idname = "BIM_PT_schedules"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "output"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "BIM Documentation"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
@@ -205,150 +223,192 @@ class BIM_PT_schedules(Panel):
|
||||
|
||||
|
||||
class BIM_PT_sheets(Panel):
|
||||
bl_label = "SVG Sheets"
|
||||
bl_label = "Sheets"
|
||||
bl_idname = "BIM_PT_sheets"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "output"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "BIM Documentation"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
props = context.scene.DocProperties
|
||||
if not SheetsData.is_loaded:
|
||||
SheetsData.load()
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "titleblock", text="")
|
||||
row.operator("bim.add_sheet")
|
||||
self.props = context.scene.DocProperties
|
||||
|
||||
if props.sheets:
|
||||
if not self.props.is_editing_sheets:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=f"{SheetsData.data['total_sheets']} Sheets Found", icon="FILE")
|
||||
row.operator("bim.load_sheets", text="", icon="IMPORT")
|
||||
return
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "titleblock", text="")
|
||||
row.operator("bim.add_sheet", text="", icon="ADD")
|
||||
row.operator("bim.disable_editing_sheets", text="", icon="CANCEL")
|
||||
|
||||
if self.props.sheets and self.props.active_sheet_index < len(self.props.sheets):
|
||||
active_sheet = self.props.sheets[self.props.active_sheet_index]
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
row.operator("bim.open_sheet", icon="URL", text="")
|
||||
row.operator("bim.remove_sheet", icon="X", text="").index = props.active_sheet_index
|
||||
row.operator("bim.add_drawing_to_sheet", icon="IMAGE_PLANE", text="")
|
||||
row.operator("bim.add_schedule_to_sheet", icon="PRESET_NEW", text="")
|
||||
row.operator("bim.create_sheets", icon="FILE_REFRESH", text="")
|
||||
if active_sheet.is_sheet:
|
||||
row.operator("bim.remove_sheet", icon="X", text="").sheet = active_sheet.ifc_definition_id
|
||||
else:
|
||||
op = row.operator("bim.remove_drawing_from_sheet", icon="X", text="")
|
||||
op.reference = active_sheet.ifc_definition_id
|
||||
|
||||
layout.template_list("BIM_UL_generic", "", props, "sheets", props, "active_sheet_index")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.add_drawing_to_sheet")
|
||||
row.operator("bim.add_schedule_to_sheet")
|
||||
row = layout.row()
|
||||
row.operator("bim.create_sheets")
|
||||
self.layout.template_list("BIM_UL_sheets", "", self.props, "sheets", self.props, "active_sheet_index")
|
||||
|
||||
|
||||
class BIM_PT_text(Panel):
|
||||
bl_label = "Text Paper Space"
|
||||
bl_label = "IFC Text"
|
||||
bl_idname = "BIM_PT_text"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "data"
|
||||
bl_context = "object"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return type(context.curve) is bpy.types.TextCurve
|
||||
if not tool.Ifc.get() or not context.active_object:
|
||||
return
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
if not element:
|
||||
return
|
||||
return element.is_a("IfcAnnotation") and element.ObjectType in ["TEXT", "TEXT_LEADER"]
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
props = context.active_object.data.BIMTextProperties
|
||||
if not TextData.is_loaded:
|
||||
TextData.load()
|
||||
|
||||
row = layout.row()
|
||||
row.operator("bim.propagate_text_data")
|
||||
props = context.active_object.BIMTextProperties
|
||||
|
||||
row = layout.row()
|
||||
if props.is_editing:
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.edit_text", icon="CHECKMARK")
|
||||
row.operator("bim.disable_editing_text", icon="CANCEL", text="")
|
||||
blenderbim.bim.helper.draw_attributes(props.attributes, self.layout)
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.operator("bim.enable_editing_text", icon="GREASEPENCIL")
|
||||
|
||||
for attribute in TextData.data["attributes"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=attribute["name"])
|
||||
row.label(text=attribute["value"])
|
||||
|
||||
if props.is_editing_product:
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "relating_product", text="")
|
||||
row.operator("bim.edit_text_product", icon="CHECKMARK", text="")
|
||||
row.operator("bim.disable_editing_text_product", icon="CANCEL", text="")
|
||||
else:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=TextData.data["relating_product"] or "No Relating Product", icon="OBJECT_DATA")
|
||||
row.operator("bim.enable_editing_text_product", icon="GREASEPENCIL", text="")
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(props, "font_size")
|
||||
row = layout.row()
|
||||
row = self.layout.row()
|
||||
row.prop(props, "symbol")
|
||||
row = layout.row()
|
||||
row.prop(props, "related_element")
|
||||
|
||||
row = layout.row()
|
||||
row.operator("bim.add_variable")
|
||||
|
||||
for index, variable in enumerate(props.variables):
|
||||
row = layout.row(align=True)
|
||||
row.prop(variable, "name")
|
||||
row.operator("bim.remove_variable", icon="X", text="").index = index
|
||||
row = layout.row()
|
||||
row.prop(variable, "prop_key")
|
||||
|
||||
|
||||
class BIM_PT_annotation_utilities(Panel):
|
||||
bl_idname = "BIM_PT_annotation_utilities"
|
||||
bl_label = "Annotation"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "BlenderBIM"
|
||||
bl_category = "BIM Documentation"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.clean_wireframes")
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.add_grid")
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.add_sections_annotations")
|
||||
self.props = context.scene.DocProperties
|
||||
|
||||
row = layout.row(align=True)
|
||||
op = row.operator("bim.add_annotation", text="Dim", icon="ARROW_LEFTRIGHT")
|
||||
op.obj_name = "Dimension"
|
||||
op.object_type = "DIMENSION"
|
||||
op.data_type = "curve"
|
||||
op = row.operator("bim.add_annotation", text="Dim (Eq)", icon="ARROW_LEFTRIGHT")
|
||||
op.obj_name = "Equal"
|
||||
op.object_type = "EQUAL_DIMENSION"
|
||||
op.data_type = "curve"
|
||||
|
||||
row = layout.row(align=True)
|
||||
op = row.operator("bim.add_annotation", text="Text", icon="SMALL_CAPS")
|
||||
op.data_type = "text"
|
||||
op.object_type = "TEXT"
|
||||
op.data_type = "empty"
|
||||
op = row.operator("bim.add_annotation", text="Leader", icon="TRACKING_BACKWARDS")
|
||||
op.obj_name = "Leader"
|
||||
op.object_type = "TEXT_LEADER"
|
||||
op.data_type = "curve"
|
||||
|
||||
row = layout.row(align=True)
|
||||
op = row.operator("bim.add_annotation", text="Stair Arrow", icon="SCREEN_BACK")
|
||||
op.obj_name = "Stair"
|
||||
op.object_type = "STAIR_ARROW"
|
||||
op.data_type = "curve"
|
||||
op = row.operator("bim.add_annotation", text="Hidden", icon="CON_TRACKTO")
|
||||
op.obj_name = "Hidden"
|
||||
op.object_type = "HIDDEN_LINE"
|
||||
op.data_type = "mesh"
|
||||
|
||||
row = layout.row(align=True)
|
||||
op = row.operator("bim.add_annotation", text="Level (Plan)", icon="SORTBYEXT")
|
||||
op.obj_name = "Plan Level"
|
||||
op.object_type = "PLAN_LEVEL"
|
||||
op.data_type = "curve"
|
||||
op = row.operator("bim.add_annotation", text="Level (Section)", icon="TRIA_DOWN")
|
||||
op.obj_name = "Section Level"
|
||||
op.object_type = "SECTION_LEVEL"
|
||||
op.data_type = "curve"
|
||||
|
||||
row = layout.row(align=True)
|
||||
op = row.operator("bim.add_annotation", text="Breakline", icon="FCURVE")
|
||||
op.obj_name = "Break"
|
||||
op.object_type = "BREAKLINE"
|
||||
op.data_type = "mesh"
|
||||
op = row.operator("bim.add_annotation", text="Misc", icon="MESH_MONKEY")
|
||||
op.obj_name = "Misc"
|
||||
op.object_type = "MISC"
|
||||
op.data_type = "mesh"
|
||||
|
||||
props = context.scene.DocProperties
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.add_drawing")
|
||||
row.operator("bim.refresh_drawing_list", icon="FILE_REFRESH", text="")
|
||||
|
||||
if props.drawings:
|
||||
if props.active_drawing_index < len(props.drawings):
|
||||
op = row.operator("bim.open_view", icon="URL", text="")
|
||||
op.view = props.active_drawing.name
|
||||
row.operator("bim.remove_drawing", icon="X", text="").index = props.active_drawing_index
|
||||
layout.template_list("BIM_UL_drawinglist", "", props, "drawings", props, "active_drawing_index")
|
||||
|
||||
layout.prop(props, "should_draw_decorations")
|
||||
layout.prop(props, "decorations_colour")
|
||||
row.prop(self.props, "should_draw_decorations", text="Viewport Annotations")
|
||||
|
||||
|
||||
class BIM_UL_drawinglist(bpy.types.UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
row.prop(item, "name", text="", emboss=False)
|
||||
op = row.operator("bim.open_view_camera", icon="OUTLINER_OB_CAMERA", text="")
|
||||
op.view_name = item.name
|
||||
icon = "UV_FACESEL"
|
||||
if item.target_view == "ELEVATION_VIEW":
|
||||
icon = "UV_VERTEXSEL"
|
||||
elif item.target_view == "SECTION_VIEW":
|
||||
icon = "UV_EDGESEL"
|
||||
elif item.target_view == "REFLECTED_PLAN_VIEW":
|
||||
icon = "XRAY"
|
||||
elif item.target_view == "MODEL_VIEW":
|
||||
icon = "SNAP_VOLUME"
|
||||
row.prop(item, "name", text="", icon=icon, emboss=False)
|
||||
else:
|
||||
layout.label(text="", translate=False)
|
||||
|
||||
|
||||
class BIM_UL_sheets(bpy.types.UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
|
||||
if item.is_sheet:
|
||||
if item.is_expanded:
|
||||
row.operator(
|
||||
"bim.contract_sheet", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN"
|
||||
).sheet = item.ifc_definition_id
|
||||
else:
|
||||
row.operator(
|
||||
"bim.expand_sheet", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT"
|
||||
).sheet = item.ifc_definition_id
|
||||
else:
|
||||
row.label(text="", icon="BLANK1")
|
||||
if item.reference_type == "DRAWING":
|
||||
row.label(text="", icon="IMAGE_DATA")
|
||||
|
||||
name = "{} - {}".format(item.identification or "X", item.name or "Unnamed")
|
||||
row.label(text=name)
|
||||
else:
|
||||
layout.label(text="", translate=False)
|
||||
|
||||
@@ -25,6 +25,7 @@ classes = (
|
||||
operator.EditObjectPlacement,
|
||||
operator.GetRepresentationIfcParameters,
|
||||
operator.OverrideDelete,
|
||||
operator.OverrideOutlinerDelete,
|
||||
operator.OverrideDuplicateMove,
|
||||
operator.OverrideDuplicateMoveLinked,
|
||||
operator.OverridePasteBuffer,
|
||||
|
||||
@@ -40,6 +40,7 @@ class RepresentationsData:
|
||||
def representations(cls):
|
||||
results = []
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
representations = []
|
||||
if element.is_a("IfcProduct") and element.Representation:
|
||||
representations = element.Representation.Representations
|
||||
elif element.is_a("IfcTypeProduct"):
|
||||
@@ -49,15 +50,17 @@ class RepresentationsData:
|
||||
if representation_type == "MappedRepresentation":
|
||||
representation_type = representation.Items[0].MappingSource.MappedRepresentation.RepresentationType
|
||||
representation_type += "*"
|
||||
results.append(
|
||||
{
|
||||
"id": representation.id(),
|
||||
"ContextType": representation.ContextOfItems.ContextType,
|
||||
"ContextIdentifier": representation.ContextOfItems.ContextIdentifier,
|
||||
"TargetView": representation.ContextOfItems.TargetView,
|
||||
"RepresentationType": representation_type,
|
||||
}
|
||||
)
|
||||
data = {
|
||||
"id": representation.id(),
|
||||
"ContextType": representation.ContextOfItems.ContextType or "",
|
||||
"ContextIdentifier": "",
|
||||
"TargetView": "",
|
||||
"RepresentationType": representation_type or "",
|
||||
}
|
||||
if representation.ContextOfItems.is_a("IfcGeometricRepresentationSubContext"):
|
||||
data["ContextIdentifier"] = representation.ContextOfItems.ContextIdentifier or ""
|
||||
data["TargetView"] = representation.ContextOfItems.TargetView or ""
|
||||
results.append(data)
|
||||
return results
|
||||
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import mathutils
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.unit
|
||||
from math import pi
|
||||
from mathutils import Vector, Matrix
|
||||
from mathutils import Vector, Matrix, geometry
|
||||
|
||||
|
||||
class Helper:
|
||||
@@ -215,6 +215,95 @@ class Helper:
|
||||
|
||||
return {"profile": outer_loop, "inner_curves": inner_loops, "extrusion": extrusion}
|
||||
|
||||
# An arbitrary face with voids is similar to a profile with voids for extrusion.
|
||||
# Before we begin we check that all faces are coplanar instead of finding suitable face.
|
||||
# Then we process the same way.
|
||||
# Only 2 parameters are returned as there is no extrusion.
|
||||
def auto_detect_curve_bounded_plane(self, mesh, tolerance=0.001):
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
|
||||
bm.faces.ensure_lookup_table()
|
||||
faces = bm.faces
|
||||
normal = faces[0].normal
|
||||
point = faces[0].verts[0].co
|
||||
for face in faces:
|
||||
pt_f = face.verts[0].co
|
||||
if (
|
||||
normal.dot(face.normal) < 1 - tolerance
|
||||
or abs(geometry.distance_point_to_plane(pt_f, point, normal)) > tolerance
|
||||
):
|
||||
raise ValueError("All faces must be coplanar and have same orientation")
|
||||
|
||||
loop_edges = set()
|
||||
for face in faces:
|
||||
potential_edges = set(face.edges)
|
||||
for face2 in faces:
|
||||
if face == face2:
|
||||
continue
|
||||
potential_edges -= set(face2.edges)
|
||||
loop_edges |= potential_edges
|
||||
|
||||
# Create loops from edges
|
||||
loops = []
|
||||
while loop_edges:
|
||||
edge = loop_edges.pop()
|
||||
loop = [edge]
|
||||
has_found_connected_edge = True
|
||||
while has_found_connected_edge:
|
||||
has_found_connected_edge = False
|
||||
for edge in loop_edges.copy():
|
||||
edge_verts = set(edge.verts)
|
||||
if edge_verts & set(loop[0].verts):
|
||||
loop.insert(0, edge)
|
||||
loop_edges.remove(edge)
|
||||
has_found_connected_edge = True
|
||||
elif edge_verts & set(loop[-1].verts):
|
||||
loop.append(edge)
|
||||
loop_edges.remove(edge)
|
||||
has_found_connected_edge = True
|
||||
loops.append(loop)
|
||||
|
||||
# Determine outer loop
|
||||
max_area = 0
|
||||
outer_loop = None
|
||||
inner_loops = []
|
||||
|
||||
for loop in loops:
|
||||
loop_vertices = []
|
||||
total_edges = len(loop)
|
||||
for i, edge in enumerate(loop):
|
||||
if i + 1 == total_edges and edge.verts[0] in loop[i - 1].verts:
|
||||
loop_vertices.append(edge.verts[0])
|
||||
elif i + 1 == total_edges and edge.verts[1] in loop[i - 1].verts:
|
||||
loop_vertices.append(edge.verts[1])
|
||||
elif edge.verts[0] in loop[i + 1].verts:
|
||||
loop_vertices.append(edge.verts[1])
|
||||
elif edge.verts[1] in loop[i + 1].verts:
|
||||
loop_vertices.append(edge.verts[0])
|
||||
|
||||
loop_bm = bmesh.new()
|
||||
for vert in loop_vertices:
|
||||
loop_bm.verts.new(vert.co)
|
||||
face = loop_bm.faces.new(loop_bm.verts)
|
||||
|
||||
loop_vertex_indices = [v.index for v in loop_vertices]
|
||||
face_area = face.calc_area()
|
||||
if face_area > max_area:
|
||||
max_area = face_area
|
||||
outer_loop = loop_vertex_indices
|
||||
inner_loops.append(loop_vertex_indices)
|
||||
loop_bm.free()
|
||||
|
||||
inner_loops.remove(outer_loop)
|
||||
|
||||
bm.to_mesh(mesh)
|
||||
mesh.update()
|
||||
bm.free()
|
||||
|
||||
return {"outer_curve": outer_loop, "inner_curves": inner_loops}
|
||||
|
||||
# An extrusion edge is an edge that shares a single vertex with a profile
|
||||
# face and is not on the plane of the face.
|
||||
def detect_extrusion_edge(self, bm, profile_face):
|
||||
|
||||
@@ -30,12 +30,13 @@ import blenderbim.core.style
|
||||
import blenderbim.core.root
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.bim.handler
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim import import_ifc
|
||||
from mathutils import Vector
|
||||
from ifcopenshell.api.geometry.data import Data
|
||||
from ifcopenshell.api.context.data import Data as ContextData
|
||||
from ifcopenshell.api.void.data import Data as VoidData
|
||||
from mathutils import Vector
|
||||
from blenderbim.bim import import_ifc
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.root.prop import get_contexts
|
||||
|
||||
|
||||
class Operator:
|
||||
@@ -54,7 +55,7 @@ class EditObjectPlacement(bpy.types.Operator, Operator):
|
||||
def _execute(self, context):
|
||||
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
|
||||
for obj in objs:
|
||||
core.edit_object_placement(tool.Ifc, tool.Surveyor, obj=obj)
|
||||
core.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
|
||||
|
||||
class AddRepresentation(bpy.types.Operator, Operator):
|
||||
@@ -67,7 +68,9 @@ class AddRepresentation(bpy.types.Operator, Operator):
|
||||
profile_set_usage: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
ifc_context = self.context_id or int(context.scene.BIMProperties.contexts or "0") or None
|
||||
ifc_context = self.context_id
|
||||
if not ifc_context and get_contexts(self, context):
|
||||
ifc_context = int(context.scene.BIMRootProperties.contexts or "0") or None
|
||||
if ifc_context:
|
||||
ifc_context = tool.Ifc.get().by_id(ifc_context)
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
@@ -81,16 +84,6 @@ class AddRepresentation(bpy.types.Operator, Operator):
|
||||
ifc_representation_class=self.ifc_representation_class,
|
||||
profile_set_usage=tool.Ifc.get().by_id(self.profile_set_usage) if self.profile_set_usage else None,
|
||||
)
|
||||
Data.load(tool.Ifc.get(), obj.BIMObjectProperties.ifc_definition_id)
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element.is_a("IfcTypeProduct"):
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
types = element.ObjectTypeOf
|
||||
else:
|
||||
types = element.Types
|
||||
if types:
|
||||
for element in types[0].RelatedObjects:
|
||||
Data.load(tool.Ifc.get(), element.id())
|
||||
|
||||
|
||||
class SwitchRepresentation(bpy.types.Operator, Operator):
|
||||
@@ -111,6 +104,7 @@ class SwitchRepresentation(bpy.types.Operator, Operator):
|
||||
should_reload=self.should_reload,
|
||||
enable_dynamic_voids=self.disable_opening_subtractions,
|
||||
is_global=self.should_switch_all_meshes,
|
||||
should_sync_changes_first=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -118,37 +112,15 @@ class RemoveRepresentation(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.remove_representation"
|
||||
bl_label = "Remove Representation"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
representation_id: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
representation = self.file.by_id(self.representation_id)
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
is_mapped_representation = representation.RepresentationType == "MappedRepresentation"
|
||||
if is_mapped_representation:
|
||||
mesh_name = "{}/{}".format(
|
||||
representation.ContextOfItems.id(), representation.Items[0].MappingSource.MappedRepresentation.id()
|
||||
)
|
||||
else:
|
||||
mesh_name = "{}/{}".format(representation.ContextOfItems.id(), representation.id())
|
||||
mesh = bpy.data.meshes.get(mesh_name)
|
||||
if mesh:
|
||||
if obj.data == mesh:
|
||||
# TODO we can do better than this
|
||||
void_mesh = bpy.data.meshes.get("Void")
|
||||
if not void_mesh:
|
||||
void_mesh = bpy.data.meshes.new("Void")
|
||||
obj.data = void_mesh
|
||||
if not is_mapped_representation:
|
||||
bpy.data.meshes.remove(mesh)
|
||||
product = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
ifcopenshell.api.run(
|
||||
"geometry.unassign_representation", self.file, **{"product": product, "representation": representation}
|
||||
core.remove_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=context.active_object,
|
||||
representation=tool.Ifc.get().by_id(self.representation_id),
|
||||
)
|
||||
ifcopenshell.api.run("geometry.remove_representation", self.file, **{"representation": representation})
|
||||
Data.load(self.file, product.id())
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class UpdateRepresentation(bpy.types.Operator):
|
||||
@@ -173,6 +145,11 @@ class UpdateRepresentation(bpy.types.Operator):
|
||||
self.file = IfcStore.get_file()
|
||||
|
||||
for obj in objs:
|
||||
# TODO: write unit tests to see how this bulk operation handles
|
||||
# contradictory ifc_representation_class values and when
|
||||
# ifc_representation_class is IfcTextLiteral
|
||||
if not obj.data:
|
||||
continue
|
||||
self.update_obj_mesh_representation(context, obj)
|
||||
IfcStore.edited_objs.discard(obj)
|
||||
return {"FINISHED"}
|
||||
@@ -180,11 +157,19 @@ class UpdateRepresentation(bpy.types.Operator):
|
||||
def update_obj_mesh_representation(self, context, obj):
|
||||
product = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
|
||||
if not product.is_a("IfcGridAxis"):
|
||||
tool.Geometry.clear_cache(product)
|
||||
|
||||
if product.is_a("IfcGridAxis"):
|
||||
ifcopenshell.api.run("grid.create_axis_curve", self.file, **{"axis_curve": obj, "grid_axis": product})
|
||||
return
|
||||
if product.is_a("IfcRelSpaceBoundary"):
|
||||
# TODO refactor
|
||||
settings = tool.Boundary.get_assign_connection_geometry_settings(obj)
|
||||
ifcopenshell.api.run("boundary.assign_connection_geometry", tool.Ifc.get(), **settings)
|
||||
return
|
||||
|
||||
core.edit_object_placement(tool.Ifc, tool.Surveyor, obj=obj)
|
||||
core.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
|
||||
old_representation = self.file.by_id(obj.data.BIMMeshProperties.ifc_definition_id)
|
||||
context_of_items = old_representation.ContextOfItems
|
||||
@@ -206,8 +191,9 @@ class UpdateRepresentation(bpy.types.Operator):
|
||||
"geometry": obj.data,
|
||||
"coordinate_offset": coordinate_offset,
|
||||
"total_items": max(1, len(obj.material_slots)),
|
||||
"should_force_faceted_brep": context.scene.BIMGeometryProperties.should_force_faceted_brep,
|
||||
"should_force_triangulation": context.scene.BIMGeometryProperties.should_force_triangulation,
|
||||
"should_force_faceted_brep": tool.Geometry.should_force_faceted_brep(),
|
||||
"should_force_triangulation": tool.Geometry.should_force_triangulation(),
|
||||
"should_generate_uvs": tool.Geometry.should_generate_uvs(obj),
|
||||
"ifc_representation_class": self.ifc_representation_class,
|
||||
}
|
||||
|
||||
@@ -216,6 +202,7 @@ class UpdateRepresentation(bpy.types.Operator):
|
||||
product, old_representation
|
||||
)
|
||||
representation_data["profile_set_usage"] = tool.Geometry.get_profile_set_usage(product)
|
||||
representation_data["text_literal"] = tool.Geometry.get_text_literal(old_representation)
|
||||
|
||||
new_representation = ifcopenshell.api.run("geometry.add_representation", self.file, **representation_data)
|
||||
|
||||
@@ -239,6 +226,7 @@ class UpdateRepresentation(bpy.types.Operator):
|
||||
"should_use_presentation_style_assignment": context.scene.BIMGeometryProperties.should_use_presentation_style_assignment,
|
||||
},
|
||||
)
|
||||
tool.Geometry.record_object_materials(obj)
|
||||
|
||||
# TODO: move this into a replace_representation usecase or something
|
||||
for inverse in self.file.get_inverse(old_representation):
|
||||
@@ -246,10 +234,10 @@ class UpdateRepresentation(bpy.types.Operator):
|
||||
|
||||
obj.data.BIMMeshProperties.ifc_definition_id = int(new_representation.id())
|
||||
obj.data.name = f"{old_representation.ContextOfItems.id()}/{new_representation.id()}"
|
||||
bpy.ops.bim.remove_representation(representation_id=old_representation.id(), obj=obj.name)
|
||||
core.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_representation)
|
||||
Data.load(self.file, obj.BIMObjectProperties.ifc_definition_id)
|
||||
if obj.data.BIMMeshProperties.ifc_parameters:
|
||||
bpy.ops.bim.get_representation_ifc_parameters()
|
||||
core.get_representation_ifc_parameters(tool.Geometry, obj=obj)
|
||||
|
||||
|
||||
class UpdateParametricRepresentation(bpy.types.Operator):
|
||||
@@ -276,35 +264,20 @@ class UpdateParametricRepresentation(bpy.types.Operator):
|
||||
should_reload=True,
|
||||
enable_dynamic_voids=False,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
if show_representation_parameters:
|
||||
bpy.ops.bim.get_representation_ifc_parameters()
|
||||
core.get_representation_ifc_parameters(tool.Geometry, obj=obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GetRepresentationIfcParameters(bpy.types.Operator):
|
||||
class GetRepresentationIfcParameters(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.get_representation_ifc_parameters"
|
||||
bl_label = "Get Representation IFC Parameters"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
obj = context.active_object
|
||||
props = obj.data.BIMMeshProperties
|
||||
elements = self.file.traverse(self.file.by_id(props.ifc_definition_id))
|
||||
props.ifc_parameters.clear()
|
||||
for element in elements:
|
||||
if element.is_a("IfcRepresentationItem") or element.is_a("IfcParameterizedProfileDef"):
|
||||
for i in range(0, len(element)):
|
||||
if element.attribute_type(i) == "DOUBLE":
|
||||
new = props.ifc_parameters.add()
|
||||
new.name = "{}/{}".format(element.is_a(), element.attribute_name(i))
|
||||
new.step_id = element.id()
|
||||
new.type = element.attribute_type(i)
|
||||
new.index = i
|
||||
if element[i]:
|
||||
new.value = element[i]
|
||||
return {"FINISHED"}
|
||||
def _execute(self, context):
|
||||
core.get_representation_ifc_parameters(tool.Geometry, obj=context.active_object)
|
||||
|
||||
|
||||
class CopyRepresentation(bpy.types.Operator, Operator):
|
||||
@@ -357,9 +330,35 @@ class CopyRepresentation(bpy.types.Operator, Operator):
|
||||
return r.MappedRepresentation
|
||||
|
||||
|
||||
class OverrideDelete(bpy.types.Operator):
|
||||
class OverrideDeleteTrait:
|
||||
def delete_ifc_object(self, obj):
|
||||
if obj.BIMObjectProperties.ifc_definition_id:
|
||||
element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
IfcStore.delete_element(element)
|
||||
if getattr(element, "FillsVoids", None):
|
||||
self.remove_filling(element)
|
||||
if element.is_a("IfcOpeningElement"):
|
||||
for rel in element.HasFillings:
|
||||
self.remove_filling(rel.RelatedBuildingElement)
|
||||
if element.VoidsElements:
|
||||
self.delete_opening_element(element)
|
||||
elif getattr(element, "HasOpenings", None):
|
||||
for rel in element.HasOpenings:
|
||||
self.delete_opening_element(rel.RelatedOpeningElement)
|
||||
|
||||
def delete_opening_element(self, element):
|
||||
obj = IfcStore.get_element(element.VoidsElements[0].RelatingBuildingElement.id())
|
||||
bpy.ops.bim.remove_opening(opening_id=element.id(), obj=obj.name)
|
||||
|
||||
def remove_filling(self, element):
|
||||
obj = IfcStore.get_element(element.id())
|
||||
bpy.ops.bim.remove_filling(obj=obj.name)
|
||||
|
||||
|
||||
class OverrideDelete(bpy.types.Operator, OverrideDeleteTrait):
|
||||
bl_idname = "object.delete"
|
||||
bl_label = "Delete"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
use_global: bpy.props.BoolProperty(default=False)
|
||||
confirm: bpy.props.BoolProperty(default=True)
|
||||
|
||||
@@ -373,41 +372,99 @@ class OverrideDelete(bpy.types.Operator):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
for obj in context.selected_objects:
|
||||
bpy.data.objects.remove(obj)
|
||||
# Required otherwise gizmos are still visible
|
||||
context.view_layer.objects.active = None
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_confirm(self, event)
|
||||
if self.confirm:
|
||||
return context.window_manager.invoke_confirm(self, event)
|
||||
self.confirm = True
|
||||
return self.execute(context)
|
||||
|
||||
def _execute(self, context):
|
||||
file = IfcStore.get_file()
|
||||
for obj in context.selected_objects:
|
||||
if obj.BIMObjectProperties.ifc_definition_id:
|
||||
element = file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
if element.FillsVoids:
|
||||
self.remove_filling(element)
|
||||
if element.is_a("IfcOpeningElement"):
|
||||
for rel in element.HasFillings:
|
||||
self.remove_filling(rel.RelatedBuildingElement)
|
||||
if element.VoidsElements:
|
||||
self.delete_opening_element(element)
|
||||
elif element.HasOpenings:
|
||||
for rel in element.HasOpenings:
|
||||
self.delete_opening_element(rel.RelatedOpeningElement)
|
||||
self.delete_ifc_object(obj)
|
||||
bpy.data.objects.remove(obj)
|
||||
# Required otherwise gizmos are still visible
|
||||
context.view_layer.objects.active = None
|
||||
return {"FINISHED"}
|
||||
|
||||
def delete_opening_element(self, element):
|
||||
obj = IfcStore.get_element(element.VoidsElements[0].RelatingBuildingElement.id())
|
||||
bpy.ops.bim.remove_opening(opening_id=element.id(), obj=obj.name)
|
||||
|
||||
def remove_filling(self, element):
|
||||
obj = IfcStore.get_element(element.id())
|
||||
bpy.ops.bim.remove_filling(obj=obj.name)
|
||||
class OverrideOutlinerDelete(bpy.types.Operator, OverrideDeleteTrait):
|
||||
bl_idname = "outliner.delete"
|
||||
bl_label = "Delete"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
hierarchy: bpy.props.BoolProperty(default=False)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return len(context.selected_ids) > 0
|
||||
|
||||
def execute(self, context):
|
||||
# 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
|
||||
# unintended IFC spatial modifications. To make life less confusing for
|
||||
# the user, Delete means Delete. End of story.
|
||||
# Deep magick from the dawn of time
|
||||
if IfcStore.get_file():
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
# https://blender.stackexchange.com/questions/203729/python-get-selected-objects-in-outliner
|
||||
objects_to_delete = set()
|
||||
collections_to_delete = set()
|
||||
for item in context.selected_ids:
|
||||
if item.bl_rna.identifier == "Collection":
|
||||
collection = bpy.data.collections.get(item.name)
|
||||
collection_data = self.get_collection_objects_and_children(collection)
|
||||
objects_to_delete |= collection_data["objects"]
|
||||
collections_to_delete |= collection_data["children"]
|
||||
collections_to_delete.add(collection)
|
||||
elif item.bl_rna.identifier == "Object":
|
||||
objects_to_delete.add(bpy.data.objects.get(item.name))
|
||||
for obj in objects_to_delete:
|
||||
bpy.data.objects.remove(obj)
|
||||
for collection in collections_to_delete:
|
||||
bpy.data.collections.remove(collection)
|
||||
return {"FINISHED"}
|
||||
|
||||
def _execute(self, context):
|
||||
objects_to_delete = set()
|
||||
collections_to_delete = set()
|
||||
for item in context.selected_ids:
|
||||
if item.bl_rna.identifier == "Collection":
|
||||
collection = bpy.data.collections.get(item.name)
|
||||
collection_data = self.get_collection_objects_and_children(collection)
|
||||
objects_to_delete |= collection_data["objects"]
|
||||
collections_to_delete |= collection_data["children"]
|
||||
collections_to_delete.add(collection)
|
||||
elif item.bl_rna.identifier == "Object":
|
||||
objects_to_delete.add(bpy.data.objects.get(item.name))
|
||||
for obj in objects_to_delete:
|
||||
# This is the only difference
|
||||
self.delete_ifc_object(obj)
|
||||
bpy.data.objects.remove(obj)
|
||||
for collection in collections_to_delete:
|
||||
bpy.data.collections.remove(collection)
|
||||
return {"FINISHED"}
|
||||
|
||||
def get_collection_objects_and_children(self, collection):
|
||||
objects = set()
|
||||
children = set()
|
||||
queue = [collection]
|
||||
while queue:
|
||||
collection = queue.pop()
|
||||
for obj in collection.objects:
|
||||
objects.add(obj)
|
||||
queue.extend(collection.children)
|
||||
children = children.union(collection.children)
|
||||
return {"objects": objects, "children": children}
|
||||
|
||||
|
||||
class OverrideDuplicateMove(bpy.types.Operator):
|
||||
bl_idname = "object.duplicate_move"
|
||||
bl_label = "Duplicate Objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
|
||||
@@ -29,6 +29,7 @@ class BIM_PT_representations(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "object"
|
||||
bl_parent_id = "BIM_PT_geometry_object"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -49,7 +50,7 @@ class BIM_PT_representations(Panel):
|
||||
layout.label(text="No representations found")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(context.scene.BIMProperties, "contexts", text="")
|
||||
row.prop(context.scene.BIMRootProperties, "contexts", text="")
|
||||
row.operator("bim.add_representation", icon="ADD", text="")
|
||||
|
||||
for representation in RepresentationsData.data["representations"]:
|
||||
|
||||
@@ -30,6 +30,7 @@ class BIM_PT_gis(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_geometry"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from . import prop
|
||||
from . import operator
|
||||
from . import ui
|
||||
|
||||
classes = (
|
||||
prop.BIMCityJsonProperties,
|
||||
operator.BIM_OT_cityjson2ifc,
|
||||
operator.BIM_OT_find_cityjson_lod,
|
||||
ui.BIM_PT_cityjson_converter,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.Scene.ifccityjson_properties = bpy.props.PointerProperty(type=prop.BIMCityJsonProperties)
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.Scene.ifccityjson_properties
|
||||
@@ -0,0 +1,69 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
|
||||
|
||||
class BIM_OT_cityjson2ifc(Operator):
|
||||
bl_idname = "bim.convert_cityjson2ifc"
|
||||
bl_label = "Convert CityJSON to IFC"
|
||||
bl_context = "scene"
|
||||
|
||||
def execute(self, context):
|
||||
from cjio import cityjson
|
||||
from ifccityjson.cityjson2ifc.cityjson2ifc import Cityjson2ifc
|
||||
|
||||
props = context.scene.ifccityjson_properties
|
||||
city_model = cityjson.load(props.input, transform=False)
|
||||
data = {
|
||||
"file_destination": props.output,
|
||||
"lod": props.lod,
|
||||
"split": props.split_lod,
|
||||
}
|
||||
if props.name != "":
|
||||
data["name_attribute"] = props.name
|
||||
|
||||
converter = Cityjson2ifc()
|
||||
converter.configuration(**data)
|
||||
converter.convert(city_model)
|
||||
if props.load_after_convert:
|
||||
bpy.ops.bim.load_project(filepath=props.output)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_find_cityjson_lod(Operator):
|
||||
bl_idname = "bim.find_cityjson_lod"
|
||||
bl_label = "Find LODs in CityJSON file"
|
||||
bl_context = "scene"
|
||||
|
||||
def execute(self, context):
|
||||
from cjio import cityjson
|
||||
|
||||
props = context.scene.ifccityjson_properties
|
||||
city_model = cityjson.load(props.input)
|
||||
lods = set()
|
||||
for obj_id, obj in city_model.get_cityobjects().items():
|
||||
for geometry in obj.geometry:
|
||||
lod = str(geometry.lod)
|
||||
if lod not in lods:
|
||||
lods.add(lod)
|
||||
props.lods.add()
|
||||
props.lods[-1].name = lod
|
||||
props.is_lod_found = True
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,36 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import StringProperty, BoolProperty, EnumProperty, CollectionProperty
|
||||
from blenderbim.bim.prop import StrProperty
|
||||
|
||||
|
||||
class BIMCityJsonProperties(PropertyGroup):
|
||||
def get_lods(self, context):
|
||||
return [(item.name, "LOD" + item.name, "Level of Detail " + item.name) for item in self.lods]
|
||||
|
||||
# TODO: instead of subtype it would be nice to have a helper operator that allows filtered file browsing
|
||||
input: StringProperty(name="CityJSON Input", default="", subtype="FILE_PATH")
|
||||
output: StringProperty(name="IFC Output", default="", subtype="FILE_PATH")
|
||||
name: StringProperty(name="Identifier", default="")
|
||||
split_lod: BoolProperty(name="Should Split LOD", default=True)
|
||||
lods: CollectionProperty(name="LODs", type=StrProperty)
|
||||
lod: EnumProperty(name="LOD", description="", items=get_lods, options={"ANIMATABLE"}, default=None)
|
||||
is_lod_found: BoolProperty(name="Is LOD Found", default=False)
|
||||
load_after_convert: BoolProperty(name="Load After Converting", default=True)
|
||||
@@ -0,0 +1,52 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from bpy.types import Panel
|
||||
|
||||
|
||||
class BIM_PT_cityjson_converter(Panel):
|
||||
bl_label = "IFC CityJSON"
|
||||
bl_idname = "BIM_PT_ifccityjson"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_collaboration"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
scene = context.scene
|
||||
props = scene.ifccityjson_properties
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "input")
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.find_cityjson_lod")
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "output")
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "name")
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "split_lod")
|
||||
row.prop(props, "load_after_convert")
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "lod")
|
||||
if not props.is_lod_found:
|
||||
row.enabled = False
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.convert_cityjson2ifc")
|
||||
@@ -28,6 +28,7 @@ class BIM_PT_groups(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_project_setup"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -74,6 +75,7 @@ class BIM_PT_object_groups(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "object"
|
||||
bl_parent_id = "BIM_PT_utilities_object"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
|
||||
@@ -28,6 +28,7 @@ class BIM_PT_layers(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_geometry_object"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
|
||||
@@ -21,6 +21,7 @@ from . import ui, prop, operator
|
||||
|
||||
classes = (
|
||||
operator.CalculateLCA,
|
||||
operator.ConnectOpenLCA,
|
||||
prop.BIMLCAProperties,
|
||||
ui.BIM_PT_lca,
|
||||
)
|
||||
|
||||