mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-07 16:31:37 +00:00
Compare commits
576 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 05a3004ef0 | |||
| fe5afaf172 | |||
| 0039f376ca | |||
| c307871d37 | |||
| 31c9f60bca | |||
| 93e7d92076 | |||
| 16f09655b8 | |||
| b6115a6d71 | |||
| f0b5e93737 | |||
| 9895302c9d | |||
| 8f5f346fa4 | |||
| 6adf5d957a | |||
| e6be79f2ad | |||
| e3228513c0 | |||
| 547506d626 | |||
| 3fe29fa842 | |||
| e18e2caea8 | |||
| 1cac12dc53 | |||
| 8d530eba68 | |||
| 4593e5aea6 | |||
| cba6161043 | |||
| 42c49af4e3 | |||
| 7bdbfee041 | |||
| f37bda2d7c | |||
| 0bf065c4bd | |||
| c58b84ddb5 | |||
| 81b5fc30c7 | |||
| fd423f21e2 | |||
| 4afed8f2f3 | |||
| 9254601924 | |||
| e9460a2c9a | |||
| f08f9ad310 | |||
| 69ddf52ff0 | |||
| 2fd02e281d | |||
| fdb283b7af | |||
| 013d7f6659 | |||
| 5b33c6018a | |||
| 50324a566f | |||
| 64b8842055 | |||
| 05be0f69bd | |||
| 3f823573cf | |||
| a3c5c0ce43 | |||
| 2e61532919 | |||
| c3107e37c0 | |||
| c85f9f02ba | |||
| f888a32fa4 | |||
| d35a2dcf5b | |||
| 244320dd49 | |||
| 0d7439c7a5 | |||
| bd92821b2e | |||
| 3744b81ccd | |||
| 9311f50206 | |||
| eec8c88e6f | |||
| dd53f9b494 | |||
| c6e3ac4155 | |||
| 4594f596c2 | |||
| 4f091af935 | |||
| 2f34f997dd | |||
| 1600e00295 | |||
| 7a55e837f7 | |||
| 595ee3fa47 | |||
| c84f8e55a6 | |||
| b63d9090a1 | |||
| 58c7976a5e | |||
| 384a74734e | |||
| b269a806eb | |||
| f43e666583 | |||
| 26b21ddcb3 | |||
| f1138d1aac | |||
| 51eef295c8 | |||
| 212da8b6d6 | |||
| f7163b8db8 | |||
| 9ee947b730 | |||
| 328fb8bc0e | |||
| 75fe548bb0 | |||
| 5830573cda | |||
| 775106d0a0 | |||
| 25b7003d2d | |||
| c80c1e9e15 | |||
| b5c062415d | |||
| a98a7ac856 | |||
| ee2b836b84 | |||
| e075c32a5a | |||
| a567af370b | |||
| 544b61b95a | |||
| 45c45d6862 | |||
| 6880e31a66 | |||
| e327932f4e | |||
| f2283e5895 | |||
| ea84f829af | |||
| 3231752fef | |||
| 963ba5fc30 | |||
| 8e1e0aec79 | |||
| 9c4ec34de3 | |||
| 4c2844292e | |||
| 35fe79dfb2 | |||
| 58a5f881e6 | |||
| 4d5a3ec633 | |||
| 1feb00678c | |||
| f10ebe0166 | |||
| 5f8fc1ba8c | |||
| f8f36cf2f0 | |||
| 2a2bfea870 | |||
| 8bdf8369b5 | |||
| 2bc230919e | |||
| 53fd1284b5 | |||
| 3c777a0b85 | |||
| fa3c57b016 | |||
| e658b00768 | |||
| 26b55216ca | |||
| d912012f76 | |||
| d868d8ceac | |||
| 8def5ec30b | |||
| ad03a0c8d9 | |||
| 09a43f1339 | |||
| c176b9d607 | |||
| 6cf5cc002e | |||
| 0abe6329d6 | |||
| 5109168533 | |||
| 0b4deee73f | |||
| 1f43f7f807 | |||
| 73a83c6d4e | |||
| 9c5b1a04ff | |||
| e67753eb4f | |||
| 778a839e5f | |||
| 102de32737 | |||
| cf3a554b15 | |||
| a8ff6ecf14 | |||
| 55be5cc289 | |||
| 9af00d4e72 | |||
| d8592a2a95 | |||
| 16ba10f957 | |||
| 0d1298aac1 | |||
| 210257de8e | |||
| 37ec2bf0e4 | |||
| 6e7c8ad640 | |||
| bfc4490c97 | |||
| d50e8060ac | |||
| a21939dc0d | |||
| 8845440edd | |||
| ba37754068 | |||
| ed48afc633 | |||
| 7262a50184 | |||
| 67b0ab6685 | |||
| 2941a4ad1a | |||
| b2e2f765ca | |||
| 2270185f8c | |||
| faef2df2a6 | |||
| bef7ff33f7 | |||
| 615ca24825 | |||
| 7e42b10159 | |||
| 04e07db0a3 | |||
| eed47c8c87 | |||
| 9ba3b6a61f | |||
| 6cfd15e819 | |||
| e55523ce3e | |||
| f36830d6dc | |||
| 2f87029434 | |||
| 9f78d9881e | |||
| 1d447e6e23 | |||
| 4c53c2ab1f | |||
| 86ebc968e5 | |||
| 1de67ffd9c | |||
| 1408beeac5 | |||
| 791a2e40f1 | |||
| 3f6a9a55cd | |||
| ec702e2ec1 | |||
| 4f6dbbe72b | |||
| 8ca344259f | |||
| e038f968f2 | |||
| 0e8810c1f5 | |||
| 10ecac33b8 | |||
| 069b630b16 | |||
| 8f6cd18288 | |||
| 7b7108b938 | |||
| ebd719783c | |||
| 46a31386da | |||
| c80c7c6d36 | |||
| ed7cf248ec | |||
| b40f630783 | |||
| f99015e123 | |||
| be76d4cb44 | |||
| 6949f9ceae | |||
| 4daf4693f3 | |||
| edd60a73ca | |||
| 6c04669543 | |||
| 4eae112087 | |||
| 5be076cb96 | |||
| ef67d866b6 | |||
| 609f468ce1 | |||
| 3852e3c63f | |||
| 5cb9e1c68d | |||
| 52fbe09dd4 | |||
| 4cff2f1fc1 | |||
| 6517cafd98 | |||
| 5114576457 | |||
| 9bafe07c36 | |||
| 6b85e9ec85 | |||
| d89957faab | |||
| ba8753d55e | |||
| 5df9c45795 | |||
| 5cc0aa71c7 | |||
| b06ddbcad4 | |||
| 18431dec31 | |||
| 7e9985758f | |||
| ece91a94db | |||
| dccfb634d9 | |||
| ffc6c6a7ad | |||
| dbf4a2a5ed | |||
| f6640d3904 | |||
| 63aada25dd | |||
| 6596f5bc4b | |||
| acdc40fb41 | |||
| 9be0a60c0a | |||
| e9a3d06352 | |||
| dfd30fbcaa | |||
| a941eb3f90 | |||
| 226736eb41 | |||
| f5a1c4aae3 | |||
| a7a4198fc1 | |||
| 6e0e19d690 | |||
| 72bbf244f2 | |||
| 41c1c6e8fa | |||
| a32735ac6a | |||
| 031540e982 | |||
| db1d8d938f | |||
| aa478ab359 | |||
| 19c103c627 | |||
| 7af672712f | |||
| 6bd8636c92 | |||
| a9f7d4ab3b | |||
| 977d814d39 | |||
| 5b95bb37e6 | |||
| 85b9906e20 | |||
| 64f51d733e | |||
| ed0b1c5009 | |||
| c1bc36ab86 | |||
| 9cfcaeb587 | |||
| d9e48467cd | |||
| f08dba6483 | |||
| 1e97d2c5fa | |||
| bf54132805 | |||
| 570513b60b | |||
| e1ba98c5f8 | |||
| 8ed9c32404 | |||
| 5b9ef23bdd | |||
| f3f7939c8b | |||
| 469b4f3502 | |||
| c4b57eb903 | |||
| 3886c79a2c | |||
| 566fe32ab2 | |||
| 7fb6adc025 | |||
| c03cf4ce58 | |||
| 41188e26e1 | |||
| 823f27cd6a | |||
| 4260313a99 | |||
| 1259607a9f | |||
| 9f1ffe6381 | |||
| ba1b6b5b10 | |||
| 2318132ea2 | |||
| b6b553e726 | |||
| 076f39e2d6 | |||
| abfe8ec315 | |||
| 9d7fe179cf | |||
| 8ffb5c56e4 | |||
| 4109b19c3a | |||
| 7503530741 | |||
| e78127d54f | |||
| 5fa4f2e79b | |||
| e5027e0be8 | |||
| af2dd1d7c6 | |||
| 5d9528d791 | |||
| 47d11daa15 | |||
| 01ec750814 | |||
| 8c9a657a8c | |||
| 48acfad8bb | |||
| 01b63d87d0 | |||
| 36744d391e | |||
| f0e5430daa | |||
| 197281eb5a | |||
| 7b370e6142 | |||
| 71a7d0c086 | |||
| b86e735530 | |||
| 46104f5bf5 | |||
| d68068a84b | |||
| fc7dd3b234 | |||
| 85fc97eb84 | |||
| 9076deddbc | |||
| eb0d3c38b2 | |||
| 590288496e | |||
| 269e137ea2 | |||
| ed722c6f13 | |||
| 2b9e3d2a2e | |||
| 7dee888ecf | |||
| a67927ae67 | |||
| 1271e4b7e3 | |||
| a44d0e713a | |||
| a358cff1b7 | |||
| 188ca3e402 | |||
| b5260d3005 | |||
| 79940641b2 | |||
| 7458040949 | |||
| 14eb543662 | |||
| e786687882 | |||
| 37e131193b | |||
| dfb00f27bb | |||
| 53dea65d8b | |||
| 2c16839de7 | |||
| c4f2d556f1 | |||
| 74900193ee | |||
| 2987d38a31 | |||
| 08b50bb9ea | |||
| 1257eb0872 | |||
| 1c8ef176b1 | |||
| e1e51cbf26 | |||
| 4c97cc8a41 | |||
| 7c6e124fe9 | |||
| 3fc312752b | |||
| d0cb5456b3 | |||
| 86072079de | |||
| 9440096106 | |||
| 649a913bbc | |||
| 53a3afaded | |||
| 33d3984990 | |||
| 5f86c6d1c2 | |||
| 10d5378ee3 | |||
| 7f27785927 | |||
| fed063a0c6 | |||
| 855062e36b | |||
| 7b74b6c68f | |||
| a01278aa8d | |||
| 99c49736fa | |||
| c1efdbb8ea | |||
| e240ae2669 | |||
| 945099b817 | |||
| f4b0fe6560 | |||
| efc6842a9d | |||
| 43931e951e | |||
| 134a480b03 | |||
| 0eb32ed7b7 | |||
| 1855ac421c | |||
| 7a69d6f20e | |||
| 2609a8293c | |||
| e68c47a897 | |||
| 06fa2b05d6 | |||
| db52e70121 | |||
| c81fe97cc2 | |||
| b9f4503206 | |||
| bd4dc501a2 | |||
| 8d1255f0bc | |||
| cc7abe6a8d | |||
| edfcc3f74b | |||
| b1c87b1eb4 | |||
| d37bda6871 | |||
| bb0d156bbf | |||
| b89ffcb6d0 | |||
| 323967b9fd | |||
| 4dd10b16e6 | |||
| 15a0d43180 | |||
| 5a1f7000ec | |||
| 277aaca6c5 | |||
| 9187f2d2b5 | |||
| 62d53ba62d | |||
| 9055f14bb9 | |||
| 0bd608b6cb | |||
| 497542d7a0 | |||
| 26b2a7d584 | |||
| 2d0bd9782a | |||
| 746a61a199 | |||
| 24df52e2f7 | |||
| 257f0b11f5 | |||
| b4f2039367 | |||
| cfb7d026d1 | |||
| 34acd6de35 | |||
| 0f3de599a6 | |||
| 0104a2cc9f | |||
| 81f4b08b6d | |||
| 10776b7de8 | |||
| b0efb648f2 | |||
| a20a8a68a6 | |||
| f8ec8402fe | |||
| aaf008f007 | |||
| f63d6b80c5 | |||
| 2f6ae1745f | |||
| 5f4098e5d4 | |||
| b49cc6c5e9 | |||
| dcc681ac74 | |||
| 240c78c12a | |||
| 6de409031e | |||
| ea274588b0 | |||
| e46e1512fa | |||
| 70182d943d | |||
| 0bd4451ce9 | |||
| 899ada0019 | |||
| f6ae711f6c | |||
| 1d815cdb67 | |||
| ce6339869d | |||
| 5eb9cb2ee4 | |||
| 6eda4e389c | |||
| 0783f8082e | |||
| 3854afb34d | |||
| 0b2321f478 | |||
| 5510fb1e3c | |||
| 7331f7361f | |||
| e06684e274 | |||
| 57d6e9b544 | |||
| 5fa0aa03e4 | |||
| 1d2ac9426d | |||
| e79a85a4fd | |||
| 076a6c152d | |||
| 76277c35d2 | |||
| a81b4a7dbb | |||
| ee8a3927b4 | |||
| 365393aa07 | |||
| 9289e1d0cc | |||
| c30b5cbc08 | |||
| 9736345d79 | |||
| 5599e52184 | |||
| ce26af5a7e | |||
| 973c9e2ec9 | |||
| 29a890662e | |||
| b42c3ba8c2 | |||
| a7509dbe29 | |||
| 23158b1b7e | |||
| 8c16f394c1 | |||
| 12f8a1f769 | |||
| 7ffd2742d6 | |||
| 6429fe34ed | |||
| d38f56e82d | |||
| c549dea943 | |||
| 894c05e138 | |||
| 92c2fc0b7f | |||
| ee2fa739bb | |||
| f7efbe5924 | |||
| 6e20ee9fea | |||
| e6d3396630 | |||
| cf1647a451 | |||
| 69535c53e0 | |||
| fed8c21625 | |||
| 0f92c2d26b | |||
| c11060b46a | |||
| 9f0d408e79 | |||
| d37baa4e0c | |||
| ae4f125371 | |||
| 25d8fff5fd | |||
| 331f491d50 | |||
| b19ece18c6 | |||
| abddc739fe | |||
| 174f909137 | |||
| a826115803 | |||
| 79abbad796 | |||
| 04a20b5c80 | |||
| f4d101788d | |||
| 4bb32b252d | |||
| 8b92b28dfc | |||
| 43cc7a0304 | |||
| 67bac441ad | |||
| 6c511f6b1b | |||
| deb0a46acb | |||
| ee0599e263 | |||
| a8efd53d6b | |||
| 02f0c92a41 | |||
| f11e09c7d8 | |||
| bb1e00e474 | |||
| 4efb79e472 | |||
| 527e0f3dbf | |||
| 8ea3142451 | |||
| a68cb74699 | |||
| 17642ca4e9 | |||
| 83a351b1c7 | |||
| 37baa7a57b | |||
| 7ca2c043b5 | |||
| 1e3ddce5c2 | |||
| c79020aeab | |||
| 645298e9d6 | |||
| ae76d3253d | |||
| 693f7c5397 | |||
| a8960957c0 | |||
| c609aea1de | |||
| 247bff4075 | |||
| 5a3a032fe6 | |||
| 5d8c2f2ec2 | |||
| 93ee92c401 | |||
| 8c2d71b306 | |||
| 8f1446bfa4 | |||
| 32608ff194 | |||
| b0fbea60ae | |||
| 8f261be338 | |||
| 42e184c936 | |||
| e0a93626a5 | |||
| aaa4be1a94 | |||
| 9f453d54d8 | |||
| b226a79ee8 | |||
| 774a770c48 | |||
| aff90e89f7 | |||
| aad4dad9a7 | |||
| 6ac6f13018 | |||
| d23cfcb2cf | |||
| 71668fbaf1 | |||
| 81b7f3dc30 | |||
| a0fa861b92 | |||
| a63c8e1d70 | |||
| 3e1d1bf7fa | |||
| 20e1fc2618 | |||
| 1014487d82 | |||
| 3832077d0f | |||
| 768bf17b05 | |||
| fb4ad5290f | |||
| f20bdd0ab6 | |||
| 5fe87bf5aa | |||
| c23a20df84 | |||
| b87cd33b12 | |||
| 8acd5321b0 | |||
| 1165ef3f33 | |||
| 2b7108a1d8 | |||
| 33d9e050ec | |||
| b88fd964bc | |||
| 0b32c89c26 | |||
| 9db1783062 | |||
| 2d54a48f8e | |||
| 39a30f2577 | |||
| 1e946fd98e | |||
| 79046e2970 | |||
| f6c6cc9a1b | |||
| af03953ef7 | |||
| cdcbc2ad3b | |||
| ba2456ad36 | |||
| 299b5bcf6b | |||
| cb95539cb0 | |||
| 7b78ebc0f3 | |||
| f39e6ccb54 | |||
| 3e1710c6fc | |||
| fac5104bd8 | |||
| 51c4059771 | |||
| f24de9d384 | |||
| 1e08a71c5a | |||
| 24b4be3ea1 | |||
| d2420c2e45 | |||
| d093d80698 | |||
| aef972a518 | |||
| 06c6fa5dff | |||
| f6087613f0 | |||
| 45e36590eb | |||
| 7f37acfd38 | |||
| de2329c352 | |||
| 09abf0c7ab | |||
| 77c2e626b3 | |||
| 9ea6a1fc75 | |||
| 5b7964f04c | |||
| 57d7d83ebb | |||
| 2f4c3de92d | |||
| 136270b566 | |||
| 557a5f03d2 | |||
| 4e1ff8ea6a | |||
| 08680d8726 | |||
| 53cf1ee0a1 | |||
| ddf7d1b0b9 | |||
| eeac0a2e30 | |||
| 1f83b4f7a0 | |||
| ada8cc90ef | |||
| 21562d7e7d | |||
| 77f9dbf4f0 | |||
| 875dd37579 | |||
| e1c49ad317 | |||
| 8e9d4be55d | |||
| 65d65972ac | |||
| b2d66bbb89 | |||
| d8a250f153 | |||
| 4c0a7e1ca9 | |||
| 11c6b55f7c | |||
| 1d2214efd9 | |||
| cb027c80f9 | |||
| 44c7c6d14e | |||
| 3e7f244100 | |||
| da1c92c42b | |||
| 17682498da |
@@ -7,14 +7,24 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Replace this text describing what problem occurred and what you expected to happen instead.
|
||||
**Error Description and Steps to Reproduce**
|
||||
|
||||
<!--
|
||||
Describe what problem occurred and what you expected to happen instead.
|
||||
|
||||
1. To reproduce this, open file '...'
|
||||
2. Click on '....'
|
||||
3. See error
|
||||
-->
|
||||
|
||||
**Attachments**
|
||||
|
||||
<!--
|
||||
If applicable, add screenshots to help explain your problem. Please also drag-drop any files necessary to show the error (rename the file extension from .ifc to .txt to upload). Private files can be uploaded to https://ifcopenshell.org/upload.html - only viewed by core developers and will be deleted afterwards.
|
||||
-->
|
||||
|
||||
**Debug information**
|
||||
|
||||
<!--
|
||||
If this is in Bonsai, paste the output from the Copy Debug Information option in Bonsai. It can be found under Quality and Coordination -> Quality Control -> Debug. If this is a general software issue, if relevant include details about IfcOpenShell version, operating system, Python version, etc.
|
||||
-->
|
||||
|
||||
@@ -7,4 +7,9 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Replace this text and describe a feature you'd like us to add. If it's not obvious, explain why this feature is awesome. Note that feature requests must be specific and measurable.
|
||||
|
||||
**Feature Description**
|
||||
|
||||
<!--
|
||||
Describe a feature you'd like us to add. If it's not obvious, explain why this feature is awesome. Note that feature requests must be specific and measurable.
|
||||
-->
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
name: Dispatch Build IfcOpenShell
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
trigger-workflows:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
workflow:
|
||||
- 'Build IfcOpenShell Linux'
|
||||
- 'Build IfcOpenShell Linux ARM'
|
||||
- 'Build IfcOpenShell OSX'
|
||||
- 'Build IfcOpenShell WASM / Pyodide'
|
||||
- 'Build IfcOpenShell Windows'
|
||||
steps:
|
||||
- name: Trigger binary build workflows
|
||||
uses: benc-uk/workflow-dispatch@v1
|
||||
with:
|
||||
workflow: ${{ matrix.workflow }}
|
||||
@@ -128,10 +128,8 @@ jobs:
|
||||
#
|
||||
# To force the link and overwrite all conflicting files:
|
||||
# brew link --overwrite python@3.13
|
||||
brew link --overwrite python@3.12
|
||||
brew link --overwrite python@3.13
|
||||
# https://github.com/rust-lang/rustup/pull/3989/files
|
||||
brew install --overwrite awscli
|
||||
brew install --overwrite awscli | true
|
||||
|
||||
- name: Upload .zip archives to S3
|
||||
run: |
|
||||
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build_ifcopenshell:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build_ifcopenshell:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
container: rockylinux:8
|
||||
|
||||
steps:
|
||||
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
# Ensure Bonsai and ifcsverchok enable/disable works before uploading to extensions repo.
|
||||
|
||||
# Download Blender.
|
||||
wget -q -O blender.tar.xz https://ftp.nluug.nl/pub/graphics/blender/release/Blender4.3/blender-4.3.0-linux-x64.tar.xz
|
||||
wget -q -O blender.tar.xz https://download.blender.org/release/Blender4.3/blender-4.3.2-linux-x64.tar.xz
|
||||
tar -xf blender.tar.xz
|
||||
|
||||
# Setup Blender.
|
||||
|
||||
@@ -24,10 +24,6 @@ jobs:
|
||||
name: "Windows 64bit",
|
||||
short_name: win64,
|
||||
}
|
||||
- {
|
||||
name: "Windows 32bit",
|
||||
short_name: win32,
|
||||
}
|
||||
- {
|
||||
name: "Linux 64bit",
|
||||
short_name: linux64
|
||||
|
||||
@@ -25,10 +25,6 @@ jobs:
|
||||
name: "Windows 64bit",
|
||||
short_name: win64,
|
||||
}
|
||||
- {
|
||||
name: "Windows 32bit",
|
||||
short_name: win32,
|
||||
}
|
||||
- {
|
||||
name: "Linux 64bit",
|
||||
short_name: linux64
|
||||
|
||||
@@ -48,7 +48,6 @@ jobs:
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely
|
||||
pip install mathutils
|
||||
pip install src/bcf --no-deps
|
||||
pip install https://github.com/Andrej730/aud/archive/refs/heads/master-reduced-size.zip
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ on:
|
||||
paths:
|
||||
- 'src/pyodide/**'
|
||||
- '.github/workflows/publish-pyodide-demo-app.yml'
|
||||
branches:
|
||||
- v0.8.0
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
|
||||
+1
-1
@@ -350,7 +350,7 @@ def git_clone_or_pull_repository(clone_url, target_dir, revision=None):
|
||||
run([git, "clone", "--recursive", clone_url, target_dir])
|
||||
else:
|
||||
logger.info(f"directory '{target_dir}' already cloned. Pulling latest changes.")
|
||||
run([git, "-C", target_dir, "fetch", "--all", "--tags"])
|
||||
run([git, "-C", target_dir, "fetch", "--all", "--tags", "--force"])
|
||||
|
||||
# detect whether we are on a branch and pull
|
||||
if run([git, "rev-parse", "--abbrev-ref", "HEAD"], cwd=target_dir) != "HEAD":
|
||||
|
||||
+3
-1
@@ -46,6 +46,7 @@ VERSION_MINOR:=$(shell cat '../../VERSION' | cut -d '.' -f 2)
|
||||
VERSION_PATCH:=$(shell cat '../../VERSION' | cut -d '.' -f 3)
|
||||
VERSION_DATE:=$(shell date '+%y%m%d')
|
||||
LAST_COMMIT_HASH:=$(shell git rev-parse HEAD)
|
||||
LAST_COMMIT_DATE:=$(shell git show -s --format=%cI)
|
||||
PYVERSION:=py310
|
||||
PYPI_IMP:=cp
|
||||
|
||||
@@ -85,7 +86,7 @@ BLENDER_PLATFORM:=windows-x64
|
||||
endif
|
||||
|
||||
# Current build commit hash.
|
||||
OLD:=463289e
|
||||
OLD:=c49ca69
|
||||
.PHONY: bump
|
||||
bump:
|
||||
ifndef NEW
|
||||
@@ -254,6 +255,7 @@ ifeq ($(IS_STABLE), TRUE)
|
||||
else
|
||||
$(SED) "s/0.0.0/$(VERSION)-alpha$(VERSION_DATE)/" build/bonsai/blender_manifest.toml
|
||||
$(SED) "s/8888888/$(LAST_COMMIT_HASH)/" build/bonsai/__init__.py
|
||||
$(SED) "s/9999999/$(LAST_COMMIT_DATE)/" build/bonsai/__init__.py
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' build/pyproject.toml
|
||||
endif
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ from typing import Union, Any, Generator
|
||||
|
||||
|
||||
last_commit_hash = "8888888"
|
||||
last_commit_date = "9999999"
|
||||
|
||||
|
||||
def get_last_commit_hash() -> Union[str, None]:
|
||||
@@ -51,6 +52,12 @@ def get_last_commit_hash() -> Union[str, None]:
|
||||
return last_commit_hash[:7]
|
||||
|
||||
|
||||
def get_last_commit_date() -> Union[str, None]:
|
||||
if last_commit_date == str(9_999999):
|
||||
return None
|
||||
return last_commit_date
|
||||
|
||||
|
||||
# Accessed from bonsai extension:
|
||||
bbim_semver: dict[str, Any] = {}
|
||||
|
||||
@@ -96,6 +103,7 @@ def get_debug_info():
|
||||
"blender_version": bpy.app.version_string,
|
||||
"bonsai_version": bbim_version,
|
||||
"bonsai_commit_hash": get_last_commit_hash(),
|
||||
"bonsai_commit_date": get_last_commit_date(),
|
||||
"last_actions": last_actions,
|
||||
"last_error": last_error,
|
||||
}
|
||||
@@ -218,6 +226,7 @@ if IN_BLENDER:
|
||||
path = Path(__file__).resolve().parent
|
||||
repo = git.Repo(str(path), search_parent_directories=True)
|
||||
last_commit_hash = repo.head.object.hexsha
|
||||
last_commit_date = repo.head.object.committed_datetime.isoformat()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@ modules = {
|
||||
"covering": None,
|
||||
"web": None,
|
||||
"light": None,
|
||||
"alignment": 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,
|
||||
@@ -115,10 +116,8 @@ classes = [
|
||||
operator.ReloadIfcFile,
|
||||
operator.RemoveIfcFile,
|
||||
operator.RevertClippingPlaneCut,
|
||||
operator.SelectDataDir,
|
||||
operator.SelectCacheDir,
|
||||
operator.SelectDir,
|
||||
operator.SelectIfcFile,
|
||||
operator.SelectSchemaDir,
|
||||
operator.SelectURIAttribute,
|
||||
operator.SetTab,
|
||||
operator.SwitchTab,
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 420 B |
Binary file not shown.
|
After Width: | Height: | Size: 415 B |
Binary file not shown.
|
After Width: | Height: | Size: 519 B |
Binary file not shown.
|
After Width: | Height: | Size: 513 B |
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1');
|
||||
FILE_NAME('/dev/null','2024-03-03T16:03:58+11:00',(),(),'IfcOpenShell v0.7.0-eaa2aa05a','IfcOpenShell v0.7.0-eaa2aa05a','Nobody');
|
||||
FILE_NAME('/dev/null','2025-03-27T17:30:27+05:00',(''),(''),'IfcOpenShell 0.0.0','IfcOpenShell 0.0.0','Nobody');
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
@@ -29,7 +29,7 @@ DATA;
|
||||
#22=IFCMATERIALLAYERSET((#24),$,$);
|
||||
#23=IFCRELASSOCIATESMATERIAL('0khP_Smdj12f2JrOTRbRKj',$,$,$,(#21),#22);
|
||||
#24=IFCMATERIALLAYER(#20,0.05,$,$,$,$,$);
|
||||
#25=IFCRELDECLARES('31JriXdev6BP2sC9lcxdC1',$,$,$,#2,(#26,#48,#72,#59,#92,#30,#96,#63,#21,#54,#87,#210,#153,#34,#67,#38,#82,#77,#42));
|
||||
#25=IFCRELDECLARES('31JriXdev6BP2sC9lcxdC1',$,$,$,#2,(#59,#92,#38,#82,#30,#96,#77,#63,#42,#54,#87,#153,#34,#21,#67,#210,#26,#48,#72));
|
||||
#26=IFCWALLTYPE('3R5onkTtX1IxdCMPoThRaw',$,'WAL100',$,$,$,$,$,$,.NOTDEFINED.);
|
||||
#27=IFCMATERIALLAYERSET((#29),$,$);
|
||||
#28=IFCRELASSOCIATESMATERIAL('0$mXRPCT16nP6Q70XFNvZv',$,$,$,(#26),#27);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -28,11 +28,13 @@ DATA;
|
||||
#21=IFCSIMPLEPROPERTYTEMPLATE('1UDakJ5_f7kBhggNSW4$h5',$,'SymbolsPath','Default symbols SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#22=IFCSIMPLEPROPERTYTEMPLATE('0d53LEtgLDQxnv__NfgH7i',$,'PatternsPath','Default patterns SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#23=IFCSIMPLEPROPERTYTEMPLATE('26qFNMv7nCHgU6Jd7Anga5',$,'ShadingStylesPath','Default shading styles',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#24=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/DIMENSION,IfcTypeProduct',(#25,#26,#27,#28));
|
||||
#24=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/DIMENSION,IfcAnnotation/RADIUS,IfcAnnotation/DIAMETER,IfcTypeProduct',(#25,#26,#27,#28,#30));
|
||||
#25=IFCSIMPLEPROPERTYTEMPLATE('1rL2AbQsXD8RbpoWH5pYOV',$,'ShowDescriptionOnly','Hide the measurement values and show only annotation description',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#26=IFCSIMPLEPROPERTYTEMPLATE('0SVyOfB0rC2xNfdRYf3XvY',$,'SuppressZeroInches','Suppress 0 inch values in dimension annotation text (for example: 12'' - 0" -> 12'')',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#27=IFCSIMPLEPROPERTYTEMPLATE('2bUmj458PBqPAtUoI3MXsb',$,'TextPrefix','Text to add before annotation measurement value',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#28=IFCSIMPLEPROPERTYTEMPLATE('0bnzttUb9BPuN597uNTXOE',$,'TextSuffix','Text to add after annotation measurement value',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#29=IFCSIMPLEPROPERTYTEMPLATE('2pJmUDpB50VBdCOib1zcJJ',$,'Newline_At','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
|
||||
#30=IFCSIMPLEPROPERTYTEMPLATE('2TJn72t_v2cvBUG916Dpev',$,'CustomUnit','Dimension''s custom unit',.P_ENUMERATEDVALUE.,'IfcText',$,#31,$,$,$,.READWRITE.);
|
||||
#31=IFCPROPERTYENUMERATION('CustomUnit',(IFCTEXT('Feet and Inches - Fractional'),IFCTEXT('Feet - Decimal'),IFCTEXT('Inches - Fractional'),IFCTEXT('Inches - Decimal'),IFCTEXT('Meters'),IFCTEXT('Decimeters'),IFCTEXT('Centimeters'),IFCTEXT('Millimeters')),$);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
../../libraries/IFC2X3 Demo Library.ifc
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
../../libraries/IFC4 Demo Library.ifc
|
||||
@@ -0,0 +1 @@
|
||||
../../libraries/IFC4X3 Demo Library.ifc
|
||||
@@ -132,11 +132,12 @@ class IfcExporter:
|
||||
bpy.ops.bim.update_representation(obj=obj.name)
|
||||
|
||||
def has_changed_materials(self, obj: bpy.types.Object) -> bool:
|
||||
checksum = obj.data.BIMMeshProperties.material_checksum
|
||||
mprops = tool.Geometry.get_mesh_props(obj.data)
|
||||
checksum = mprops.material_checksum
|
||||
return checksum != tool.Geometry.get_material_checksum(obj)
|
||||
|
||||
def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
|
||||
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
element = self.file.by_id(tool.Blender.get_object_bim_props(obj).ifc_definition_id)
|
||||
if tool.Geometry.is_scaled(obj):
|
||||
bpy.ops.bim.update_representation(obj=obj.name)
|
||||
# update_representation might not apply scale if the object has openings
|
||||
|
||||
@@ -24,18 +24,15 @@ import ifcopenshell.util.unit
|
||||
import ifcopenshell.api.owner.settings
|
||||
import bonsai.bim
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.owner as core_owner
|
||||
from bpy.app.handlers import persistent
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.module.owner.prop import get_user_person, get_user_organisation
|
||||
from bonsai.bim.module.model.data import AuthoringData
|
||||
from bonsai.bim.module.model.workspace import LIST_OF_TOOLS, TOOLS_TO_CLASSES_MAP
|
||||
from bonsai.bim.module.aggregate.decorator import AggregateDecorator
|
||||
from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator
|
||||
from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator
|
||||
from bonsai.bim.module.nest.decorator import NestDecorator
|
||||
from mathutils import Vector
|
||||
from math import cos, degrees
|
||||
from math import cos
|
||||
from typing import Union, Callable
|
||||
|
||||
|
||||
@@ -56,23 +53,24 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -
|
||||
return
|
||||
|
||||
if isinstance(obj, bpy.types.Material):
|
||||
props = obj.BIMStyleProperties
|
||||
props = tool.Style.get_material_style_props(obj)
|
||||
if ifc_definition_id := props.ifc_definition_id:
|
||||
if props.is_renaming:
|
||||
props.is_renmaing = False
|
||||
props.is_renaming = False
|
||||
return
|
||||
IfcStore.get_file().by_id(ifc_definition_id).Name = obj.name
|
||||
tool.Ifc.get().by_id(ifc_definition_id).Name = obj.name
|
||||
refresh_ui_data()
|
||||
return
|
||||
|
||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
if not props.ifc_definition_id:
|
||||
return
|
||||
|
||||
if obj.BIMObjectProperties.is_renaming:
|
||||
obj.BIMObjectProperties.is_renaming = False
|
||||
if props.is_renaming:
|
||||
props.is_renaming = False
|
||||
return
|
||||
|
||||
element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
element = tool.Ifc.get().by_id(props.ifc_definition_id)
|
||||
if "/" in obj.name:
|
||||
object_name = obj.name
|
||||
element_name = obj.name.split("/", 1)[1]
|
||||
@@ -88,8 +86,8 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -
|
||||
if not element.is_a("IfcRoot"):
|
||||
return
|
||||
element.Name = element_name
|
||||
if obj.BIMObjectProperties.collection:
|
||||
obj.BIMObjectProperties.collection.name = object_name
|
||||
if props.collection:
|
||||
props.collection.name = object_name
|
||||
refresh_ui_data()
|
||||
|
||||
|
||||
@@ -108,29 +106,44 @@ def update_bim_tool_props():
|
||||
return
|
||||
mode = bpy.context.mode
|
||||
current_tool = bpy.context.workspace.tools.from_space_view3d_mode(mode)
|
||||
if not current_tool or current_tool.idname not in LIST_OF_TOOLS:
|
||||
if not current_tool or current_tool.idname not in tool.Blender.get_list_of_tools():
|
||||
return
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return
|
||||
|
||||
props = tool.Model.get_model_props()
|
||||
aprops = tool.Drawing.get_annotation_props()
|
||||
TOOLS_TO_CLASSES_MAP = tool.Blender.get_tools_to_classes_map()
|
||||
is_annotation_tool = current_tool.idname == "bim.annotation_tool"
|
||||
if element.is_a("IfcTypeProduct") or element.is_a("IfcProduct"):
|
||||
element_type = ifcopenshell.util.element.get_type(element)
|
||||
if element_type:
|
||||
is_bim_tool = current_tool.idname == "bim.bim_tool"
|
||||
|
||||
if is_annotation_tool and (object_type := tool.Drawing.get_annotation_type_object_type(element_type)):
|
||||
aprops.object_type = object_type
|
||||
aprops.relating_type_id = str(element_type.id())
|
||||
return
|
||||
|
||||
if is_bim_tool:
|
||||
props.ifc_class = element_type.is_a()
|
||||
|
||||
if is_bim_tool or TOOLS_TO_CLASSES_MAP.get(current_tool.idname) == element_type.is_a():
|
||||
props.relating_type_id = str(element_type.id())
|
||||
|
||||
if is_annotation_tool:
|
||||
return
|
||||
|
||||
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if not representation:
|
||||
return
|
||||
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
if element.is_a("IfcElementType") or element.is_a("IfcElement"):
|
||||
element_type = ifcopenshell.util.element.get_type(element)
|
||||
if element_type:
|
||||
is_bim_tool = current_tool.idname == "bim.bim_tool"
|
||||
if is_bim_tool:
|
||||
props.ifc_class = element_type.is_a()
|
||||
if is_bim_tool or TOOLS_TO_CLASSES_MAP.get(current_tool.idname) == element_type.is_a():
|
||||
props.relating_type_id = str(element_type.id())
|
||||
extrusion = tool.Model.get_extrusion(representation)
|
||||
if not extrusion:
|
||||
return
|
||||
|
||||
def get_x_angle(extrusion):
|
||||
def get_x_angle(extrusion: ifcopenshell.entity_instance) -> float:
|
||||
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
|
||||
x_angle = Vector((0, 1)).angle_signed(Vector((y, z)))
|
||||
return x_angle
|
||||
@@ -207,35 +220,38 @@ def refresh_ui_data():
|
||||
if isinstance(tool.Ifc.get(), ifcopenshell.sqlite):
|
||||
tool.Ifc.get().clear_cache()
|
||||
|
||||
bpy.context.scene.DocProperties.should_draw_decorations = bpy.context.scene.DocProperties.should_draw_decorations
|
||||
props = tool.Drawing.get_document_props()
|
||||
props.should_draw_decorations = props.should_draw_decorations
|
||||
if bpy.context.scene.WebProperties.is_connected:
|
||||
tool.Web.send_webui_data()
|
||||
|
||||
|
||||
@persistent
|
||||
def loadIfcStore(scene):
|
||||
def loadIfcStore(scene: bpy.types.Scene) -> None:
|
||||
IfcStore.purge()
|
||||
refresh_ui_data()
|
||||
if not IfcStore.get_file():
|
||||
if not tool.Ifc.get():
|
||||
return
|
||||
IfcStore.get_schema()
|
||||
tool.Ifc.schema()
|
||||
IfcStore.relink_all_objects()
|
||||
|
||||
|
||||
@persistent
|
||||
def undo_post(scene):
|
||||
if IfcStore.last_transaction != bpy.context.scene.BIMProperties.last_transaction:
|
||||
IfcStore.last_transaction = bpy.context.scene.BIMProperties.last_transaction
|
||||
IfcStore.undo(until_key=bpy.context.scene.BIMProperties.last_transaction)
|
||||
def undo_post(scene: bpy.types.Scene) -> None:
|
||||
props = tool.Blender.get_bim_props()
|
||||
if IfcStore.last_transaction != props.last_transaction:
|
||||
IfcStore.last_transaction = props.last_transaction
|
||||
IfcStore.undo(until_key=props.last_transaction)
|
||||
refresh_ui_data()
|
||||
tool.Ifc.rebuild_element_maps()
|
||||
|
||||
|
||||
@persistent
|
||||
def redo_post(scene):
|
||||
if IfcStore.last_transaction != bpy.context.scene.BIMProperties.last_transaction:
|
||||
IfcStore.last_transaction = bpy.context.scene.BIMProperties.last_transaction
|
||||
IfcStore.redo(until_key=bpy.context.scene.BIMProperties.last_transaction)
|
||||
def redo_post(scene: bpy.types.Scene) -> None:
|
||||
props = tool.Blender.get_bim_props()
|
||||
if IfcStore.last_transaction != props.last_transaction:
|
||||
IfcStore.last_transaction = props.last_transaction
|
||||
IfcStore.redo(until_key=props.last_transaction)
|
||||
refresh_ui_data()
|
||||
tool.Ifc.rebuild_element_maps()
|
||||
|
||||
@@ -268,10 +284,10 @@ def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None
|
||||
return pao
|
||||
|
||||
|
||||
def viewport_shading_changed_callback(area):
|
||||
def viewport_shading_changed_callback(area: bpy.types.Area) -> None:
|
||||
shading = area.spaces.active.shading.type
|
||||
if shading == "RENDERED":
|
||||
bpy.context.scene.BIMStylesProperties.active_style_type = "External"
|
||||
tool.Style.get_style_props().active_style_type = "External"
|
||||
|
||||
|
||||
def subscribe_to_viewport_shading_changes():
|
||||
@@ -326,13 +342,14 @@ def load_post(scene):
|
||||
tool.Blender.setup_tabs()
|
||||
|
||||
if tool.Ifc.get() and bpy.data.is_saved:
|
||||
bpy.context.scene.BIMProperties.has_blend_warning = True
|
||||
props = tool.Blender.get_bim_props()
|
||||
props.has_blend_warning = True
|
||||
|
||||
# Bonsai overlays
|
||||
georeference_props = bpy.context.scene.BIMGeoreferenceProperties
|
||||
georeference_props = tool.Georeference.get_georeference_props()
|
||||
aggregate_props = bpy.context.scene.BIMAggregateProperties
|
||||
nest_props = bpy.context.scene.BIMNestProperties
|
||||
model_props = bpy.context.scene.BIMModelProperties
|
||||
model_props = tool.Model.get_model_props()
|
||||
if georeference_props.should_visualise:
|
||||
GeoreferenceDecorator.install(bpy.context)
|
||||
if aggregate_props.aggregate_decorator:
|
||||
|
||||
@@ -27,12 +27,12 @@ import ifcopenshell.util.element
|
||||
import ifcopenshell.util.unit
|
||||
from ifcopenshell.util.doc import get_attribute_doc, get_predefined_type_doc, get_property_doc
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from typing import Optional, Callable, Any, Union, Iterable, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import bonsai.bim.prop
|
||||
from bonsai.bim.prop import Attribute
|
||||
from bonsai.bim.module.search.prop import BIMFilterGroup
|
||||
|
||||
# ImportCallback return values:
|
||||
# - None - property should be imported by default workflow
|
||||
@@ -76,8 +76,8 @@ def draw_attribute(
|
||||
elif value_name == "filepath_value":
|
||||
attribute.filepath_value.layout_file_select(layout, filter_glob=attribute.filter_glob, text=attribute.name)
|
||||
elif attribute.name in ("ScheduleDuration", "ActualDuration", "FreeFloat", "TotalFloat"):
|
||||
propis = bpy.context.scene.BIMWorkScheduleProperties
|
||||
for item in propis.durations_attributes:
|
||||
props = tool.Sequence.get_work_schedule_props()
|
||||
for item in props.durations_attributes:
|
||||
if item.name == attribute.name:
|
||||
duration_props = item
|
||||
layout.label(text=attribute.name)
|
||||
@@ -119,7 +119,8 @@ def import_attributes(
|
||||
data: dict[str, Any],
|
||||
callback: Optional[ImportCallback] = None,
|
||||
) -> None:
|
||||
for attribute in IfcStore.get_schema().declaration_by_name(ifc_class).all_attributes():
|
||||
schema = tool.Ifc.schema()
|
||||
for attribute in schema.declaration_by_name(ifc_class).all_attributes():
|
||||
import_attribute(attribute, props, data, callback=callback)
|
||||
|
||||
|
||||
@@ -292,7 +293,10 @@ def prop_with_search(
|
||||
should_click_ok: bool = False,
|
||||
original_operator_path: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
) -> bpy.types.UILayout:
|
||||
"""
|
||||
:return: Added row.
|
||||
"""
|
||||
# kwargs are layout.prop arguments (text, icon, etc.)
|
||||
row = layout.row(align=True)
|
||||
row.prop(data, prop_name, **kwargs)
|
||||
@@ -306,6 +310,7 @@ def prop_with_search(
|
||||
op.original_operator_path = original_operator_path or ""
|
||||
except TypeError: # Prop is not iterable
|
||||
pass
|
||||
return row
|
||||
|
||||
|
||||
def get_enum_items(
|
||||
@@ -372,11 +377,16 @@ def convert_property_group_from_si(property_group: bpy.types.PropertyGroup, skip
|
||||
setattr(property_group, prop_name, prop_value)
|
||||
|
||||
|
||||
def draw_filter(layout: bpy.types.UILayout, filter_groups, data, module: str) -> None:
|
||||
def draw_filter(
|
||||
layout: bpy.types.UILayout,
|
||||
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup],
|
||||
data,
|
||||
module: str,
|
||||
) -> None:
|
||||
if not data.is_loaded:
|
||||
data.load()
|
||||
|
||||
sprops = bpy.context.scene.BIMSearchProperties
|
||||
sprops = tool.Search.get_search_props()
|
||||
|
||||
if tool.Ifc.get():
|
||||
row = layout.row(align=True)
|
||||
|
||||
@@ -33,7 +33,7 @@ import bonsai.bim.handler
|
||||
import bonsai.tool as tool
|
||||
from pathlib import Path
|
||||
from bonsai.tool.brick import BrickStore
|
||||
from typing import Set, Union, Optional, TypedDict, Callable, NotRequired, cast
|
||||
from typing import Set, Union, Optional, TypedDict, Callable, NotRequired
|
||||
|
||||
|
||||
IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object]
|
||||
@@ -105,10 +105,8 @@ class IfcStore:
|
||||
@staticmethod
|
||||
def get_file():
|
||||
if IfcStore.file is None:
|
||||
IfcStore.path = cast(str, bpy.context.scene.BIMProperties.ifc_file)
|
||||
# Interpret relative paths as relative to .blend file.
|
||||
if IfcStore.path and not os.path.isabs(IfcStore.path):
|
||||
IfcStore.path = os.path.abspath(os.path.join(bpy.path.abspath("//"), IfcStore.path))
|
||||
props = tool.Blender.get_bim_props()
|
||||
IfcStore.set_path(props.ifc_file)
|
||||
if IfcStore.path:
|
||||
try:
|
||||
IfcStore.load_file(IfcStore.path)
|
||||
@@ -116,13 +114,21 @@ class IfcStore:
|
||||
print(f"Failed to load file {IfcStore.path}. Error details: {e}")
|
||||
return IfcStore.file
|
||||
|
||||
@staticmethod
|
||||
def set_path(value):
|
||||
IfcStore.path = value
|
||||
# Interpret relative paths as relative to .blend file.
|
||||
if IfcStore.path and not os.path.isabs(IfcStore.path):
|
||||
IfcStore.path = os.path.abspath(os.path.join(bpy.path.abspath("//"), IfcStore.path))
|
||||
|
||||
@staticmethod
|
||||
def get_cache():
|
||||
if IfcStore.cache is None and IfcStore.path:
|
||||
props = tool.Blender.get_bim_props()
|
||||
ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp
|
||||
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest()
|
||||
os.makedirs(bpy.context.scene.BIMProperties.cache_dir, exist_ok=True)
|
||||
IfcStore.cache_path = os.path.join(bpy.context.scene.BIMProperties.cache_dir, f"{ifc_hash}.h5")
|
||||
os.makedirs(props.cache_dir, exist_ok=True)
|
||||
IfcStore.cache_path = os.path.join(props.cache_dir, f"{ifc_hash}.h5")
|
||||
cache_path = Path(IfcStore.cache_path)
|
||||
cache_settings = ifcopenshell.geom.settings()
|
||||
serializer_settings = ifcopenshell.geom.serializer_settings()
|
||||
@@ -162,7 +168,8 @@ class IfcStore:
|
||||
assert IfcStore.file
|
||||
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.cache_dir, f"{ifc_hash}.h5")
|
||||
props = tool.Blender.get_bim_props()
|
||||
new_cache_path = os.path.join(props.cache_dir, f"{ifc_hash}.h5")
|
||||
IfcStore.cache = None
|
||||
try:
|
||||
shutil.move(IfcStore.cache_path, new_cache_path)
|
||||
@@ -178,6 +185,7 @@ class IfcStore:
|
||||
if not os.path.isfile(path):
|
||||
return
|
||||
extension = path.split(".")[-1]
|
||||
props = tool.Project.get_project_props()
|
||||
if extension.lower() == "ifczip":
|
||||
with tempfile.TemporaryDirectory() as unzipped_path:
|
||||
with zipfile.ZipFile(path, "r") as zip_ref:
|
||||
@@ -187,7 +195,7 @@ class IfcStore:
|
||||
return
|
||||
elif extension.lower() == "ifcxml":
|
||||
IfcStore.file = ifcopenshell.file(ifcopenshell.ifcopenshell_wrapper.parse_ifcxml(path))
|
||||
elif bpy.context.scene.BIMProjectProperties.should_stream:
|
||||
elif props.should_stream:
|
||||
IfcStore.file = ifcopenshell.open(path, should_stream=True)
|
||||
else:
|
||||
IfcStore.file = ifcopenshell.open(path)
|
||||
@@ -195,9 +203,8 @@ class IfcStore:
|
||||
@staticmethod
|
||||
def get_schema() -> ifcopenshell.ifcopenshell_wrapper.schema_definition:
|
||||
if IfcStore.file is None:
|
||||
IfcStore.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(
|
||||
bpy.context.scene.BIMProjectProperties.export_schema
|
||||
)
|
||||
props = tool.Project.get_project_props()
|
||||
IfcStore.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(props.export_schema)
|
||||
elif IfcStore.schema is None:
|
||||
IfcStore.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(IfcStore.file.schema_identifier)
|
||||
return IfcStore.schema
|
||||
@@ -247,7 +254,7 @@ class IfcStore:
|
||||
# refactor this class and deprecate usage of IfcStore in favour of
|
||||
# tools.
|
||||
if not isinstance(obj, (bpy.types.Object, bpy.types.Material)):
|
||||
obj.BIMMeshProperties.ifc_definition_id = element.id()
|
||||
tool.Geometry.get_mesh_props(obj).ifc_definition_id = element.id()
|
||||
return
|
||||
|
||||
existing_obj = IfcStore.id_map.get(element.id(), None)
|
||||
@@ -262,9 +269,11 @@ class IfcStore:
|
||||
IfcStore.guid_map[global_id] = obj
|
||||
|
||||
if element.is_a("IfcSurfaceStyle"):
|
||||
obj.BIMStyleProperties.ifc_definition_id = element.id()
|
||||
props = tool.Style.get_material_style_props(obj)
|
||||
props.ifc_definition_id = element.id()
|
||||
else:
|
||||
obj.BIMObjectProperties.ifc_definition_id = element.id()
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
props.ifc_definition_id = element.id()
|
||||
|
||||
tool.Ifc.setup_listeners(obj)
|
||||
|
||||
@@ -402,15 +411,24 @@ class IfcStore:
|
||||
@staticmethod
|
||||
def purge_blender_ifc_data(obj: IFC_CONNECTED_TYPE) -> None:
|
||||
if isinstance(obj, bpy.types.Material):
|
||||
obj.BIMStyleProperties.ifc_definition_id = 0
|
||||
props = tool.Style.get_material_style_props(obj)
|
||||
props.ifc_definition_id = 0
|
||||
else: # bpy.types.Object
|
||||
obj.BIMObjectProperties.ifc_definition_id = 0
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
props.ifc_definition_id = 0
|
||||
|
||||
@staticmethod
|
||||
def execute_ifc_operator(operator: tool.Ifc.Operator, context: bpy.types.Context, is_invoke=False) -> set[str]:
|
||||
def execute_ifc_operator(
|
||||
operator: tool.Ifc.Operator,
|
||||
context: bpy.types.Context,
|
||||
event=None,
|
||||
method: Literal["EXECUTE", "INVOKE", "MODAL"] = "EXECUTE",
|
||||
) -> set[str]:
|
||||
bonsai.last_actions.append({"type": "operator", "name": operator.bl_idname})
|
||||
bpy.context.scene.BIMProperties.is_dirty = True
|
||||
is_top_level_operator = not bool(IfcStore.current_transaction)
|
||||
props = tool.Blender.get_bim_props()
|
||||
props.is_dirty = True
|
||||
# Modals don't nest, and Blender handles the loop that continuously calls modal()
|
||||
is_top_level_operator = not bool(IfcStore.current_transaction) or (method == "MODAL")
|
||||
|
||||
if is_top_level_operator:
|
||||
IfcStore.begin_transaction(operator)
|
||||
@@ -436,10 +454,12 @@ class IfcStore:
|
||||
bonsai.bim.handler.refresh_ui_data()
|
||||
|
||||
try:
|
||||
if is_invoke:
|
||||
result = getattr(operator, "_invoke")(context, None)
|
||||
else:
|
||||
if method == "EXECUTE":
|
||||
result = getattr(operator, "_execute")(context)
|
||||
elif method == "INVOKE":
|
||||
result = getattr(operator, "_invoke")(context, event)
|
||||
elif method == "MODAL":
|
||||
result = getattr(operator, "_modal")(context, event)
|
||||
except:
|
||||
bonsai.last_error = traceback.format_exc()
|
||||
# Try to ensure undo will work since Blender undo does work in case of errors.
|
||||
@@ -451,12 +471,27 @@ class IfcStore:
|
||||
end_top_level_operator()
|
||||
raise
|
||||
|
||||
end_top_level_operator()
|
||||
if method == "MODAL":
|
||||
if result == {"FINISHED"}:
|
||||
end_top_level_operator()
|
||||
elif result == {"CANCELLED"}:
|
||||
# Please read the docs: https://docs.blender.org/api/current/bpy.types.Operator.html
|
||||
# > "when an operator returns {'CANCELLED'}, no undo step will be created".
|
||||
# This means that if your modal edits IFC data, then the user
|
||||
# cancels it, Blender's undo history will not be in sync with
|
||||
# Bonsai / IfcOpenShell's undo history. Instead of hoping for
|
||||
# Bonsai devs to remember to handle the "cancel" state (i.e.
|
||||
# detect escape keypress) and return {"FINISHED"}, we instead
|
||||
# always enforce an undo step.
|
||||
bpy.ops.ed.undo_push(message=f"Cancel {operator.bl_idname}")
|
||||
end_top_level_operator()
|
||||
else:
|
||||
end_top_level_operator()
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def begin_transaction(operator: tool.Ifc.Operator) -> None:
|
||||
IfcStore.current_transaction = str(uuid.uuid4())
|
||||
IfcStore.current_transaction = str(uuid.uuid4()) + operator.__class__.__name__
|
||||
operator.transaction_key = IfcStore.current_transaction
|
||||
|
||||
@staticmethod
|
||||
@@ -470,7 +505,8 @@ class IfcStore:
|
||||
) -> None:
|
||||
key = getattr(operator, "transaction_key", None)
|
||||
data = getattr(operator, "transaction_data", None)
|
||||
bpy.context.scene.BIMProperties.last_transaction = key
|
||||
props = tool.Blender.get_bim_props()
|
||||
props.last_transaction = key
|
||||
IfcStore.last_transaction = key
|
||||
rollback = rollback or getattr(operator, "rollback", lambda data: True)
|
||||
commit = commit or getattr(operator, "commit", lambda data: True)
|
||||
|
||||
@@ -20,10 +20,12 @@ from __future__ import annotations
|
||||
import bpy
|
||||
import time
|
||||
import json
|
||||
import ifcpatch
|
||||
import logging
|
||||
import traceback
|
||||
import mathutils
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
import multiprocessing
|
||||
import ifcopenshell
|
||||
import ifcopenshell.geom
|
||||
@@ -36,7 +38,8 @@ import ifcopenshell.util.shape
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.ifc import IfcStore, IFC_CONNECTED_TYPE
|
||||
from bonsai.tool.loader import OBJECT_DATA_TYPE
|
||||
from typing import Dict, Union, Optional, Any
|
||||
from typing import Dict, Union, Optional, Any, Literal
|
||||
from ifcopenshell.util.shape import MatrixType
|
||||
|
||||
|
||||
class MaterialCreator:
|
||||
@@ -68,6 +71,9 @@ class MaterialCreator:
|
||||
if isinstance(mesh, bpy.types.Curve):
|
||||
return
|
||||
|
||||
if mesh.get("has_layer_styles", None) == True:
|
||||
return
|
||||
|
||||
self.mesh = mesh
|
||||
self.obj = obj
|
||||
if element.is_a("IfcTypeProduct"):
|
||||
@@ -81,15 +87,16 @@ class MaterialCreator:
|
||||
|
||||
def load_existing_materials(self) -> None:
|
||||
for material in bpy.data.materials:
|
||||
if ifc_definition_id := material.BIMStyleProperties.ifc_definition_id:
|
||||
if ifc_definition_id := tool.Blender.get_ifc_definition_id(material):
|
||||
self.styles[ifc_definition_id] = material
|
||||
|
||||
def parse_element_type_material_styles(self, element: ifcopenshell.entity_instance) -> None:
|
||||
if self.mesh["ios_materials"]:
|
||||
return # Already has materials assign to the representation itself
|
||||
return # Already has materials assign to the representation itself
|
||||
# Otherwise, we need to check for material styles on the element, since
|
||||
# create_shape on types only works on representations.
|
||||
context = tool.Ifc.get().by_id(self.mesh.BIMMeshProperties.ifc_definition_id).ContextOfItems
|
||||
mprops = tool.Geometry.get_mesh_props(self.mesh)
|
||||
context = tool.Ifc.get().by_id(mprops.ifc_definition_id).ContextOfItems
|
||||
for material in ifcopenshell.util.element.get_materials(element):
|
||||
if style := ifcopenshell.util.representation.get_material_style(material, context):
|
||||
self.mesh["ios_materials"] = (style.id(),)
|
||||
@@ -167,7 +174,7 @@ class MaterialCreator:
|
||||
if -1 in self.mesh["ios_material_ids"]:
|
||||
material_to_slot[-1] = get_empty_slot_index()
|
||||
|
||||
material_index = [material_to_slot[mat_id] for mat_id in self.mesh["ios_material_ids"]]
|
||||
material_index = np.array([material_to_slot[mat_id] for mat_id in self.mesh["ios_material_ids"]], dtype="I")
|
||||
self.mesh.polygons.foreach_set("material_index", material_index)
|
||||
|
||||
def resolve_all_stylable_representation_items(
|
||||
@@ -217,7 +224,7 @@ class IfcImporter:
|
||||
self.progress = 0
|
||||
|
||||
self.material_creator = MaterialCreator(ifc_import_settings, self)
|
||||
classes_to_wireframe_str = bpy.context.scene.DocProperties.classes_to_wireframe
|
||||
classes_to_wireframe_str = tool.Drawing.get_document_props().classes_to_wireframe
|
||||
self.classes_to_wireframe_list = [word.strip() for word in classes_to_wireframe_str.split(",")]
|
||||
|
||||
def profile_code(self, message: str) -> None:
|
||||
@@ -293,6 +300,20 @@ class IfcImporter:
|
||||
bpy.context.window_manager.progress_end()
|
||||
|
||||
def process_context_filter(self) -> None:
|
||||
contexts = self.file.by_type("IfcGeometricRepresentationContext")
|
||||
if len(contexts) > 100: # Probably something strange happening. Encountered from Revizto.
|
||||
print("Warning! Excessive contexts were found and merged where applicable.")
|
||||
uniques = {}
|
||||
i = 0
|
||||
for element in contexts:
|
||||
data = "-".join([str(a) for a in element])
|
||||
if unique := uniques.get(data, None):
|
||||
ifcopenshell.util.element.replace_element(element, unique)
|
||||
self.file.remove(element)
|
||||
i += 1
|
||||
else:
|
||||
uniques[data] = element
|
||||
print(f"Replaced {i} IfcGeometricRepresentationContext")
|
||||
tool.Loader.settings.contexts = ifcopenshell.util.representation.get_prioritised_contexts(self.file)
|
||||
tool.Loader.settings.context_settings = tool.Loader.create_settings()
|
||||
tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True)
|
||||
@@ -433,7 +454,7 @@ class IfcImporter:
|
||||
return False
|
||||
|
||||
def calculate_model_offset(self) -> None:
|
||||
props = bpy.context.scene.BIMGeoreferenceProperties
|
||||
props = tool.Georeference.get_georeference_props()
|
||||
if self.ifc_import_settings.false_origin_mode == "MANUAL":
|
||||
tool.Loader.set_manual_blender_offset(self.file)
|
||||
elif self.ifc_import_settings.false_origin_mode == "AUTOMATIC":
|
||||
@@ -474,14 +495,16 @@ class IfcImporter:
|
||||
if grid.WAxes:
|
||||
self.create_grid_axes(grid.WAxes, grid_obj, grid_placement)
|
||||
|
||||
def create_grid_axes(self, axes, grid_obj, grid_placement):
|
||||
def create_grid_axes(
|
||||
self, axes: list[ifcopenshell.entity_instance], grid_obj: bpy.types.Object, grid_placement: MatrixType
|
||||
) -> None:
|
||||
for axis in axes:
|
||||
shape = tool.Loader.create_generic_shape(axis.AxisCurve)
|
||||
mesh = self.create_mesh(axis, shape)
|
||||
obj = bpy.data.objects.new(tool.Loader.get_name(axis), mesh)
|
||||
obj.show_in_front = True
|
||||
self.link_element(axis, obj)
|
||||
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, grid_placement.copy()))
|
||||
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, grid_placement))
|
||||
|
||||
def create_element_types(self):
|
||||
for element_type in self.element_types:
|
||||
@@ -506,8 +529,9 @@ class IfcImporter:
|
||||
pass
|
||||
elif shape:
|
||||
mesh = self.create_mesh(element, shape)
|
||||
tool.Loader.link_mesh(shape, mesh)
|
||||
self.meshes[mesh_name] = mesh
|
||||
if mesh is not None:
|
||||
tool.Loader.link_mesh(shape, mesh)
|
||||
self.meshes[mesh_name] = mesh
|
||||
else:
|
||||
self.ifc_import_settings.logger.error("Failed to generate shape for %s", element)
|
||||
break
|
||||
@@ -588,6 +612,7 @@ class IfcImporter:
|
||||
obj.hide_select = True
|
||||
|
||||
def create_generic_sqlite_elements(self, elements: set[ifcopenshell.entity_instance]) -> None:
|
||||
assert isinstance(self.file, ifcopenshell.sql.sqlite)
|
||||
self.geometry_cache = self.file.get_geometry([e.id() for e in elements])
|
||||
for geometry_id, geometry in self.geometry_cache["geometry"].items():
|
||||
mesh_name = tool.Loader.get_mesh_name_from_shape(type("Geometry", (), {"id": geometry_id}))
|
||||
@@ -597,21 +622,9 @@ class IfcImporter:
|
||||
mesh["has_cartesian_point_offset"] = False
|
||||
|
||||
if geometry["faces"]:
|
||||
num_vertices = len(verts) // 3
|
||||
total_faces = len(geometry["faces"])
|
||||
loop_start = range(0, total_faces, 3)
|
||||
num_loops = total_faces // 3
|
||||
loop_total = [3] * num_loops
|
||||
num_vertex_indices = len(geometry["faces"])
|
||||
|
||||
mesh.vertices.add(num_vertices)
|
||||
mesh.vertices.foreach_set("co", verts)
|
||||
mesh.loops.add(num_vertex_indices)
|
||||
mesh.loops.foreach_set("vertex_index", geometry["faces"])
|
||||
mesh.polygons.add(num_loops)
|
||||
mesh.polygons.foreach_set("loop_start", loop_start)
|
||||
mesh.polygons.foreach_set("loop_total", loop_total)
|
||||
mesh.update()
|
||||
mesh = tool.Loader.create_mesh_from_shape(
|
||||
mesh=mesh, faces=geometry["faces"].reshape(-1, 3), verts=verts.reshape(-1, 3)
|
||||
)
|
||||
else:
|
||||
e = geometry["edges"]
|
||||
v = verts
|
||||
@@ -748,12 +761,9 @@ class IfcImporter:
|
||||
def create_pointclouds(self, products: set[ifcopenshell.entity_instance]) -> set[ifcopenshell.entity_instance]:
|
||||
result = set()
|
||||
for product in products:
|
||||
representation = self.get_pointcloud_representation(product)
|
||||
if representation is not None:
|
||||
pointcloud = self.create_pointcloud(product, representation)
|
||||
if pointcloud is not None:
|
||||
if representation := self.get_pointcloud_representation(product):
|
||||
if pointcloud := self.create_pointcloud(product, representation):
|
||||
result.add(pointcloud)
|
||||
|
||||
return result
|
||||
|
||||
def create_pointcloud(
|
||||
@@ -801,8 +811,9 @@ class IfcImporter:
|
||||
materials_updated = bool(mesh)
|
||||
if mesh is None:
|
||||
mesh = self.create_mesh(element, shape)
|
||||
tool.Loader.link_mesh(shape, mesh)
|
||||
self.meshes[mesh_name] = mesh
|
||||
if mesh is not None:
|
||||
tool.Loader.link_mesh(shape, mesh)
|
||||
self.meshes[mesh_name] = mesh
|
||||
else:
|
||||
mesh = None
|
||||
|
||||
@@ -813,11 +824,10 @@ class IfcImporter:
|
||||
if element.is_a(ifcclass):
|
||||
obj.display_type = "WIRE"
|
||||
|
||||
if shape:
|
||||
if shape and mesh:
|
||||
# We use numpy here because Blender mathutils.Matrix is not accurate enough
|
||||
mat = np.array(shape.transformation.matrix).reshape((4, 4), order="F")
|
||||
mat = ifcopenshell.util.shape.get_shape_matrix(shape)
|
||||
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, mat))
|
||||
assert mesh # Type checker.
|
||||
if not materials_updated:
|
||||
self.material_creator.create(element, obj, mesh, tool.Geometry.does_shape_has_openings(shape))
|
||||
elif mesh: # When does this occur?
|
||||
@@ -888,17 +898,19 @@ class IfcImporter:
|
||||
|
||||
def load_file(self):
|
||||
self.ifc_import_settings.logger.info("loading file %s", self.ifc_import_settings.input_file)
|
||||
if not bpy.context.scene.BIMProperties.ifc_file:
|
||||
bpy.context.scene.BIMProperties.ifc_file = self.ifc_import_settings.input_file
|
||||
self.file = IfcStore.get_file()
|
||||
props = tool.Blender.get_bim_props()
|
||||
if not props.ifc_file:
|
||||
props.ifc_file = self.ifc_import_settings.input_file
|
||||
self.file = tool.Ifc.get()
|
||||
|
||||
def calculate_unit_scale(self):
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
||||
tool.Loader.set_unit_scale(self.unit_scale)
|
||||
|
||||
def set_units(self):
|
||||
def set_units(self) -> None:
|
||||
if not (assignment := self.file.by_type("IfcProject")[0].UnitsInContext):
|
||||
return # Geometry is optional in IFC
|
||||
props = tool.Blender.get_bim_props()
|
||||
for unit in assignment.Units:
|
||||
if unit.is_a("IfcNamedUnit") and unit.UnitType == "LENGTHUNIT":
|
||||
if unit.is_a("IfcSIUnit"):
|
||||
@@ -918,26 +930,27 @@ class IfcImporter:
|
||||
elif unit.is_a("IfcNamedUnit") and unit.UnitType == "AREAUNIT":
|
||||
name = unit.Name if unit.is_a("IfcSIUnit") else unit.Name.lower()
|
||||
try:
|
||||
bpy.context.scene.BIMProperties.area_unit = "{}{}".format(
|
||||
props.area_unit = "{}{}".format(
|
||||
unit.Prefix + "/" if hasattr(unit, "Prefix") and unit.Prefix else "", name
|
||||
)
|
||||
except: # Probably an invalid unit.
|
||||
bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE"
|
||||
props.area_unit = "SQUARE_METRE"
|
||||
elif unit.is_a("IfcNamedUnit") and unit.UnitType == "VOLUMEUNIT":
|
||||
name = unit.Name if unit.is_a("IfcSIUnit") else unit.Name.lower()
|
||||
try:
|
||||
bpy.context.scene.BIMProperties.volume_unit = "{}{}".format(
|
||||
props.volume_unit = "{}{}".format(
|
||||
unit.Prefix + "/" if hasattr(unit, "Prefix") and unit.Prefix else "", name
|
||||
)
|
||||
except: # Probably an invalid unit.
|
||||
bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE"
|
||||
props.volume_unit = "CUBIC_METRE"
|
||||
|
||||
def create_project(self):
|
||||
project = self.file.by_type("IfcProject")[0]
|
||||
self.project = {"ifc": project}
|
||||
obj = tool.Ifc.get_object(project)
|
||||
if obj:
|
||||
self.project["blender"] = obj.BIMObjectProperties.collection
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
self.project["blender"] = props.collection
|
||||
self.has_existing_project = True
|
||||
return
|
||||
self.project["blender"] = bpy.data.collections.new(
|
||||
@@ -947,10 +960,16 @@ class IfcImporter:
|
||||
obj.hide_select = True
|
||||
self.project["blender"].objects.link(obj)
|
||||
self.project["blender"].BIMCollectionProperties.obj = obj
|
||||
obj.BIMObjectProperties.collection = self.collections[project.GlobalId] = self.project["blender"]
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
props.collection = self.collections[project.GlobalId] = self.project["blender"]
|
||||
|
||||
def create_styles(self) -> None:
|
||||
for style in self.file.by_type("IfcSurfaceStyle"):
|
||||
styles = self.file.by_type("IfcSurfaceStyle")
|
||||
if len(styles) > self.ifc_import_settings.style_limit: # Probably something strange happening
|
||||
print("Warning! Excessive styles were found and merged where applicable.")
|
||||
ifcpatch.execute({"file": self.file, "recipe": "MergeStyles", "arguments": []})
|
||||
styles = self.file.by_type("IfcSurfaceStyle")
|
||||
for style in styles:
|
||||
self.create_style(style)
|
||||
|
||||
def create_style(self, style: ifcopenshell.entity_instance) -> None:
|
||||
@@ -963,10 +982,11 @@ class IfcImporter:
|
||||
self.material_creator.styles[style.id()] = blender_material
|
||||
|
||||
style_elements = tool.Style.get_style_elements(blender_material)
|
||||
props = tool.Style.get_material_style_props(blender_material)
|
||||
if tool.Style.has_blender_external_style(style_elements):
|
||||
blender_material.BIMStyleProperties.active_style_type = "External"
|
||||
props.active_style_type = "External"
|
||||
else:
|
||||
blender_material.BIMStyleProperties.active_style_type = "Shading"
|
||||
props.active_style_type = "Shading"
|
||||
|
||||
def place_objects_in_collections(self) -> None:
|
||||
for ifc_definition_id, obj in self.added_data.items():
|
||||
@@ -974,10 +994,10 @@ class IfcImporter:
|
||||
tool.Collector.assign(obj, should_clean_users_collection=False)
|
||||
|
||||
def is_curve_annotation(self, element: ifcopenshell.entity_instance) -> bool:
|
||||
object_type = element.ObjectType
|
||||
object_type = ifcopenshell.util.element.get_predefined_type(element)
|
||||
return (
|
||||
object_type in tool.Drawing.ANNOTATION_TYPES_DATA
|
||||
and tool.Drawing.ANNOTATION_TYPES_DATA[object_type][3] == "curve"
|
||||
and tool.Drawing.ANNOTATION_TYPES_DATA[object_type].data_type == "curve"
|
||||
)
|
||||
|
||||
def get_drawing_group(self, element):
|
||||
@@ -1030,8 +1050,8 @@ class IfcImporter:
|
||||
self,
|
||||
element: ifcopenshell.entity_instance,
|
||||
shape: Union[ifcopenshell.geom.ShapeElementType, ifcopenshell.geom.ShapeType],
|
||||
cartesian_point_offset=None,
|
||||
) -> bpy.types.Mesh:
|
||||
cartesian_point_offset: Union[npt.NDArray[np.float64], Literal[False]] = None,
|
||||
) -> Union[bpy.types.Mesh, None]:
|
||||
try:
|
||||
if hasattr(shape, "geometry"):
|
||||
# shape is ShapeElementType
|
||||
@@ -1046,40 +1066,36 @@ class IfcImporter:
|
||||
old_mesh.name = mesh_name + ".old"
|
||||
mesh = bpy.data.meshes.new(mesh_name)
|
||||
|
||||
verts = ifcopenshell.util.shape.get_vertices(geometry)
|
||||
if cartesian_point_offset is False:
|
||||
verts = geometry.verts
|
||||
mesh["has_cartesian_point_offset"] = False
|
||||
elif cartesian_point_offset is not None:
|
||||
verts_array = np.array(geometry.verts)
|
||||
offset = np.array([-cartesian_point_offset[0], -cartesian_point_offset[1], -cartesian_point_offset[2]])
|
||||
offset_verts = verts_array + np.tile(offset, len(verts_array) // 3)
|
||||
verts = offset_verts.tolist()
|
||||
verts = verts - cartesian_point_offset
|
||||
|
||||
mesh["has_cartesian_point_offset"] = True
|
||||
mesh["cartesian_point_offset"] = (
|
||||
f"{cartesian_point_offset[0]},{cartesian_point_offset[1]},{cartesian_point_offset[2]}"
|
||||
)
|
||||
elif geometry.verts and tool.Loader.is_point_far_away(
|
||||
(geometry.verts[0], geometry.verts[1], geometry.verts[2]), is_meters=True
|
||||
):
|
||||
elif verts.size and tool.Loader.is_point_far_away(verts[0], is_meters=True):
|
||||
# Shift geometry close to the origin based off that first vert it found
|
||||
verts_array = np.array(geometry.verts)
|
||||
offset = np.array([-geometry.verts[0], -geometry.verts[1], -geometry.verts[2]])
|
||||
offset_verts = verts_array + np.tile(offset, len(verts_array) // 3)
|
||||
verts = offset_verts.tolist()
|
||||
offset = verts[0]
|
||||
verts = verts - offset
|
||||
|
||||
mesh["has_cartesian_point_offset"] = True
|
||||
mesh["cartesian_point_offset"] = f"{geometry.verts[0]},{geometry.verts[1]},{geometry.verts[2]}"
|
||||
mesh["cartesian_point_offset"] = f"{offset[0]},{offset[1]},{offset[2]}"
|
||||
else:
|
||||
verts = geometry.verts
|
||||
mesh["has_cartesian_point_offset"] = False
|
||||
|
||||
return tool.Loader.convert_geometry_to_mesh(
|
||||
mesh = tool.Loader.convert_geometry_to_mesh(
|
||||
geometry,
|
||||
mesh,
|
||||
verts=verts,
|
||||
load_indexed_maps=self.ifc_import_settings.load_indexed_maps,
|
||||
)
|
||||
# E.g. `create_mesh` is also used for IfcRelSpaceBoundary.
|
||||
if element.is_a("IfcObjectDefinition"):
|
||||
return tool.Loader.slice_layerset_mesh(element, mesh)
|
||||
return mesh
|
||||
except:
|
||||
self.ifc_import_settings.logger.error("Could not create mesh for %s", element)
|
||||
import traceback
|
||||
@@ -1087,9 +1103,10 @@ class IfcImporter:
|
||||
print(traceback.format_exc())
|
||||
|
||||
def set_default_context(self):
|
||||
rprops = tool.Root.get_root_props()
|
||||
for subcontext in self.file.by_type("IfcGeometricRepresentationSubContext"):
|
||||
if subcontext.ContextIdentifier == "Body":
|
||||
bpy.context.scene.BIMRootProperties.contexts = str(subcontext.id())
|
||||
rprops.contexts = str(subcontext.id())
|
||||
break
|
||||
|
||||
def link_element(self, element: ifcopenshell.entity_instance, obj: IFC_CONNECTED_TYPE) -> None:
|
||||
@@ -1135,6 +1152,7 @@ class IfcImportSettings:
|
||||
self.deflection_tolerance = 0.001
|
||||
self.angular_tolerance = 0.5
|
||||
self.void_limit = 30
|
||||
self.style_limit = 300
|
||||
# Locations greater than 1km are not considered "small sites" according to the georeferencing guide
|
||||
# Users can configure this if they have to handle larger sites but beware of surveying precision
|
||||
self.distance_limit = 1000
|
||||
@@ -1155,7 +1173,7 @@ class IfcImportSettings:
|
||||
|
||||
@staticmethod
|
||||
def factory(context=None, input_file=None, logger=None):
|
||||
scene_diff = bpy.context.scene.DiffProperties
|
||||
scene_diff = tool.Blender.get_diff_props()
|
||||
props = tool.Project.get_project_props()
|
||||
settings = IfcImportSettings()
|
||||
settings.input_file = input_file
|
||||
@@ -1172,6 +1190,7 @@ class IfcImportSettings:
|
||||
settings.deflection_tolerance = props.deflection_tolerance
|
||||
settings.angular_tolerance = props.angular_tolerance
|
||||
settings.void_limit = props.void_limit
|
||||
settings.style_limit = props.style_limit
|
||||
settings.distance_limit = props.distance_limit
|
||||
settings.false_origin_mode = props.false_origin_mode
|
||||
try:
|
||||
|
||||
@@ -131,6 +131,9 @@ class AggregateDecorator:
|
||||
return shader
|
||||
|
||||
def draw_custom_batch(self, coords, color):
|
||||
indices = None
|
||||
if not tool.Blender.validate_shader_batch_data(coords, indices):
|
||||
return
|
||||
shader = self.dotted_line_shader()
|
||||
|
||||
arc_lengths = [0]
|
||||
@@ -141,6 +144,7 @@ class AggregateDecorator:
|
||||
shader,
|
||||
"LINE_STRIP",
|
||||
{"position": coords, "arcLength": arc_lengths},
|
||||
indices=indices,
|
||||
)
|
||||
|
||||
matrix = bpy.context.region_data.perspective_matrix
|
||||
@@ -150,6 +154,8 @@ class AggregateDecorator:
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
@@ -243,6 +249,8 @@ class AggregateModeDecorator:
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
|
||||
@@ -23,7 +23,6 @@ import ifcopenshell.util.element
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.aggregate as core
|
||||
import bonsai.core.spatial
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
|
||||
|
||||
class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
@@ -102,6 +102,7 @@ class BIMObjectAggregateProperties(PropertyGroup):
|
||||
class Objects(bpy.types.PropertyGroup):
|
||||
obj: PointerProperty(type=bpy.types.Object)
|
||||
previous_display_type: bpy.props.StringProperty(default="TEXTURED")
|
||||
previous_hide_select: bpy.props.BoolProperty(default=False)
|
||||
|
||||
|
||||
class BIMAggregateProperties(PropertyGroup):
|
||||
|
||||
@@ -34,12 +34,12 @@ class BIM_PT_aggregate(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
if not (obj := context.active_object):
|
||||
return False
|
||||
props = context.active_object.BIMObjectProperties
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
if not props.ifc_definition_id:
|
||||
return False
|
||||
if not IfcStore.get_element(props.ifc_definition_id):
|
||||
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
|
||||
return False
|
||||
if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"):
|
||||
return False
|
||||
@@ -66,9 +66,9 @@ class BIM_PT_aggregate(Panel):
|
||||
col.enabled = False
|
||||
op = col.operator("bim.aggregate_assign_object", icon="CHECKMARK")
|
||||
if props.relating_object:
|
||||
op.relating_object = props.relating_object.BIMObjectProperties.ifc_definition_id
|
||||
op.relating_object = tool.Blender.get_object_bim_props(props.relating_object).ifc_definition_id
|
||||
elif props.related_object:
|
||||
op.related_object = props.related_object.BIMObjectProperties.ifc_definition_id
|
||||
op.related_object = tool.Blender.get_object_bim_props(props.related_object).ifc_definition_id
|
||||
row.operator("bim.disable_editing_aggregate", icon="CANCEL", text="")
|
||||
return
|
||||
else:
|
||||
@@ -115,14 +115,14 @@ class BIM_PT_linked_aggregate(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
if not (obj := context.active_object):
|
||||
return False
|
||||
props = context.active_object.BIMObjectProperties
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
if not props.ifc_definition_id:
|
||||
return False
|
||||
if not IfcStore.get_element(props.ifc_definition_id):
|
||||
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
|
||||
return False
|
||||
if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"):
|
||||
if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
|
||||
# from . import ui, prop, operator
|
||||
from . import operator
|
||||
|
||||
classes = (operator.ImportAlignmentCSV,)
|
||||
|
||||
|
||||
def menu_func_import(self, context):
|
||||
self.layout.operator(operator.ImportAlignmentCSV.bl_idname, text="Alignment (.csv)")
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
|
||||
@@ -0,0 +1,113 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>, 2022 Yassine Oualid <yassine@sigmadimensions.com>
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# pyright: reportUnnecessaryTypeIgnoreComment=error
|
||||
|
||||
import os
|
||||
|
||||
import ifcopenshell.api.alignment
|
||||
import ifcopenshell.api.alignment.add_stationing_to_alignment
|
||||
|
||||
import bpy
|
||||
import json
|
||||
import time
|
||||
import calendar
|
||||
import isodate
|
||||
import bonsai.core.sequence as core
|
||||
import bonsai.tool as tool
|
||||
import bonsai.bim.module.sequence.helper as helper
|
||||
import ifcopenshell.api.spatial
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.util.sequence
|
||||
import ifcopenshell.util.selector
|
||||
from datetime import datetime
|
||||
from dateutil import parser, relativedelta
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
from typing import get_args, TYPE_CHECKING
|
||||
from typing_extensions import assert_never
|
||||
|
||||
|
||||
class ImportAlignmentCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
bl_idname = "bim.import_alignment_csv"
|
||||
bl_label = "Import Alignment CSV"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filename_ext = ".csv"
|
||||
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
ifc_file = tool.Ifc.get()
|
||||
if ifc_file is None:
|
||||
cls.poll_message_set("No IFC file is loaded.")
|
||||
return False
|
||||
elif ifc_file.schema != "IFC4X3":
|
||||
cls.poll_message_set("Schema must be IFC4x3.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
self.file = tool.Ifc.get()
|
||||
start = time.time()
|
||||
alignment = ifcopenshell.api.alignment.create_alignment_from_csv(self.file, self.filepath)
|
||||
ifcopenshell.api.alignment.create_geometric_representation(self.file, alignment)
|
||||
ifcopenshell.api.alignment.add_stationing_to_alignment(self.file, alignment=alignment, start_station=0.0)
|
||||
|
||||
# IFC 4.1.5.1 alignments cannot be contained in spatial structures, but can be referenced into them
|
||||
sites = self.file.by_type("IfcSite")
|
||||
for site in sites:
|
||||
ifcopenshell.api.spatial.reference_structure(self.file, products=[alignment], relating_structure=site)
|
||||
|
||||
# process the generated IfcReferent for the alignment
|
||||
for rel in alignment.IsNestedBy:
|
||||
for referent in rel.RelatedObjects:
|
||||
if referent.is_a("IfcReferent"):
|
||||
referent_obj = bpy.data.objects.new(tool.Loader.get_name(referent), None)
|
||||
tool.Geometry.link(referent, referent_obj)
|
||||
tool.Collector.assign(referent_obj, should_clean_users_collection=False)
|
||||
|
||||
# an alignment can be an aggregation of multiple child alignments (ie. multiple verticals for a single horizontal)
|
||||
# get all the alignment curves
|
||||
curves = []
|
||||
for rel in alignment.IsDecomposedBy:
|
||||
for agg in rel.RelatedObjects:
|
||||
if agg.is_a("IfcAlignment"):
|
||||
curves.append(ifcopenshell.api.alignment.get_curve(agg)) # 3D curve
|
||||
|
||||
# if there aren't any curves from aggregation, then there is only a single vertical or no vertical
|
||||
if len(curves) == 0:
|
||||
curves.append(ifcopenshell.api.alignment.get_curve(alignment))
|
||||
|
||||
settings = ifcopenshell.geom.settings()
|
||||
for curve in curves:
|
||||
shape = ifcopenshell.geom.create_shape(settings, curve)
|
||||
|
||||
# create a new Blender mesh
|
||||
mesh_name = tool.Loader.get_mesh_name_from_shape(shape)
|
||||
mesh = bpy.data.meshes.new(mesh_name)
|
||||
m = tool.Loader.convert_geometry_to_mesh(shape, mesh)
|
||||
|
||||
# create a new Blender object
|
||||
alignment_obj = bpy.data.objects.new(tool.Loader.get_name(alignment), m)
|
||||
|
||||
# link the blender object to with the alignment element
|
||||
tool.Geometry.link(alignment, alignment_obj)
|
||||
|
||||
# assign the object to the blender collections
|
||||
tool.Collector.assign(alignment_obj, should_clean_users_collection=False)
|
||||
|
||||
self.report({"INFO"}, "Imported in %s seconds" % (time.time() - start))
|
||||
@@ -26,7 +26,6 @@ import bonsai.bim.helper
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.attribute as core
|
||||
import bonsai.core.spatial
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
|
||||
|
||||
def get_objs_for_operation(operator_properties, context):
|
||||
@@ -117,7 +116,7 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
self.file = tool.Ifc.get()
|
||||
obj = tool.Blender.get_active_object(is_selected=False)
|
||||
if not (element := tool.Ifc.get_entity(obj)):
|
||||
return
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.prop import StrProperty, Attribute
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
import bonsai.bim.helper
|
||||
from bpy.types import Panel
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.module.attribute.data import AttributesData
|
||||
import bonsai.tool as tool
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ class AuginCreateNewModel(bpy.types.Operator):
|
||||
context.scene.collection.objects.link(cam_obj)
|
||||
context.scene.camera = cam_obj
|
||||
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
tmpdir = tempfile.mkdtemp(dir=tool.Blender.get_addon_preferences().tmp_dir or None)
|
||||
thumb_path = os.path.join(tmpdir, "thumb.png")
|
||||
context.scene.render.image_settings.file_format = "PNG"
|
||||
context.scene.render.filepath = thumb_path
|
||||
@@ -122,15 +122,16 @@ class AuginCreateNewModel(bpy.types.Operator):
|
||||
context.scene.render.image_settings.file_format = old_file_format
|
||||
context.scene.render.filepath = old_filepath
|
||||
|
||||
client.upload_file(context.scene.BIMProperties.ifc_file, result["s3_bucket"], result["model_path"])
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
client.upload_file(bim_props.ifc_file, result["s3_bucket"], result["model_path"])
|
||||
client.upload_file(thumb_path, result["s3_bucket"], result["thumb_path"])
|
||||
|
||||
# Notify done
|
||||
url = "https://server.auge.pro.br/API/v3/augin_rest.php/files_uploaded"
|
||||
payload = {
|
||||
"user_token": props.token,
|
||||
"ifc_filesize": os.path.getsize(context.scene.BIMProperties.ifc_file),
|
||||
"model_filesize": os.path.getsize(context.scene.BIMProperties.ifc_file),
|
||||
"ifc_filesize": os.path.getsize(bim_props.ifc_file),
|
||||
"model_filesize": os.path.getsize(bim_props.ifc_file),
|
||||
"thumb_filesize": os.path.getsize(thumb_path),
|
||||
"model_upload_path": result["model_path"],
|
||||
"thumb_upload_path": result["thumb_path"],
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy.types
|
||||
import bonsai.tool as tool
|
||||
|
||||
|
||||
class BIM_PT_augin(bpy.types.Panel):
|
||||
@@ -47,7 +48,8 @@ class BIM_PT_augin(bpy.types.Panel):
|
||||
row = layout.row()
|
||||
row.label(text="Logged in as " + props.username)
|
||||
|
||||
if not context.scene.BIMProperties.ifc_file:
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
if not bim_props.ifc_file:
|
||||
row = layout.row()
|
||||
row.label(text="No IFC Found")
|
||||
return
|
||||
|
||||
@@ -21,6 +21,7 @@ import bpy
|
||||
import bcf
|
||||
import bcf.bcfxml
|
||||
import bcf.v2.bcfxml
|
||||
import bonsai.tool as tool
|
||||
from typing import Union
|
||||
|
||||
|
||||
@@ -30,7 +31,8 @@ class BcfStore:
|
||||
@classmethod
|
||||
def get_bcfxml(cls) -> Union[bcf.bcfxml.BcfXml, None]:
|
||||
if not cls.bcfxml:
|
||||
bcf_filepath = bpy.context.scene.BCFProperties.bcf_file
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
bcf_filepath = props.bcf_file
|
||||
if not os.path.isabs(bcf_filepath):
|
||||
bcf_filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), bcf_filepath))
|
||||
if bcf_filepath:
|
||||
@@ -46,7 +48,7 @@ class BcfStore:
|
||||
@classmethod
|
||||
def set(cls, bcfxml: Union[bcf.bcfxml.BcfXml, None], filepath: str) -> None:
|
||||
cls.bcfxml = bcfxml
|
||||
props = bpy.context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
props.bcf_file = filepath
|
||||
|
||||
# Set bcf_version prop on load.
|
||||
|
||||
@@ -42,8 +42,8 @@ import ifcopenshell.util.unit
|
||||
import bonsai.tool as tool
|
||||
import bonsai.bim.module.bcf.prop as bcf_prop
|
||||
import bonsai.bim.module.bcf.bcfstore as bcfstore
|
||||
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
||||
from pathlib import Path
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from math import radians, degrees, atan, tan, cos, sin
|
||||
from mathutils import Vector, Matrix, Euler, geometry
|
||||
from xsdata.models.datatype import XmlDateTime
|
||||
@@ -55,7 +55,7 @@ class NewBcfProject(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
bcf_v2 = props.bcf_version == "2"
|
||||
bcf_class = bcf.v2.bcfxml.BcfXml if bcf_v2 else bcf.v3.bcfxml.BcfXml
|
||||
bcfxml = bcf_class.create_new("New Project")
|
||||
@@ -64,12 +64,14 @@ class NewBcfProject(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class LoadBcfProject(bpy.types.Operator):
|
||||
class LoadBcfProject(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.load_bcf_project"
|
||||
bl_label = "Load BCF Project"
|
||||
bl_description = "Load the BCF file."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
|
||||
filter_glob: bpy.props.StringProperty(default="*.bcf;*.bcfzip", options={"HIDDEN"})
|
||||
filename_ext = ".bcf"
|
||||
|
||||
def execute(self, context):
|
||||
# Operator is also used when new project is created by not yet saved.
|
||||
@@ -103,15 +105,12 @@ class LoadBcfProject(bpy.types.Operator):
|
||||
assert bcfxml.project
|
||||
if bcfxml.project.name is None:
|
||||
bcfxml.project.name = nameless
|
||||
context.scene.BCFProperties.name = bcfxml.project.name
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
props.name = bcfxml.project.name
|
||||
bpy.ops.bim.load_bcf_topics()
|
||||
self.report({"INFO"}, f"BCF Project '{Path(self.filepath).name}' is loaded.")
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class UnloadBcfProject(bpy.types.Operator):
|
||||
bl_idname = "bim.unload_bcf_project"
|
||||
@@ -131,7 +130,7 @@ class LoadBcfTopics(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
assert bcfxml
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
props.topics.clear()
|
||||
# workaround, one non standard topic would break reading entire bcf
|
||||
# ignored these topics ATM
|
||||
@@ -163,7 +162,8 @@ class LoadBcfTopic(bpy.types.Operator):
|
||||
assert bcfxml
|
||||
topic = bcfxml.topics[self.topic_guid]
|
||||
bcfxml.get_header(self.topic_guid)
|
||||
new = context.scene.BCFProperties.topics[self.topic_index]
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
new = props.topics[self.topic_index]
|
||||
data_map = {
|
||||
"name": topic.guid,
|
||||
"title": topic.topic.title,
|
||||
@@ -244,7 +244,8 @@ class LoadBcfComments(bpy.types.Operator):
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
assert bcfxml
|
||||
|
||||
blender_topic = context.scene.BCFProperties.topics.get(self.topic_guid)
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
blender_topic = props.topics.get(self.topic_guid)
|
||||
blender_topic.comments.clear()
|
||||
for comment in bcfxml.topics[self.topic_guid].comments:
|
||||
new = blender_topic.comments.add()
|
||||
@@ -274,7 +275,9 @@ class EditBcfProjectName(bpy.types.Operator):
|
||||
|
||||
# Bonsai creates default project on load.
|
||||
assert bcfxml.project
|
||||
bcfxml.project.name = context.scene.BCFProperties.name
|
||||
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
bcfxml.project.name = props.name
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -284,7 +287,7 @@ class EditBcfTopicName(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
blender_topic = props.active_topic
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
assert bcfxml
|
||||
@@ -300,7 +303,7 @@ class EditBcfTopic(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
blender_topic = props.active_topic
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
assert bcfxml
|
||||
@@ -337,13 +340,15 @@ class EditBcfTopic(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SaveBcfProject(bpy.types.Operator):
|
||||
class SaveBcfProject(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "bim.save_bcf_project"
|
||||
bl_label = "Save BCF Project"
|
||||
bl_description = "Save active BCF project by the provided filepath."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.bcf;*.bcfzip", options={"HIDDEN"})
|
||||
save_current_bcf: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
filename_ext = ".bcf"
|
||||
|
||||
def execute(self, context):
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
@@ -360,8 +365,7 @@ class SaveBcfProject(bpy.types.Operator):
|
||||
self.filepath = str(path)
|
||||
return self.execute(context)
|
||||
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
return ExportHelper.invoke(self, context, event)
|
||||
|
||||
|
||||
class AddBcfTopic(bpy.types.Operator):
|
||||
@@ -371,13 +375,15 @@ class AddBcfTopic(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.scene.BCFProperties.author
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
return props.author
|
||||
|
||||
def execute(self, context):
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
assert bcfxml
|
||||
|
||||
bcfxml.add_topic("New Topic", "", context.scene.BCFProperties.author)
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
bcfxml.add_topic("New Topic", "", props.author)
|
||||
bpy.ops.bim.load_bcf_topics()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -389,7 +395,7 @@ class AddBcfBimSnippet(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
props_are_filled = all(
|
||||
(getattr(props, attr) for attr in ("bim_snippet_reference", "bim_snippet_schema", "bim_snippet_type"))
|
||||
)
|
||||
@@ -403,7 +409,7 @@ class AddBcfBimSnippet(bpy.types.Operator):
|
||||
assert bcfxml
|
||||
bcf_v2 = (bcfxml.version.version_id or "").startswith("2")
|
||||
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
blender_topic = props.active_topic
|
||||
topic = bcfxml.topics[blender_topic.name]
|
||||
is_external = "://" in props.bim_snippet_reference
|
||||
@@ -435,7 +441,7 @@ class AddBcfRelatedTopic(bpy.types.Operator):
|
||||
assert bcfxml
|
||||
bcf_v2 = (bcfxml.version.version_id or "").startswith("2")
|
||||
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
blender_topic = props.active_topic
|
||||
topic = bcfxml.topics[blender_topic.name]
|
||||
related_topics = tool.Bcf.get_topic_related_topics(topic)
|
||||
@@ -462,14 +468,15 @@ class AddBcfHeaderFile(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.scene.BCFProperties.file_reference
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
return props.file_reference
|
||||
|
||||
def execute(self, context):
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
assert bcfxml
|
||||
bcf_v2 = (bcfxml.version.version_id or "").startswith("2")
|
||||
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
blender_topic = props.active_topic
|
||||
topic = bcfxml.topics[blender_topic.name]
|
||||
|
||||
@@ -519,9 +526,10 @@ class ViewBcfTopic(bpy.types.Operator):
|
||||
topic_guid: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
for index, topic in enumerate(context.scene.BCFProperties.topics):
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
for index, topic in enumerate(props.topics):
|
||||
if topic.name.lower() == self.topic_guid.lower():
|
||||
context.scene.BCFProperties.active_topic_index = index
|
||||
props.active_topic_index = index
|
||||
break
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -546,7 +554,7 @@ class AddBcfViewpoint(bpy.types.Operator):
|
||||
blender_camera = context.scene.camera
|
||||
assert blender_camera
|
||||
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
blender_topic = props.active_topic
|
||||
topic = bcfxml.topics[blender_topic.name]
|
||||
|
||||
@@ -675,7 +683,7 @@ class RemoveBcfViewpoint(bpy.types.Operator):
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
if not bcfxml:
|
||||
return False
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
topic = props.active_topic
|
||||
if not topic:
|
||||
return False
|
||||
@@ -690,7 +698,7 @@ class RemoveBcfViewpoint(bpy.types.Operator):
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
assert bcfxml
|
||||
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
blender_topic = props.active_topic
|
||||
topic = bcfxml.topics[blender_topic.name]
|
||||
del topic.viewpoints[blender_topic.viewpoints]
|
||||
@@ -715,7 +723,7 @@ class RemoveBcfFile(bpy.types.Operator):
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
assert bcfxml
|
||||
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
blender_topic = props.active_topic
|
||||
topic = bcfxml.topics[blender_topic.name]
|
||||
header_files = tool.Bcf.get_topic_header_files(topic)
|
||||
@@ -733,13 +741,14 @@ class RemoveBcfTopic(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.scene.BCFProperties.topics
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
return props.topics
|
||||
|
||||
def execute(self, context):
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
assert bcfxml
|
||||
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
del bcfxml.topics[props.active_topic.name]
|
||||
bpy.ops.bim.load_bcf_topics()
|
||||
return {"FINISHED"}
|
||||
@@ -752,13 +761,14 @@ class AddBcfReferenceLink(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.scene.BCFProperties.reference_link
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
return bool(props.reference_link)
|
||||
|
||||
def execute(self, context):
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
assert bcfxml
|
||||
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
blender_topic = props.active_topic
|
||||
topic = bcfxml.topics[blender_topic.name]
|
||||
reference_links = tool.Bcf.get_topic_reference_links(topic)
|
||||
@@ -776,14 +786,15 @@ class AddBcfDocumentReference(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.scene.BCFProperties.document_reference
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
return bool(props.document_reference)
|
||||
|
||||
def execute(self, context):
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
assert bcfxml
|
||||
|
||||
bcf_v2 = (bcfxml.version.version_id or "").startswith("2")
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
blender_topic = props.active_topic
|
||||
topic = bcfxml.topics[blender_topic.name]
|
||||
|
||||
@@ -855,13 +866,14 @@ class AddBcfLabel(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.scene.BCFProperties.label
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
return bool(props.label)
|
||||
|
||||
def execute(self, context):
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
assert bcfxml
|
||||
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
blender_topic = props.active_topic
|
||||
topic = bcfxml.topics[blender_topic.name]
|
||||
new = blender_topic.labels.add()
|
||||
@@ -883,7 +895,7 @@ class EditBcfReferenceLinks(bpy.types.Operator):
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
assert bcfxml
|
||||
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
blender_topic = props.active_topic
|
||||
topic = bcfxml.topics[blender_topic.name]
|
||||
reference_links = [r.name for r in blender_topic.reference_links]
|
||||
@@ -900,7 +912,7 @@ class EditBcfLabels(bpy.types.Operator):
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
assert bcfxml
|
||||
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
blender_topic = props.active_topic
|
||||
topic = bcfxml.topics[blender_topic.name]
|
||||
labels = [l.name for l in blender_topic.labels]
|
||||
@@ -918,7 +930,7 @@ class RemoveBcfReferenceLink(bpy.types.Operator):
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
assert bcfxml
|
||||
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
blender_topic = props.active_topic
|
||||
topic = bcfxml.topics[blender_topic.name]
|
||||
reference_links = tool.Bcf.get_topic_reference_links(topic)
|
||||
@@ -938,7 +950,7 @@ class RemoveBcfLabel(bpy.types.Operator):
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
assert bcfxml
|
||||
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
blender_topic = props.active_topic
|
||||
topic = bcfxml.topics[blender_topic.name]
|
||||
labels = tool.Bcf.get_topic_labels(topic)
|
||||
@@ -957,7 +969,7 @@ class RemoveBcfBimSnippet(bpy.types.Operator):
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
assert bcfxml
|
||||
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
blender_topic = props.active_topic
|
||||
topic = bcfxml.topics[blender_topic.name]
|
||||
tool.Bcf.set_topic_bim_snippet(topic, None)
|
||||
@@ -978,7 +990,7 @@ class RemoveBcfDocumentReference(bpy.types.Operator):
|
||||
assert bcfxml
|
||||
bcf_v2 = (bcfxml.version.version_id or "").startswith("2")
|
||||
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
blender_topic = props.active_topic
|
||||
topic = bcfxml.topics[blender_topic.name]
|
||||
|
||||
@@ -1043,7 +1055,7 @@ class RemoveBcfRelatedTopic(bpy.types.Operator):
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
assert bcfxml
|
||||
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
blender_topic = props.active_topic
|
||||
topic = bcfxml.topics[blender_topic.name]
|
||||
related_topics = tool.Bcf.get_topic_related_topics(topic)
|
||||
@@ -1063,7 +1075,7 @@ class RemoveBcfComment(bpy.types.Operator):
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
assert bcfxml
|
||||
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
blender_topic = props.active_topic
|
||||
topic = bcfxml.topics[blender_topic.name]
|
||||
comments = topic.comments
|
||||
@@ -1084,7 +1096,7 @@ class EditBcfComment(bpy.types.Operator):
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
assert bcfxml
|
||||
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
blender_topic = props.active_topic
|
||||
blender_comment = blender_topic.comments.get(self.comment_guid)
|
||||
topic = bcfxml.topics[blender_topic.name]
|
||||
@@ -1092,7 +1104,7 @@ class EditBcfComment(bpy.types.Operator):
|
||||
if comment.guid == self.comment_guid:
|
||||
comment.comment = blender_comment.comment
|
||||
comment.modified_date = XmlDateTime.now()
|
||||
comment.modified_author = context.scene.BCFProperties.author
|
||||
comment.modified_author = props.author
|
||||
bpy.ops.bim.load_bcf_comments(topic_guid=topic.guid)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -1105,7 +1117,7 @@ class AddBcfComment(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
if not props.comment:
|
||||
cls.poll_message_set("No comment to add.")
|
||||
return False
|
||||
@@ -1128,7 +1140,7 @@ class AddBcfComment(bpy.types.Operator):
|
||||
assert bcfxml
|
||||
bcf_v2 = (bcfxml.version.version_id or "").startswith("2")
|
||||
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
blender_topic = props.active_topic
|
||||
topic = bcfxml.topics[blender_topic.name]
|
||||
comments = topic.comments
|
||||
@@ -1136,7 +1148,7 @@ class AddBcfComment(bpy.types.Operator):
|
||||
if bcf_v2:
|
||||
comment = bcf.v2.model.Comment(
|
||||
date=XmlDateTime.now(),
|
||||
author=context.scene.BCFProperties.author,
|
||||
author=props.author,
|
||||
comment=props.comment,
|
||||
guid=str(uuid.uuid4()),
|
||||
)
|
||||
@@ -1149,7 +1161,7 @@ class AddBcfComment(bpy.types.Operator):
|
||||
else:
|
||||
comment = bcf.v3.model.Comment(
|
||||
date=XmlDateTime.now(),
|
||||
author=context.scene.BCFProperties.author,
|
||||
author=props.author,
|
||||
comment=props.comment,
|
||||
guid=str(uuid.uuid4()),
|
||||
)
|
||||
@@ -1179,7 +1191,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
blender_topic = props.active_topic
|
||||
if blender_topic is None:
|
||||
cls.poll_message_set("No topic is active.")
|
||||
@@ -1193,11 +1205,11 @@ class ActivateBcfViewpoint(bpy.types.Operator):
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
self.file = tool.Ifc.get()
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
assert bcfxml
|
||||
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
blender_topic = props.active_topic
|
||||
topic = bcfxml.topics[blender_topic.name]
|
||||
if self.viewpoint_guid:
|
||||
@@ -1303,7 +1315,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
|
||||
[0, 0, 0, 1],
|
||||
)
|
||||
)
|
||||
props = context.scene.BIMGeoreferenceProperties
|
||||
props = tool.Georeference.get_georeference_props()
|
||||
if props.has_blender_offset:
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
||||
matrix = ifcopenshell.util.geolocation.global2local(
|
||||
@@ -1342,7 +1354,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
|
||||
|
||||
objs: list[bpy.types.Object] = []
|
||||
for global_id in exception_global_ids:
|
||||
obj = IfcStore.get_element(global_id)
|
||||
obj = tool.Ifc.get_object_by_identifier(global_id)
|
||||
if obj and context.view_layer.objects.get(obj.name):
|
||||
assert isinstance(obj, bpy.types.Object)
|
||||
objs.append(obj)
|
||||
@@ -1401,7 +1413,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
|
||||
return
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
for global_id in selected_global_ids:
|
||||
obj = IfcStore.get_element(global_id)
|
||||
obj = tool.Ifc.get_object_by_identifier(global_id)
|
||||
if obj:
|
||||
obj.select_set(True)
|
||||
obj.hide_set(False)
|
||||
@@ -1414,7 +1426,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
|
||||
for acomponent in acoloring.component:
|
||||
global_id_colours.setdefault(acomponent.ifc_guid, acoloring.color)
|
||||
for global_id, color in global_id_colours.items():
|
||||
obj = IfcStore.get_element(global_id)
|
||||
obj = tool.Ifc.get_object_by_identifier(global_id)
|
||||
if obj:
|
||||
obj.color = self.hex_to_rgb(color)
|
||||
|
||||
@@ -1517,58 +1529,51 @@ class OpenBcfReferenceLink(bpy.types.Operator):
|
||||
index: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open(context.scene.BCFProperties.topic_links[self.index].name)
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
webbrowser.open(props.topic_links[self.index].name)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectBcfHeaderFile(bpy.types.Operator):
|
||||
class SelectBcfHeaderFile(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.select_bcf_header_file"
|
||||
bl_label = "Select BCF Header File"
|
||||
bl_description = "Select filepath for BCF header reference."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcjson", options={"HIDDEN"})
|
||||
filename_ext = ".ifc"
|
||||
|
||||
def execute(self, context):
|
||||
if self.filepath:
|
||||
context.scene.BCFProperties.file_reference = self.filepath
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
props.file_reference = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class SelectBcfBimSnippetReference(bpy.types.Operator):
|
||||
class SelectBcfBimSnippetReference(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.select_bcf_bim_snippet_reference"
|
||||
bl_label = "Select BCF BIM Snippet Reference"
|
||||
bl_description = "Select filepath for BCF snippet reference."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def execute(self, context):
|
||||
if self.filepath:
|
||||
context.scene.BCFProperties.bim_snippet_reference = self.filepath
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
props.bim_snippet_reference = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class SelectBcfDocumentReference(bpy.types.Operator):
|
||||
class SelectBcfDocumentReference(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.select_bcf_document_reference"
|
||||
bl_label = "Select BCF Document Reference"
|
||||
bl_description = "Select filepath for BCF document reference."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def execute(self, context):
|
||||
if self.filepath:
|
||||
context.scene.BCFProperties.document_reference = self.filepath
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
props.document_reference = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class LoadBcfHeaderIfcFile(bpy.types.Operator):
|
||||
bl_idname = "bim.load_bcf_header_ifc_file"
|
||||
@@ -1585,7 +1590,8 @@ class LoadBcfHeaderIfcFile(bpy.types.Operator):
|
||||
assert bcfxml
|
||||
|
||||
bcf_path = tool.Bcf.get_path()
|
||||
topic = bcfxml.topics[context.scene.BCFProperties.active_topic.name]
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
topic = bcfxml.topics[props.active_topic.name]
|
||||
entity = tool.Bcf.get_topic_header_files(topic)[self.index]
|
||||
ifc_path = bcf.agnostic.topic.extract_file(topic, entity)
|
||||
bpy.ops.bim.load_project(filepath=ifc_path)
|
||||
@@ -1605,7 +1611,8 @@ class ExtractBcfFile(bpy.types.Operator):
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
assert bcfxml
|
||||
|
||||
topic = bcfxml.topics[context.scene.BCFProperties.active_topic.name]
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
topic = bcfxml.topics[props.active_topic.name]
|
||||
|
||||
if self.entity_type == "HEADER_FILE":
|
||||
entity = tool.Bcf.get_topic_header_files(topic)[self.index]
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import bonsai.tool as tool
|
||||
from . import bcfstore
|
||||
from bonsai.bim.prop import StrProperty
|
||||
from bpy.types import PropertyGroup
|
||||
@@ -34,6 +35,7 @@ from functools import partial
|
||||
from typing import Literal
|
||||
from typing_extensions import assert_never
|
||||
from bcf.agnostic.extensions import get_extensions_attributes
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
|
||||
bcfviewpoints_enum = None
|
||||
@@ -95,7 +97,7 @@ def getBcfViewpoints(self, context, force_update=False):
|
||||
global bcfviewpoints_enum
|
||||
if bcfviewpoints_enum is None or force_update: # Retrieving Viewpoints is slow. Make sure we only do when needed
|
||||
bcfviewpoints_enum = []
|
||||
props = context.scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
assert bcfxml
|
||||
topic = props.active_topic
|
||||
@@ -110,6 +112,12 @@ class BcfBimSnippet(PropertyGroup):
|
||||
type: StringProperty(name="Type")
|
||||
is_external: BoolProperty(name="Is External")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
schema: str
|
||||
reference: str
|
||||
type: str
|
||||
is_external: bool
|
||||
|
||||
|
||||
class BcfDocumentReference(PropertyGroup):
|
||||
reference: StringProperty(name="Reference")
|
||||
@@ -117,6 +125,12 @@ class BcfDocumentReference(PropertyGroup):
|
||||
guid: StringProperty(name="GUID")
|
||||
is_external: BoolProperty(name="Is External")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
reference: str
|
||||
description: str
|
||||
guid: str
|
||||
is_external: bool
|
||||
|
||||
|
||||
class BcfComment(PropertyGroup):
|
||||
name: StringProperty(name="GUID")
|
||||
@@ -128,6 +142,16 @@ class BcfComment(PropertyGroup):
|
||||
modified_author: StringProperty(name="Modified Author")
|
||||
is_editable: BoolProperty(name="Is Editable", default=False, update=updateBcfCommentIsEditable)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
name: str
|
||||
date: str
|
||||
author: str
|
||||
comment: str
|
||||
viewpoint: str
|
||||
modified_date: str
|
||||
modified_author: str
|
||||
is_editable: bool
|
||||
|
||||
|
||||
def get_extensions_items(
|
||||
self: "BCFProperties", context: bpy.types.Context, edit_text: str, extensions_attr: str
|
||||
@@ -183,6 +207,30 @@ class BcfTopic(PropertyGroup):
|
||||
comments: CollectionProperty(name="Comments", type=BcfComment)
|
||||
is_editable: BoolProperty(name="Edit Topic Attributes", default=False, update=updateBcfTopicIsEditable)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
name: str
|
||||
title: str
|
||||
type: str
|
||||
status: str
|
||||
priority: str
|
||||
stage: str
|
||||
creation_date: str
|
||||
creation_author: str
|
||||
modified_date: str
|
||||
modified_author: str
|
||||
assigned_to: str
|
||||
due_date: str
|
||||
description: str
|
||||
viewpoints: str
|
||||
files: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
reference_links: bpy.types.bpy_prop_collection_idprop[BcfReferenceLink]
|
||||
labels: bpy.types.bpy_prop_collection_idprop[BcfLabel]
|
||||
bim_snippet: BcfBimSnippet
|
||||
document_references: bpy.types.bpy_prop_collection_idprop[BcfDocumentReference]
|
||||
related_topics: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
comments: bpy.types.bpy_prop_collection_idprop[BcfComment]
|
||||
is_editable: bool
|
||||
|
||||
|
||||
def get_related_topics(self: "BCFProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
|
||||
global RELATED_TOPICS_ENUM_ITEMS
|
||||
@@ -235,7 +283,30 @@ class BCFProperties(PropertyGroup):
|
||||
comment: StringProperty(default="", name="Comment")
|
||||
has_related_viewpoint: BoolProperty(name="Has Related Viewpoint", default=False)
|
||||
|
||||
def clear_input_fields(self):
|
||||
if TYPE_CHECKING:
|
||||
bcf_file: str
|
||||
bcf_version: str
|
||||
comment_text_width: int
|
||||
name: str
|
||||
author: str
|
||||
topics: bpy.types.bpy_prop_collection_idprop[BcfTopic]
|
||||
active_topic_index: int
|
||||
file_reference: str
|
||||
file_ifc_project: str
|
||||
file_ifc_spatial_structure_element: str
|
||||
reference_link: str
|
||||
label: str
|
||||
bim_snippet_reference: str
|
||||
bim_snippet_type: str
|
||||
bim_snippet_schema: str
|
||||
document_reference: str
|
||||
document_reference_description: str
|
||||
document_description: str
|
||||
related_topic: str
|
||||
comment: str
|
||||
has_related_viewpoint: bool
|
||||
|
||||
def clear_input_fields(self) -> None:
|
||||
self.file_reference = ""
|
||||
self.file_ifc_project = ""
|
||||
self.file_ifc_spatial_structure_element = ""
|
||||
@@ -250,7 +321,7 @@ class BCFProperties(PropertyGroup):
|
||||
self.has_related_viewpoint = False
|
||||
|
||||
@property
|
||||
def active_topic(self):
|
||||
def active_topic(self) -> Union[BcfTopic, None]:
|
||||
if len(self.topics) == 0:
|
||||
return None
|
||||
if self.active_topic_index < 0:
|
||||
@@ -259,5 +330,5 @@ class BCFProperties(PropertyGroup):
|
||||
self.active_topic_index = len(self.topics) - 1
|
||||
return self.topics[self.active_topic_index]
|
||||
|
||||
def refresh_topic(self, context):
|
||||
def refresh_topic(self, context: bpy.types.Context) -> None:
|
||||
refreshBcfTopic(self, context)
|
||||
|
||||
@@ -37,8 +37,7 @@ class BIM_PT_bcf(Panel):
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
scene = context.scene
|
||||
props = scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
|
||||
if not bcfstore.BcfStore.get_bcfxml():
|
||||
row = layout.row(align=True)
|
||||
@@ -63,18 +62,17 @@ class BIM_PT_bcf(Panel):
|
||||
row = layout.row()
|
||||
row.prop(props, "author")
|
||||
|
||||
props = context.scene.BCFProperties
|
||||
row = layout.row()
|
||||
row.template_list("BIM_UL_topics", "", props, "topics", props, "active_topic_index")
|
||||
col = row.column(align=True)
|
||||
col.operator("bim.add_bcf_topic", icon="ADD", text="")
|
||||
col.operator("bim.remove_bcf_topic", icon="REMOVE", text="")
|
||||
if props.active_topic_index < len(props.topics):
|
||||
topic = props.active_topic
|
||||
|
||||
topic = props.active_topic
|
||||
if topic is not None:
|
||||
is_editable = topic.is_editable
|
||||
col.prop(topic, "is_editable", icon="CHECKMARK" if topic.is_editable else "GREASEPENCIL", icon_only=True)
|
||||
|
||||
topic = props.active_topic
|
||||
row = layout.row()
|
||||
row.enabled = is_editable
|
||||
row.prop(topic, "description", text="")
|
||||
@@ -131,8 +129,7 @@ class BIM_PT_bcf_metadata(Panel):
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
scene = context.scene
|
||||
props = scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
if not bcfxml or props.active_topic_index >= len(props.topics):
|
||||
@@ -296,8 +293,7 @@ class BIM_PT_bcf_comments(Panel):
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
scene = context.scene
|
||||
props = scene.BCFProperties
|
||||
props = tool.Bcf.get_bcf_props()
|
||||
|
||||
if props.active_topic_index >= len(props.topics):
|
||||
layout.label(text="No BCF project is loaded")
|
||||
|
||||
@@ -43,6 +43,8 @@ class BoundaryDecorator:
|
||||
cls.installed = None
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
|
||||
@@ -25,6 +25,8 @@ import mathutils
|
||||
import numpy as np
|
||||
import multiprocessing
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.boundary
|
||||
import ifcopenshell.api.root
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.shape
|
||||
@@ -40,6 +42,7 @@ from bonsai.bim.module.model.decorator import ProfileDecorator
|
||||
from bonsai.bim.module.boundary.decorator import BoundaryDecorator
|
||||
import bonsai.core
|
||||
import bonsai.core.geometry
|
||||
from typing import Union, Optional
|
||||
|
||||
|
||||
def disable_editing_boundary_geometry(context):
|
||||
@@ -57,7 +60,7 @@ def disable_editing_boundary_geometry(context):
|
||||
|
||||
|
||||
class Loader:
|
||||
def __init__(self, operator=None):
|
||||
def __init__(self, operator: Optional[bpy.types.Operator] = None):
|
||||
self.operator = operator
|
||||
self.ifc_file = None
|
||||
self.logger = None
|
||||
@@ -66,17 +69,20 @@ class Loader:
|
||||
self.fallback_settings = self.load_fallback_settings()
|
||||
self.load_importer()
|
||||
|
||||
def create_mesh(self, boundary):
|
||||
def create_mesh(self, boundary: ifcopenshell.entity_instance) -> Union[bpy.types.Mesh, None]:
|
||||
# ConnectionGeometry is optional in IFC schema for some reasons.
|
||||
if not boundary.ConnectionGeometry:
|
||||
return None
|
||||
assert self.ifc_importer
|
||||
assert self.operator
|
||||
surface = boundary.ConnectionGeometry.SurfaceOnRelatingElement
|
||||
# Workaround for invalid geometry provided by Revit. See https://github.com/Autodesk/revit-ifc/issues/270
|
||||
if surface.is_a("IfcCurveBoundedPlane") and not getattr(surface, "InnerBoundaries", None):
|
||||
surface.InnerBoundaries = ()
|
||||
try:
|
||||
shape = ifcopenshell.geom.create_shape(self.settings, surface)
|
||||
mesh = self.ifc_importer.create_mesh(None, shape)
|
||||
mesh = self.ifc_importer.create_mesh(boundary, shape)
|
||||
assert mesh
|
||||
tool.Loader.link_mesh(shape, mesh)
|
||||
except RuntimeError:
|
||||
# Fallback solution for invalid geometry provided by Revit. (InnerBoundaries cuting OuterBoundary)
|
||||
@@ -96,7 +102,8 @@ class Loader:
|
||||
return None
|
||||
mesh = bpy.data.meshes.new(str(surface.id()))
|
||||
bm = bmesh.new()
|
||||
verts = [bm.verts.new(shape.verts[i : i + 3]) for i in range(0, len(shape.verts), 3)]
|
||||
coords = ifcopenshell.util.shape.get_vertices(shape)
|
||||
verts = [bm.verts.new(coord) for coord in coords]
|
||||
bm.faces.new(verts)
|
||||
for inner_boundary in surface.InnerBoundaries:
|
||||
try:
|
||||
@@ -107,13 +114,15 @@ class Loader:
|
||||
f"Skipping an inner boundary for IfcRelSpaceBoundary with ID {boundary.id()}. Geometry might be invalid",
|
||||
)
|
||||
return None
|
||||
verts = [bm.verts.new(shape.verts[i : i + 3]) for i in range(0, len(shape.verts), 3)]
|
||||
coords = ifcopenshell.util.shape.get_vertices(shape)
|
||||
verts = [bm.verts.new(coord) for coord in coords]
|
||||
bm.faces.new(verts)
|
||||
for i in range(len(verts) - 1):
|
||||
bm.edges.new(verts[i : i + 2])
|
||||
bm.edges.new((verts[-1], verts[0]))
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
mesh.BIMMeshProperties.ifc_definition_id = surface.id()
|
||||
tool.Ifc.link(surface, mesh)
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
matrix = mathutils.Matrix(
|
||||
ifcopenshell.util.placement.get_axis2placement(surface.BasisSurface.Position).tolist()
|
||||
@@ -130,7 +139,7 @@ class Loader:
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
return settings
|
||||
|
||||
def load_importer(self):
|
||||
def load_importer(self) -> None:
|
||||
self.ifc_file = tool.Ifc.get()
|
||||
self.logger = logging.getLogger("ImportIFC")
|
||||
ifc_import_settings = import_ifc.IfcImportSettings.factory(bpy.context, IfcStore.path, self.logger)
|
||||
@@ -279,7 +288,7 @@ class SelectProjectBoundaries(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def get_colour(ifc_boundary):
|
||||
def get_colour(ifc_boundary: ifcopenshell.entity_instance) -> tuple[float, float, float, float]:
|
||||
"""Return a color depending on IfcClass given"""
|
||||
product_colors = {
|
||||
"IfcWall": (0.7, 0.3, 0, 1),
|
||||
@@ -315,7 +324,8 @@ class ColourByRelatedBuildingElement(bpy.types.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
for obj in context.visible_objects:
|
||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
if not props.ifc_definition_id:
|
||||
continue
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element.is_a("IfcRelSpaceBoundary"):
|
||||
@@ -416,7 +426,7 @@ class UpdateBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def _execute(self, context):
|
||||
tool.Boundary.move_origin_to_space_origin(context.active_object)
|
||||
settings = tool.Boundary.get_assign_connection_geometry_settings(context.active_object)
|
||||
ifcopenshell.api.run("boundary.assign_connection_geometry", tool.Ifc.get(), **settings)
|
||||
ifcopenshell.api.boundary.assign_connection_geometry(tool.Ifc.get(), **settings)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -582,47 +592,70 @@ class DecorateBoundaries(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_boundary"
|
||||
bl_label = "Add Boundary"
|
||||
bl_description = (
|
||||
"There are two options for this operator:\n"
|
||||
"- just 1 IfcSpace is selected - generate a boundary based on "
|
||||
"potential nearby (0.1m) boundary elements.\n"
|
||||
"- 1 IfcSpace and 1 IfcElement are selected - generate a boundary "
|
||||
"for IfcSpace and use selected IfcElement as boundary's building element."
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
relating_space = None
|
||||
related_building_element = None
|
||||
relating_space_obj = None
|
||||
related_building_element_obj = None
|
||||
parent_boundaries = []
|
||||
parent_boundaries: list[ifcopenshell.entity_instance] = []
|
||||
|
||||
objs = context.selected_objects
|
||||
objs = tool.Blender.get_selected_objects()
|
||||
ifc_objects = {element: obj for obj in objs if (element := tool.Ifc.get_entity(obj))}
|
||||
if len(ifc_objects) not in (1, 2):
|
||||
self.report({"ERROR"}, "Please select one or two IFC elements.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
if not any((element := tool.Ifc.get_entity(obj)) and element.is_a("IfcSpace") for obj in objs):
|
||||
self.report({"INFO"}, "No IfcSpace elements selected.")
|
||||
return {"FINISHED"}
|
||||
if not any(element.is_a("IfcSpace") for element in ifc_objects):
|
||||
self.report({"ERROR"}, "One of the selected objects must be an IfcSpace.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
if len(objs) == 2:
|
||||
# The user may select two objects, a space and its related building element
|
||||
for obj in objs:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
if element.is_a("IfcSpace"):
|
||||
relating_space = element
|
||||
relating_space_obj = obj
|
||||
else:
|
||||
related_building_element = element
|
||||
related_building_element_obj = obj
|
||||
# Validate input.
|
||||
space = next((e for e in ifc_objects.keys() if e.is_a("IfcSpace")), None)
|
||||
if not space:
|
||||
self.report({"ERROR"}, "One of the selected objects must be an IfcSpace.")
|
||||
return {"CANCELLED"}
|
||||
element = next((e for e in ifc_objects.keys() if e.is_a("IfcElement")), None)
|
||||
if not element:
|
||||
self.report({"ERROR"}, "One of the selected objects must be an IfcElement.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
parent_boundary = self.create_element_boundary(
|
||||
context, relating_space, relating_space_obj, related_building_element, related_building_element_obj
|
||||
context, space, ifc_objects[space], element, ifc_objects[element]
|
||||
)
|
||||
if parent_boundary:
|
||||
parent_boundaries.append(parent_boundary)
|
||||
|
||||
if not parent_boundary:
|
||||
self.report({"ERROR"}, "Failed to create a boundary.")
|
||||
return {"FINISHED"}
|
||||
parent_boundaries.append(parent_boundary)
|
||||
|
||||
elif len(objs) == 1:
|
||||
space = next(iter(ifc_objects))
|
||||
if not space.is_a("IfcSpace"):
|
||||
self.report({"ERROR"}, "1 element selected but it's not an IfcSpace - please, select IfcSpace..")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# New prototype, old code not yet removed. Still testing.
|
||||
space = tool.Ifc.get_entity(objs[0])
|
||||
if space.is_a("IfcSpace"):
|
||||
self.auto_generate_boundaries(space, objs[0])
|
||||
elif len(objs) == 1:
|
||||
res = self.auto_generate_boundaries(space, ifc_objects[space])
|
||||
if isinstance(res, str):
|
||||
self.report({"ERROR"}, res)
|
||||
return {"FINISHED"}
|
||||
parent_boundaries.extend(res)
|
||||
|
||||
elif len(objs) == 1 and False:
|
||||
# Optionally the user may select just the space, and the building element shall be auto-detected
|
||||
# TODO : refactor to be able to generate all boundaries for selected space automatically or with an option
|
||||
|
||||
relating_space = None
|
||||
related_building_element = None
|
||||
relating_space_obj = None
|
||||
related_building_element_obj = None
|
||||
|
||||
def msg(self, context):
|
||||
self.layout.label(text="Please set an active container to detect space boundaries from.")
|
||||
|
||||
@@ -659,7 +692,17 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
obj = tool.Ifc.get_object(parent_boundary)
|
||||
obj.select_set(True)
|
||||
|
||||
def auto_generate_boundaries(self, space, space_obj):
|
||||
def auto_generate_boundaries(
|
||||
self, space: ifcopenshell.entity_instance, space_obj: bpy.types.Object
|
||||
) -> Union[str, list[ifcopenshell.entity_instance]]:
|
||||
"""
|
||||
:return: list of created boundaries or a string with error description.
|
||||
"""
|
||||
ifc_file = tool.Ifc.get()
|
||||
props = tool.Model.get_model_props()
|
||||
boundaries: list[ifcopenshell.entity_instance] = []
|
||||
assert isinstance(space_obj.data, bpy.types.Mesh)
|
||||
|
||||
# Identify all potential building elements
|
||||
# TODO: don't select everything, use AABB culling in Blender
|
||||
building_elements = (
|
||||
@@ -679,7 +722,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
shapes = {}
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("disable-opening-subtractions", True)
|
||||
iterator = ifcopenshell.geom.iterator(settings, tool.Ifc.get(), multiprocessing.cpu_count(), include=include)
|
||||
iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count(), include=include)
|
||||
if iterator.initialize():
|
||||
while True:
|
||||
tree.add_element(iterator.get_native())
|
||||
@@ -692,12 +735,12 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
building_elements = [e for e in tree.select(space, extend=0.1) if e != space]
|
||||
|
||||
if not building_elements:
|
||||
return
|
||||
return "No building elements found to create boundaries."
|
||||
|
||||
# Create a dissolved bmesh for the space
|
||||
space_bm = bmesh.new()
|
||||
space_bm.from_mesh(space_obj.data)
|
||||
bmesh.ops.dissolve_limit(space_bm, angle_limit=pi * 2 / 360, verts=space_bm.verts, edges=space_bm.edges)
|
||||
bmesh.ops.dissolve_limit(space_bm, angle_limit=pi * 2 / 360, verts=space_bm.verts[:], edges=space_bm.edges[:])
|
||||
|
||||
# Create dissolved bmeshes for all boundary elements
|
||||
building_element_bms = {}
|
||||
@@ -775,9 +818,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# we cheat by using the exterior boundary to mean "gross".
|
||||
exterior_boundary_polygon = shapely.Polygon(gross_boundary_polygon.exterior.coords)
|
||||
|
||||
parent_boundary = tool.Ifc.run(
|
||||
"root.create_entity", ifc_class=bpy.context.scene.BIMModelProperties.boundary_class
|
||||
)
|
||||
parent_boundary = ifcopenshell.api.root.create_entity(ifc_file, ifc_class=props.boundary_class)
|
||||
if building_element.is_a("IfcVirtualElement"):
|
||||
parent_boundary.PhysicalOrVirtualBoundary = "VIRTUAL"
|
||||
else:
|
||||
@@ -809,6 +850,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
exterior_boundary_polygon, space_face_matrix
|
||||
)
|
||||
self.set_boundary_name(parent_boundary)
|
||||
boundaries.append(parent_boundary)
|
||||
|
||||
for rel in getattr(building_element, "HasOpenings", []):
|
||||
opening = rel.RelatedOpeningElement
|
||||
@@ -854,9 +896,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if opening_polygon.intersection(exterior_boundary_polygon).area == 0:
|
||||
continue
|
||||
|
||||
boundary = tool.Ifc.run(
|
||||
"root.create_entity", ifc_class=bpy.context.scene.BIMModelProperties.boundary_class
|
||||
)
|
||||
boundary = ifcopenshell.api.root.create_entity(ifc_file, ifc_class=props.boundary_class)
|
||||
boundary.RelatingSpace = space
|
||||
boundary.RelatedBuildingElement = filling or opening
|
||||
boundary.ConnectionGeometry = self.create_connection_geometry_from_polygon(
|
||||
@@ -870,28 +910,39 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if boundary.is_a() != "IfcRelSpaceBoundary":
|
||||
boundary.ParentBoundary = parent_boundary
|
||||
self.set_boundary_name(boundary)
|
||||
boundaries.append(boundary)
|
||||
|
||||
return boundaries
|
||||
|
||||
def create_element_boundary(
|
||||
self, context, relating_space, relating_space_obj, related_building_element, related_building_element_obj
|
||||
):
|
||||
if not relating_space or not related_building_element:
|
||||
return
|
||||
self,
|
||||
context: bpy.types.Context,
|
||||
space: ifcopenshell.entity_instance,
|
||||
space_obj: bpy.types.Object,
|
||||
building_element: ifcopenshell.entity_instance,
|
||||
building_element_obj: bpy.types.Object,
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
"""Create element boundary for IfcSpace and IfcElement as building element.
|
||||
|
||||
:return: IfcRelSpaceBoundary or ``None`` if couldn't find a face on ``space_obj``
|
||||
that would be close enough to ``related_building_element_obj``.
|
||||
"""
|
||||
props = tool.Model.get_model_props()
|
||||
# Find which face on space should be bounded to related building element
|
||||
# TODO: Handle round wall where multiple faces need to be bound to the same related building element
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(relating_space_obj.data)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi * 2 / 360, verts=bm.verts, edges=bm.edges)
|
||||
space_bm = bmesh.new()
|
||||
assert isinstance(space_obj.data, bpy.types.Mesh)
|
||||
space_bm.from_mesh(space_obj.data)
|
||||
bmesh.ops.dissolve_limit(space_bm, angle_limit=pi * 2 / 360, verts=space_bm.verts[:], edges=space_bm.edges[:])
|
||||
|
||||
target_distance = inf
|
||||
target_face = None
|
||||
for face in bm.faces:
|
||||
centroid = relating_space_obj.matrix_world @ face.calc_center_median()
|
||||
raycast = related_building_element_obj.closest_point_on_mesh(
|
||||
related_building_element_obj.matrix_world.inverted() @ centroid, distance=1
|
||||
)
|
||||
to_building_obj_space = building_element_obj.matrix_world.inverted()
|
||||
for face in space_bm.faces:
|
||||
centroid = space_obj.matrix_world @ face.calc_center_median()
|
||||
raycast = building_element_obj.closest_point_on_mesh(to_building_obj_space @ centroid, distance=1)
|
||||
if raycast[0]:
|
||||
distance = (related_building_element_obj.matrix_world @ raycast[1] - centroid).length
|
||||
distance = (building_element_obj.matrix_world @ raycast[1] - centroid).length
|
||||
if distance < target_distance:
|
||||
target_face = face
|
||||
target_distance = distance
|
||||
@@ -906,9 +957,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
target_face_polygon = shapely.Polygon([tuple((target_face_matrix_i @ v).xy) for v in target_face_verts])
|
||||
|
||||
related_building_element_polygon = self.get_flattened_polygon(
|
||||
related_building_element, relating_space_obj, target_face_matrix_i
|
||||
)
|
||||
related_building_element_polygon = self.get_flattened_polygon(building_element, space_obj, target_face_matrix_i)
|
||||
|
||||
gross_boundary_polygon = target_face_polygon.intersection(related_building_element_polygon)
|
||||
|
||||
@@ -924,7 +973,8 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
):
|
||||
return
|
||||
|
||||
parent_boundary = tool.Ifc.run("root.create_entity", ifc_class=context.scene.BIMModelProperties.boundary_class)
|
||||
ifc_file = tool.Ifc.get()
|
||||
parent_boundary = ifcopenshell.api.root.create_entity(ifc_file, ifc_class=props.boundary_class)
|
||||
parent_boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
|
||||
# Set to EXTERNAL by default and turn later to internal if there is a corresponding boundary relating to an
|
||||
# internal space
|
||||
@@ -937,14 +987,14 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# space.
|
||||
exterior_boundary_polygon = shapely.Polygon(gross_boundary_polygon.exterior.coords)
|
||||
|
||||
for rel in getattr(related_building_element, "HasOpenings", []):
|
||||
for rel in getattr(building_element, "HasOpenings", []):
|
||||
opening = rel.RelatedOpeningElement
|
||||
if not opening.HasFillings:
|
||||
continue
|
||||
# TODO: HasFilling is a zero to many relationship. How to handle many ?
|
||||
filling = opening.HasFillings[0].RelatedBuildingElement
|
||||
|
||||
opening_polygon = self.get_flattened_polygon(opening, relating_space_obj, target_face_matrix_i)
|
||||
opening_polygon = self.get_flattened_polygon(opening, space_obj, target_face_matrix_i)
|
||||
|
||||
# An inner boundary is supposed to overlap its parent boundary according to IFC4 documentation
|
||||
# so we extend our exterior boundary with all opening which has a filling
|
||||
@@ -959,11 +1009,11 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
continue
|
||||
|
||||
connection_geometry = self.create_connection_geometry_from_polygon(opening_polygon, target_face_matrix)
|
||||
boundary = tool.Ifc.run("root.create_entity", ifc_class=context.scene.BIMModelProperties.boundary_class)
|
||||
boundary.RelatingSpace = relating_space
|
||||
boundary = tool.Ifc.run("root.create_entity", ifc_class=props.boundary_class)
|
||||
boundary.RelatingSpace = space
|
||||
boundary.RelatedBuildingElement = filling
|
||||
boundary.ConnectionGeometry = connection_geometry
|
||||
if related_building_element.is_a("IfcVirtualElement"):
|
||||
if building_element.is_a("IfcVirtualElement"):
|
||||
boundary.PhysicalOrVirtualBoundary = "VIRTUAL"
|
||||
else:
|
||||
boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
|
||||
@@ -975,20 +1025,23 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
connection_geometry = self.create_connection_geometry_from_polygon(
|
||||
exterior_boundary_polygon, target_face_matrix
|
||||
)
|
||||
parent_boundary.RelatingSpace = relating_space
|
||||
parent_boundary.RelatedBuildingElement = related_building_element
|
||||
parent_boundary.RelatingSpace = space
|
||||
parent_boundary.RelatedBuildingElement = building_element
|
||||
parent_boundary.ConnectionGeometry = connection_geometry
|
||||
self.set_boundary_name(parent_boundary)
|
||||
return parent_boundary
|
||||
|
||||
def get_face_matrix(self, p1, p2, p3):
|
||||
def get_face_matrix(self, p1: Vector, p2: Vector, p3: Vector) -> Matrix:
|
||||
edge1 = p2 - p1
|
||||
edge2 = p3 - p1
|
||||
normal = edge1.cross(edge2)
|
||||
assert isinstance(normal, Vector)
|
||||
|
||||
z_axis = normal.normalized()
|
||||
x_axis = p2 - p1
|
||||
x_axis.normalize()
|
||||
y_axis = z_axis.cross(x_axis)
|
||||
assert isinstance(y_axis, Vector)
|
||||
|
||||
mat = Matrix()
|
||||
mat.col[0][:3] = x_axis
|
||||
@@ -1058,7 +1111,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
return surface
|
||||
|
||||
def set_boundary_name(self, boundary):
|
||||
def set_boundary_name(self, boundary: ifcopenshell.entity_instance):
|
||||
"""
|
||||
By convention 1stLevel and 2ndLevel boundary have specific name and description
|
||||
See https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRelSpaceBoundary.htm
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
import bpy
|
||||
from bpy.types import Panel, UIList
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.module.boundary.data import SpaceBoundariesData
|
||||
|
||||
@@ -34,7 +33,7 @@ class BIM_PT_SceneBoundaries(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return IfcStore.get_file()
|
||||
return tool.Ifc.get()
|
||||
|
||||
def draw(self, context):
|
||||
row = self.layout.row(align=True)
|
||||
@@ -53,20 +52,20 @@ class BIM_PT_Boundary(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
if not (obj := context.active_object):
|
||||
return False
|
||||
props = context.active_object.BIMObjectProperties
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
if not props.ifc_definition_id:
|
||||
return False
|
||||
if not IfcStore.get_element(props.ifc_definition_id):
|
||||
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
|
||||
return False
|
||||
entity = IfcStore.get_file().by_id(props.ifc_definition_id)
|
||||
entity = tool.Ifc.get().by_id(props.ifc_definition_id)
|
||||
return entity.is_a("IfcRelSpaceBoundary")
|
||||
|
||||
def draw(self, context):
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
props = obj.BIMObjectProperties
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
ifc_file = tool.Ifc.get()
|
||||
boundary = ifc_file.by_id(props.ifc_definition_id)
|
||||
self.bprops = tool.Boundary.get_object_boundary_props(obj)
|
||||
@@ -129,12 +128,12 @@ class BIM_PT_SpaceBoundaries(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
if not (obj := context.active_object):
|
||||
return False
|
||||
props = context.active_object.BIMObjectProperties
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
if not props.ifc_definition_id:
|
||||
return False
|
||||
if not IfcStore.get_element(props.ifc_definition_id):
|
||||
if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
|
||||
return False
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
for ifc_class in ("IfcSpace", "IfcExternalSpatialElement"):
|
||||
@@ -146,7 +145,9 @@ class BIM_PT_SpaceBoundaries(Panel):
|
||||
if not SpaceBoundariesData.is_loaded:
|
||||
SpaceBoundariesData.load()
|
||||
|
||||
self.props = context.active_object.BIMObjectProperties
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
self.props = tool.Blender.get_object_bim_props(obj)
|
||||
self.ifc_file = tool.Ifc.get()
|
||||
row = self.layout.row()
|
||||
row.operator("bim.load_space_boundaries")
|
||||
|
||||
@@ -52,7 +52,7 @@ class BrickschemaData:
|
||||
def active_relations(cls):
|
||||
if BrickStore.graph is None:
|
||||
return []
|
||||
props = bpy.context.scene.BIMBrickProperties
|
||||
props = tool.Brick.get_brick_props()
|
||||
try:
|
||||
brick = props.bricks[props.active_brick_index]
|
||||
except:
|
||||
|
||||
@@ -22,29 +22,26 @@ import ifcopenshell.api
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.brick as core
|
||||
import bonsai.bim.handler
|
||||
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.tool.brick import BrickStore
|
||||
|
||||
|
||||
class LoadBrickProject(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class LoadBrickProject(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
bl_idname = "bim.load_brick_project"
|
||||
bl_label = "Load Brickschema Project"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Load in a Brick project from a file"
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.ttl", options={"HIDDEN"})
|
||||
filename_ext = ".ttl"
|
||||
|
||||
def _execute(self, context):
|
||||
if os.path.exists(self.filepath) and "ttl" in os.path.splitext(self.filepath)[1].lower():
|
||||
root = context.scene.BIMBrickProperties.brick_list_root
|
||||
root = tool.Brick.get_brick_props().brick_list_root
|
||||
core.load_brick_project(tool.Brick, filepath=self.filepath, brick_root=root)
|
||||
else:
|
||||
self.report({"ERROR"}, f"Failed to load {self.filepath}")
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class ViewBrickClass(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.view_brick_class"
|
||||
@@ -111,10 +108,10 @@ class AssignBrickReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_description = "Assign the selected Ifc entity to the selected Brick entity"
|
||||
|
||||
def _execute(self, context):
|
||||
if not context.active_object:
|
||||
if not (obj := context.active_object) or not (element := tool.Ifc.get_entity(obj)):
|
||||
self.report({"ERROR"}, f"No Ifc selected")
|
||||
return
|
||||
props = context.scene.BIMBrickProperties
|
||||
props = tool.Brick.get_brick_props()
|
||||
try:
|
||||
props.bricks[props.active_brick_index]
|
||||
except:
|
||||
@@ -123,7 +120,7 @@ class AssignBrickReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
core.assign_brick_reference(
|
||||
tool.Ifc,
|
||||
tool.Brick,
|
||||
element=tool.Ifc.get_entity(context.active_object),
|
||||
element=element,
|
||||
library=tool.Ifc.get().by_id(int(props.libraries)),
|
||||
brick_uri=props.bricks[props.active_brick_index].uri,
|
||||
)
|
||||
@@ -136,7 +133,7 @@ class AddBrick(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_description = "Create the Brick entity"
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMBrickProperties
|
||||
props = tool.Brick.get_brick_props()
|
||||
core.add_brick(
|
||||
tool.Ifc,
|
||||
tool.Brick,
|
||||
@@ -155,7 +152,7 @@ class AddBrickRelation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_description = "Create the Brick relationship"
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMBrickProperties
|
||||
props = tool.Brick.get_brick_props()
|
||||
brick = props.bricks[props.active_brick_index]
|
||||
if props.new_brick_relation_type == "http://www.w3.org/2000/01/rdf-schema#label":
|
||||
object = props.new_brick_relation_object
|
||||
@@ -173,7 +170,7 @@ class ConvertIfcToBrick(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_description = "Convert Ifc entities and relations to Brick entities and relations"
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMBrickProperties
|
||||
props = tool.Brick.get_brick_props()
|
||||
library = None
|
||||
if props.libraries:
|
||||
library = tool.Ifc.get().by_id(int(props.libraries))
|
||||
@@ -201,7 +198,8 @@ class NewBrickFile(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
def _execute(self, context):
|
||||
root = context.scene.BIMBrickProperties.brick_list_root
|
||||
props = tool.Brick.get_brick_props()
|
||||
root = props.brick_list_root
|
||||
core.new_brick_file(tool.Brick, brick_root=root)
|
||||
|
||||
def rollback(self, data):
|
||||
@@ -230,7 +228,7 @@ class RemoveBrick(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_description = "Delete this entity"
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMBrickProperties
|
||||
props = tool.Brick.get_brick_props()
|
||||
core.remove_brick(
|
||||
tool.Ifc,
|
||||
tool.Brick,
|
||||
@@ -239,18 +237,20 @@ class RemoveBrick(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
|
||||
|
||||
class SerializeBrick(bpy.types.Operator):
|
||||
class SerializeBrick(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "bim.serialize_brick"
|
||||
bl_label = "Serialize Brick"
|
||||
# Prevents crash on Blender 4.4.0.
|
||||
bl_description = "Save active Brick project by the provided filepath."
|
||||
|
||||
filename_ext = ".ttl"
|
||||
filter_glob: bpy.props.StringProperty(default="*.ttl", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
should_save_as: bpy.props.BoolProperty(name="Should Save As", default=False, options={"HIDDEN"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
if self.should_save_as or not BrickStore.path:
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
return ExportHelper.invoke(self, context, event)
|
||||
else:
|
||||
return self.execute(context)
|
||||
|
||||
@@ -273,7 +273,7 @@ class AddBrickNamespace(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_description = "Bind a new namespace to the Brick project"
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMBrickProperties
|
||||
props = tool.Brick.get_brick_props()
|
||||
alias = props.new_brick_namespace_alias
|
||||
uri = props.new_brick_namespace_uri
|
||||
core.add_brick_namespace(tool.Brick, alias=alias, uri=uri)
|
||||
@@ -288,6 +288,6 @@ class RemoveBrickRelation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
object: bpy.props.StringProperty(name="Object")
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMBrickProperties
|
||||
props = tool.Brick.get_brick_props()
|
||||
brick = props.bricks[props.active_brick_index]
|
||||
core.remove_brick_relation(tool.Brick, brick_uri=brick.uri, predicate=self.predicate, object=self.object)
|
||||
|
||||
@@ -33,6 +33,7 @@ from bpy.props import (
|
||||
import bonsai.core.brick as core
|
||||
import bonsai.tool.brick as tool
|
||||
from bonsai.tool.brick import BrickStore
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
def update_active_brick_index(self, context):
|
||||
@@ -74,13 +75,13 @@ def get_brick_relations(self, context):
|
||||
return BRICK_RELATIONS_ENUM_ITEMS
|
||||
|
||||
|
||||
def update_view(self, context):
|
||||
root = context.scene.BIMBrickProperties.brick_list_root
|
||||
def update_view(self: "BIMBrickProperties", context: bpy.types.Context) -> None:
|
||||
root = self.brick_list_root
|
||||
core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=False)
|
||||
|
||||
|
||||
def split_screen_update_view(self, context):
|
||||
root = context.scene.BIMBrickProperties.split_screen_brick_list_root
|
||||
def split_screen_update_view(self: "BIMBrickProperties", context: bpy.types.Context) -> None:
|
||||
root = self.split_screen_brick_list_root
|
||||
core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=True)
|
||||
|
||||
|
||||
@@ -90,6 +91,11 @@ class Brick(PropertyGroup):
|
||||
uri: StringProperty(name="URI")
|
||||
total_items: IntProperty(name="Total Items")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
label: str
|
||||
uri: str
|
||||
total_items: int
|
||||
|
||||
|
||||
class BIMBrickProperties(PropertyGroup):
|
||||
active_brick_class: StringProperty(name="Active Brick Class")
|
||||
@@ -124,3 +130,28 @@ class BIMBrickProperties(PropertyGroup):
|
||||
split_screen_brick_list_root: EnumProperty(
|
||||
name="Split Screen Brick List Root", items=get_brick_roots, update=split_screen_update_view
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
active_brick_class: str
|
||||
brick_breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
bricks: bpy.types.bpy_prop_collection_idprop[Brick]
|
||||
active_brick_index: int
|
||||
libraries: str
|
||||
set_list_root_toggled: bool
|
||||
brick_list_root: str
|
||||
namespace: str
|
||||
new_brick_namespace_alias: str
|
||||
new_brick_namespace_uri: str
|
||||
new_brick_label: str
|
||||
brick_entity_create_type: str
|
||||
brick_entity_class: str
|
||||
brick_create_relations_toggled: bool
|
||||
brick_edit_relations_toggled: bool
|
||||
new_brick_relation_type: str
|
||||
new_brick_relation_object: str
|
||||
split_screen_toggled: bool
|
||||
split_screen_bricks: bpy.types.bpy_prop_collection_idprop[Brick]
|
||||
split_screen_active_brick_index: int
|
||||
split_screen_active_brick_class: str
|
||||
split_screen_brick_breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
split_screen_brick_list_root: str
|
||||
|
||||
@@ -45,7 +45,7 @@ class BIM_PT_brickschema_project_info(Panel):
|
||||
bl_parent_id = "BIM_PT_brickschema"
|
||||
|
||||
def draw(self, context):
|
||||
self.props = context.scene.BIMBrickProperties
|
||||
self.props = tool.Brick.get_brick_props()
|
||||
|
||||
if not BrickschemaData.data["is_loaded"]:
|
||||
row = self.layout.row(align=True)
|
||||
@@ -87,7 +87,7 @@ class BIM_PT_brickschema_namespaces(Panel):
|
||||
return BrickStore.graph != None
|
||||
|
||||
def draw(self, context):
|
||||
self.props = context.scene.BIMBrickProperties
|
||||
self.props = tool.Brick.get_brick_props()
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Active Namespace:")
|
||||
@@ -122,7 +122,7 @@ class BIM_PT_brickschema_create_entity(Panel):
|
||||
return BrickStore.graph != None
|
||||
|
||||
def draw(self, context):
|
||||
self.props = context.scene.BIMBrickProperties
|
||||
self.props = tool.Brick.get_brick_props()
|
||||
# TO DO: hide this if selected entity already has a reference, or something similar
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
@@ -156,7 +156,7 @@ class BIM_PT_brickschema_viewport(Panel):
|
||||
return BrickStore.graph != None
|
||||
|
||||
def draw(self, context):
|
||||
self.props = context.scene.BIMBrickProperties
|
||||
self.props = tool.Brick.get_brick_props()
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.column().alignment = "RIGHT"
|
||||
@@ -178,7 +178,14 @@ class BIM_PT_brickschema_viewport(Panel):
|
||||
|
||||
row = grid_left.row()
|
||||
BIM_UL_bricks.split_screen = False
|
||||
row.template_list("BIM_UL_bricks", "", self.props, "bricks", self.props, "active_brick_index")
|
||||
row.template_list(
|
||||
BIM_UL_bricks.__name__,
|
||||
BIM_UL_bricks.__name__ + "_bricks",
|
||||
self.props,
|
||||
"bricks",
|
||||
self.props,
|
||||
"active_brick_index",
|
||||
)
|
||||
|
||||
if self.props.split_screen_toggled:
|
||||
grid_right = grid.column(align=True)
|
||||
@@ -195,7 +202,12 @@ class BIM_PT_brickschema_viewport(Panel):
|
||||
row = grid_right.row()
|
||||
BIM_UL_bricks.split_screen = True
|
||||
row.template_list(
|
||||
"BIM_UL_bricks", "", self.props, "split_screen_bricks", self.props, "split_screen_active_brick_index"
|
||||
BIM_UL_bricks.__name__,
|
||||
BIM_UL_bricks.__name__ + "_split_screen_bricks",
|
||||
self.props,
|
||||
"split_screen_bricks",
|
||||
self.props,
|
||||
"split_screen_active_brick_index",
|
||||
)
|
||||
|
||||
if BrickschemaData.data["active_relations"]:
|
||||
@@ -292,7 +304,7 @@ class BIM_PT_ifc_brickschema_references(Panel):
|
||||
def draw(self, context):
|
||||
if not BrickschemaReferencesData.is_loaded:
|
||||
BrickschemaReferencesData.load()
|
||||
self.props = context.scene.BIMBrickProperties
|
||||
self.props = tool.Brick.get_brick_props()
|
||||
|
||||
if not BrickschemaReferencesData.data["is_loaded"]:
|
||||
row = self.layout.row()
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
import bonsai.tool as tool
|
||||
from bpy.types import Panel, UIList
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
|
||||
|
||||
class BIM_PT_bsdd(Panel):
|
||||
|
||||
@@ -128,9 +128,10 @@ class CadMitre(bpy.types.Operator):
|
||||
class CadFillet(bpy.types.Operator):
|
||||
bl_idname = "bim.cad_fillet"
|
||||
bl_label = "CAD Fillet"
|
||||
bl_description = "Add fillet to the 2 selected edges."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
resolution: bpy.props.IntProperty(name="Arc Resolution", min=0, default=1)
|
||||
radius: bpy.props.FloatProperty(name="Radius", default=0.1)
|
||||
radius: bpy.props.FloatProperty(name="Radius", default=0.1, subtype="DISTANCE")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -143,18 +144,15 @@ class CadFillet(bpy.types.Operator):
|
||||
layout.prop(self, prop)
|
||||
|
||||
def execute(self, context):
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
self.radius = self.radius * si_conversion
|
||||
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
obj = bpy.context.active_object
|
||||
cursor = obj.matrix_world.inverted() @ bpy.context.scene.cursor.location
|
||||
mesh = obj.data
|
||||
assert obj and isinstance((mesh := obj.data), bpy.types.Mesh)
|
||||
bm = bmesh.from_edit_mesh(mesh)
|
||||
selected_edges = [e for e in bm.edges if e.select]
|
||||
if len(selected_edges) != 2:
|
||||
self.report({"ERROR"}, "Exactly 2 edges should be selected.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Assume the user has selected two edges sharing a vert, but merge verts
|
||||
@@ -219,6 +217,7 @@ class CadFillet(bpy.types.Operator):
|
||||
class CadArcFrom2Points(bpy.types.Operator):
|
||||
bl_idname = "bim.cad_arc_from_2_points"
|
||||
bl_label = "CAD Arc from 2 Points"
|
||||
bl_description = "Add an arc to the active mesh based on 2 selected vertices."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1)
|
||||
should_flip: bpy.props.BoolProperty(name="Flip", description="Flip arc", default=False)
|
||||
@@ -235,6 +234,7 @@ class CadArcFrom2Points(bpy.types.Operator):
|
||||
if bpy.context.mode != "EDIT_MESH":
|
||||
return {"CANCELLED"}
|
||||
obj = bpy.context.active_object
|
||||
assert obj
|
||||
region = bpy.context.region
|
||||
region_3d = bpy.context.area.spaces.active.region_3d
|
||||
cursor = bpy.context.scene.cursor.location
|
||||
@@ -242,10 +242,12 @@ class CadArcFrom2Points(bpy.types.Operator):
|
||||
if not center:
|
||||
return {"CANCELLED"}
|
||||
mesh = obj.data
|
||||
assert isinstance(mesh, bpy.types.Mesh)
|
||||
mw = obj.matrix_world
|
||||
bm = bmesh.from_edit_mesh(mesh)
|
||||
selected_verts = [v for v in bm.verts if v.select]
|
||||
if len(selected_verts) != 2:
|
||||
self.report({"ERROR"}, "Exactly 2 vertices should be selected.")
|
||||
return {"CANCELLED"}
|
||||
v1 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, mw @ selected_verts[0].co)
|
||||
v2 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, mw @ selected_verts[1].co)
|
||||
@@ -342,11 +344,12 @@ class CadArcFrom3Points(bpy.types.Operator):
|
||||
class CadOffset(bpy.types.Operator):
|
||||
bl_idname = "bim.cad_offset"
|
||||
bl_label = "CAD Offset"
|
||||
bl_description = "Copy selected mesh geometry at provided offset. Mesh copied based on the current viewport angle."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
distance: bpy.props.FloatProperty(name="Distance", default=0.1)
|
||||
distance: bpy.props.FloatProperty(name="Distance", default=0.1, subtype="DISTANCE")
|
||||
|
||||
@classmethod
|
||||
def poll(self, context):
|
||||
def poll(cls, context):
|
||||
return context.mode == "EDIT_MESH"
|
||||
|
||||
def draw(self, context):
|
||||
@@ -369,9 +372,6 @@ class CadOffset(bpy.types.Operator):
|
||||
# effect of converting an unclosed loop into a closed loop which is also
|
||||
# not what users expect.
|
||||
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
self.distance = self.distance * si_conversion
|
||||
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
@@ -543,7 +543,8 @@ class CadOffset(bpy.types.Operator):
|
||||
class AddIfcCircle(bpy.types.Operator):
|
||||
bl_idname = "bim.add_ifccircle"
|
||||
bl_label = "Add IfcCircle"
|
||||
radius: bpy.props.FloatProperty(name="Radius", default=0.5)
|
||||
bl_description = "Add IfcCircle to the currently active mesh."
|
||||
radius: bpy.props.FloatProperty(name="Radius", default=0.5, subtype="DISTANCE")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -551,18 +552,19 @@ class AddIfcCircle(bpy.types.Operator):
|
||||
return bool(obj) and obj.type == "MESH"
|
||||
|
||||
def execute(self, context):
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
self.radius = self.radius * si_conversion
|
||||
|
||||
obj = context.active_object
|
||||
assert obj and isinstance((mesh := obj.data), bpy.types.Mesh)
|
||||
self.obj = obj
|
||||
self.mesh = mesh
|
||||
if self.has_selected_existing_circle(context):
|
||||
self.change_radius(context)
|
||||
else:
|
||||
self.create_circle(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
def has_selected_existing_circle(self, context):
|
||||
obj = context.active_object
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
def has_selected_existing_circle(self, context: bpy.types.Context) -> bool:
|
||||
obj = self.obj
|
||||
bm = bmesh.from_edit_mesh(self.mesh)
|
||||
verts = [v for v in bm.verts if v.select and not v.hide]
|
||||
if len(verts) != 2:
|
||||
return False
|
||||
@@ -575,32 +577,32 @@ class AddIfcCircle(bpy.types.Operator):
|
||||
for group in groups:
|
||||
if group in verts[0][deform_layer] and group in verts[1][deform_layer]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def change_radius(self, context):
|
||||
obj = context.active_object
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
def change_radius(self, context: bpy.types.Object) -> None:
|
||||
bm = bmesh.from_edit_mesh(self.mesh)
|
||||
verts = [v for v in bm.verts if v.select and not v.hide]
|
||||
center = verts[0].co.lerp(verts[1].co, 0.5)
|
||||
verts[0].co = center + ((verts[0].co - center).normalized() * self.radius)
|
||||
verts[1].co = center + ((verts[1].co - center).normalized() * self.radius)
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
bmesh.update_edit_mesh(obj.data)
|
||||
bmesh.update_edit_mesh(self.mesh)
|
||||
|
||||
def create_circle(self, context):
|
||||
obj = context.active_object
|
||||
def create_circle(self, context: bpy.types.Context) -> None:
|
||||
obj = self.obj
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
# The last group may be the result of a prior run of this operator.
|
||||
# I tried looping through all groups but Blender group indices seem to behave unpredictably.
|
||||
if len(obj.vertex_groups):
|
||||
last_group = obj.vertex_groups[-1]
|
||||
verts_in_group = [v for v in obj.data.vertices if last_group.index in [vg.group for vg in v.groups]]
|
||||
verts_in_group = [v for v in self.mesh.vertices if last_group.index in [vg.group for vg in v.groups]]
|
||||
if "IFCCIRCLE" in last_group.name and len(verts_in_group) != 2:
|
||||
obj.vertex_groups.remove(last_group)
|
||||
group = obj.vertex_groups.new(name="IFCCIRCLE")
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
bm = bmesh.from_edit_mesh(self.mesh)
|
||||
|
||||
bm.verts.layers.deform.verify()
|
||||
deform_layer = bm.verts.layers.deform.active
|
||||
@@ -617,7 +619,7 @@ class AddIfcCircle(bpy.types.Operator):
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
bmesh.update_edit_mesh(obj.data)
|
||||
bmesh.update_edit_mesh(self.mesh)
|
||||
|
||||
|
||||
class AddIfcArcIndexFillet(bpy.types.Operator):
|
||||
@@ -638,18 +640,19 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
|
||||
layout.prop(self, prop)
|
||||
|
||||
def execute(self, context):
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
self.radius = self.radius * si_conversion
|
||||
|
||||
obj = context.active_object
|
||||
assert obj and isinstance((mesh := obj.data), bpy.types.Mesh)
|
||||
self.obj = obj
|
||||
self.mesh = mesh
|
||||
if self.has_selected_existing_arc(context):
|
||||
self.change_radius(context)
|
||||
else:
|
||||
self.create_arc(context)
|
||||
return self.create_arc(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
def has_selected_existing_arc(self, context: bpy.types.Context) -> bool:
|
||||
obj = context.active_object
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
obj = self.obj
|
||||
bm = bmesh.from_edit_mesh(self.mesh)
|
||||
verts = [v for v in bm.verts if v.select and not v.hide]
|
||||
if len(verts) != 3:
|
||||
return False
|
||||
@@ -668,13 +671,14 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
|
||||
return False
|
||||
|
||||
def change_radius(self, context: bpy.types.Context) -> None:
|
||||
obj = context.active_object
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
obj = self.obj
|
||||
bm = bmesh.from_edit_mesh(self.mesh)
|
||||
edges = [e for e in bm.edges if e.select and not e.hide]
|
||||
|
||||
mid = list(set(edges[0].verts) & set(edges[1].verts))[0]
|
||||
v1 = edges[0].other_vert(mid)
|
||||
v2 = edges[1].other_vert(mid)
|
||||
assert v1 and v2
|
||||
center = tool.Cad.get_center_of_arc([v1.co, mid.co, v2.co])
|
||||
|
||||
if len(v1.link_edges) != 2 or len(v2.link_edges) != 2:
|
||||
@@ -682,6 +686,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
|
||||
|
||||
v3 = v1.link_edges[1].other_vert(v1) if mid in v1.link_edges[0].verts else v1.link_edges[0].other_vert(v1)
|
||||
v4 = v2.link_edges[1].other_vert(v2) if mid in v2.link_edges[0].verts else v2.link_edges[0].other_vert(v2)
|
||||
assert v3 and v4
|
||||
dir1 = (v3.co - v1.co).normalized()
|
||||
dir2 = (v4.co - v2.co).normalized()
|
||||
|
||||
@@ -702,11 +707,10 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
bmesh.update_edit_mesh(obj.data)
|
||||
|
||||
def create_arc(self, context):
|
||||
obj = bpy.context.active_object
|
||||
bmesh.update_edit_mesh(self.mesh)
|
||||
|
||||
def create_arc(self, context: bpy.types.Context) -> set[str]:
|
||||
obj = self.obj
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
# The last group may be the result of a prior run of this operator.
|
||||
# I tried looping through all groups but Blender group indices seem to behave unpredictably.
|
||||
@@ -718,7 +722,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
|
||||
group = obj.vertex_groups.new(name="IFCARCINDEX")
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
bm = bmesh.from_edit_mesh(self.mesh)
|
||||
|
||||
bm.verts.layers.deform.verify()
|
||||
deform_layer = bm.verts.layers.deform.active
|
||||
@@ -737,6 +741,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
|
||||
shared_vert = list(set(selected_edges[0].verts) & set(selected_edges[1].verts))[0]
|
||||
v1 = selected_edges[0].other_vert(shared_vert)
|
||||
v2 = selected_edges[1].other_vert(shared_vert)
|
||||
assert v1 and v2
|
||||
dir1 = (v1.co - shared_vert.co).normalized()
|
||||
dir2 = (v2.co - shared_vert.co).normalized()
|
||||
edge_angle = dir1.angle(dir2)
|
||||
@@ -770,7 +775,8 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
bmesh.update_edit_mesh(obj.data)
|
||||
bmesh.update_edit_mesh(self.mesh)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AlignViewToProfile(bpy.types.Operator):
|
||||
@@ -806,8 +812,9 @@ class AlignViewToProfile(bpy.types.Operator):
|
||||
class AddRectangle(bpy.types.Operator):
|
||||
bl_idname = "bim.add_rectangle"
|
||||
bl_label = "Add Rectangle"
|
||||
x: bpy.props.FloatProperty(name="X", default=1)
|
||||
y: bpy.props.FloatProperty(name="Y", default=1)
|
||||
bl_description = "Add rectangle shape to the active mesh."
|
||||
x: bpy.props.FloatProperty(name="X", default=1, subtype="DISTANCE")
|
||||
y: bpy.props.FloatProperty(name="Y", default=1, subtype="DISTANCE")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -815,11 +822,8 @@ class AddRectangle(bpy.types.Operator):
|
||||
return bool(obj) and obj.type == "MESH"
|
||||
|
||||
def execute(self, context):
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
self.x = self.x * si_conversion
|
||||
self.y = self.y * si_conversion
|
||||
|
||||
obj = context.active_object
|
||||
assert obj and isinstance(obj.data, bpy.types.Mesh)
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
cursor = obj.matrix_world.inverted() @ context.scene.cursor.location
|
||||
new_verts = [
|
||||
|
||||
@@ -20,6 +20,7 @@ import bpy
|
||||
from bonsai.bim.module.model.data import AuthoringData
|
||||
from bpy.types import PropertyGroup
|
||||
from math import pi
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
class BIMCadProperties(PropertyGroup):
|
||||
@@ -31,3 +32,11 @@ class BIMCadProperties(PropertyGroup):
|
||||
gable_roof_edge_angle: bpy.props.FloatProperty(
|
||||
name="Gable Roof Edge Angle", default=pi / 2, soft_min=0, soft_max=pi / 2, subtype="ANGLE"
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
resolution: int
|
||||
radius: float
|
||||
distance: float
|
||||
x: float
|
||||
y: float
|
||||
gable_roof_edge_angle: float
|
||||
|
||||
@@ -23,7 +23,7 @@ import bonsai.bim.module.type.prop as type_prop
|
||||
import ifcopenshell.util.unit
|
||||
from bpy.types import WorkSpaceTool
|
||||
from bonsai.bim.module.model.data import AuthoringData, RailingData, RoofData
|
||||
from typing import Union
|
||||
from functools import partial
|
||||
|
||||
|
||||
def load_custom_icons():
|
||||
@@ -69,10 +69,12 @@ class CadTool(WorkSpaceTool):
|
||||
("bim.cad_hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}),
|
||||
)
|
||||
|
||||
def draw_settings(context, layout, workspace_tool):
|
||||
def draw_settings(
|
||||
context: bpy.types.Context, layout: bpy.types.UILayout, workspace_tool: bpy.types.WorkSpaceTool
|
||||
) -> None:
|
||||
ui_context = str(context.region.type)
|
||||
obj = context.active_object
|
||||
if not obj or not obj.data:
|
||||
if not obj or not (data := obj.data):
|
||||
return
|
||||
is_profile = tool.Geometry.is_profile_object_active()
|
||||
if is_profile:
|
||||
@@ -112,17 +114,20 @@ class CadTool(WorkSpaceTool):
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(row, "Offset", "S_O", "Offset", ui_context)
|
||||
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(row, "Rectangle", "S_R", "Rectangle", ui_context)
|
||||
add_layout_hotkey_operator(row, "Rectangle", "S_R", bpy.ops.bim.add_rectangle.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(row, "Circle", "S_C", "Circle", ui_context)
|
||||
add_layout_hotkey_operator(row, "Circle", "S_C", bpy.ops.bim.add_ifccircle.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.set_arc_index.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(row, "Reset Vertex", "S_X", bpy.ops.bim.reset_vertex.__doc__, ui_context)
|
||||
|
||||
elif hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.subshape_type == "AXIS":
|
||||
elif (
|
||||
isinstance(data, tool.Geometry.TYPES_WITH_MESH_PROPERTIES)
|
||||
and tool.Geometry.get_mesh_props(data).subshape_type == "AXIS"
|
||||
):
|
||||
add_header_apply_button(
|
||||
layout, "Edit Axis", "bim.edit_extrusion_axis", "bim.disable_editing_extrusion_axis", ui_context
|
||||
)
|
||||
@@ -133,15 +138,15 @@ class CadTool(WorkSpaceTool):
|
||||
row, "Join", "S_T", "Joins two non-parallel paths at their intersection", ui_context
|
||||
)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(row, "Fillet", "S_F", "Fillet", ui_context)
|
||||
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.cad_fillet.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(row, "Offset", "S_O", "Offset", ui_context)
|
||||
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context)
|
||||
|
||||
else:
|
||||
if (
|
||||
(RailingData.is_loaded or not RailingData.load())
|
||||
and RailingData.data["pset_data"]
|
||||
and context.active_object.BIMRailingProperties.is_editing_path
|
||||
and tool.Model.get_railing_props(obj).is_editing_path
|
||||
):
|
||||
add_header_apply_button(
|
||||
layout,
|
||||
@@ -154,7 +159,7 @@ class CadTool(WorkSpaceTool):
|
||||
elif (
|
||||
(RoofData.is_loaded or not RoofData.load())
|
||||
and RoofData.data["pset_data"]
|
||||
and context.active_object.BIMRoofProperties.is_editing_path
|
||||
and tool.Model.get_roof_props(obj).is_editing_path
|
||||
):
|
||||
add_header_apply_button(
|
||||
layout, "Edit Roof Path", "bim.finish_editing_roof_path", "bim.cancel_editing_roof_path", ui_context
|
||||
@@ -171,17 +176,17 @@ class CadTool(WorkSpaceTool):
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(row, "Offset", "S_O", "Offset", ui_context)
|
||||
add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(row, "2-Point Arc", "S_C", "2-Point Arc", ui_context)
|
||||
add_layout_hotkey_operator(row, "2-Point Arc", "S_C", bpy.ops.bim.cad_arc_from_2_points.__doc__, ui_context)
|
||||
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.cad_arc_from_3_points.__doc__, ui_context)
|
||||
|
||||
|
||||
class CadHotkey(bpy.types.Operator):
|
||||
bl_idname = "bim.cad_hotkey"
|
||||
bl_label = "CAD Hotkey"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_label = ""
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
hotkey: bpy.props.StringProperty()
|
||||
description: bpy.props.StringProperty()
|
||||
|
||||
@@ -190,12 +195,14 @@ class CadHotkey(bpy.types.Operator):
|
||||
return operator.description or ""
|
||||
|
||||
def execute(self, context):
|
||||
self.props = context.scene.BIMCadProperties
|
||||
self.props = tool.Cad.get_cad_props()
|
||||
getattr(self, f"hotkey_{self.hotkey}")()
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context):
|
||||
props = context.scene.BIMCadProperties
|
||||
props = tool.Cad.get_cad_props()
|
||||
obj = context.active_object
|
||||
|
||||
if self.hotkey == "S_C":
|
||||
if tool.Geometry.is_profile_object_active():
|
||||
row = self.layout.row()
|
||||
@@ -221,7 +228,7 @@ class CadHotkey(bpy.types.Operator):
|
||||
elif (
|
||||
(RoofData.is_loaded or not RoofData.load())
|
||||
and RoofData.data["pset_data"]
|
||||
and bpy.context.active_object.BIMRoofProperties.is_editing_path
|
||||
and tool.Model.get_roof_props(obj).is_editing_path
|
||||
):
|
||||
self.layout.row().prop(props, "gable_roof_edge_angle")
|
||||
|
||||
@@ -231,9 +238,8 @@ class CadHotkey(bpy.types.Operator):
|
||||
row.prop(props, "resolution")
|
||||
|
||||
def hotkey_S_C(self):
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
if tool.Geometry.is_profile_object_active():
|
||||
bpy.ops.bim.add_ifccircle(radius=self.props.radius / si_conversion)
|
||||
bpy.ops.bim.add_ifccircle(radius=self.props.radius)
|
||||
else:
|
||||
bpy.ops.bim.cad_arc_from_2_points()
|
||||
|
||||
@@ -241,36 +247,49 @@ class CadHotkey(bpy.types.Operator):
|
||||
bpy.ops.bim.cad_trim_extend()
|
||||
|
||||
def hotkey_S_F(self):
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
if tool.Geometry.is_profile_object_active():
|
||||
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius / si_conversion)
|
||||
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius)
|
||||
else:
|
||||
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius / si_conversion)
|
||||
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius)
|
||||
|
||||
def hotkey_S_O(self):
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
bpy.ops.bim.cad_offset(distance=self.props.distance / si_conversion)
|
||||
bpy.ops.bim.cad_offset(distance=self.props.distance)
|
||||
|
||||
def hotkey_S_Q(self):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
if bpy.context.active_object.data.BIMMeshProperties.subshape_type == "PROFILE":
|
||||
obj = bpy.context.active_object
|
||||
|
||||
if not obj:
|
||||
return
|
||||
|
||||
if not tool.Geometry.has_mesh_properties(data := obj.data):
|
||||
return
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return
|
||||
|
||||
mprops = tool.Geometry.get_mesh_props(data)
|
||||
if mprops.subshape_type == "PROFILE":
|
||||
if element.is_a("IfcProfileDef"):
|
||||
bpy.ops.bim.edit_arbitrary_profile()
|
||||
elif element.is_a("IfcRelSpaceBoundary"):
|
||||
bpy.ops.bim.edit_boundary_geometry()
|
||||
else:
|
||||
bpy.ops.bim.edit_extrusion_profile()
|
||||
elif bpy.context.active_object.data.BIMMeshProperties.subshape_type == "AXIS":
|
||||
elif mprops.subshape_type == "AXIS":
|
||||
bpy.ops.bim.edit_extrusion_axis()
|
||||
|
||||
def hotkey_S_R(self):
|
||||
obj = bpy.context.active_object
|
||||
if not obj:
|
||||
return
|
||||
|
||||
if tool.Geometry.is_profile_object_active():
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
bpy.ops.bim.add_rectangle(x=self.props.x / si_conversion, y=self.props.y / si_conversion)
|
||||
bpy.ops.bim.add_rectangle(x=self.props.x, y=self.props.y)
|
||||
elif (
|
||||
(RoofData.is_loaded or not RoofData.load())
|
||||
and RoofData.data["pset_data"]
|
||||
and bpy.context.active_object.BIMRoofProperties.is_editing_path
|
||||
and tool.Model.get_roof_props(obj).is_editing_path
|
||||
):
|
||||
bpy.ops.bim.set_gable_roof_edge_angle(angle=self.props.gable_roof_edge_angle)
|
||||
|
||||
@@ -288,7 +307,9 @@ class CadHotkey(bpy.types.Operator):
|
||||
bpy.ops.bim.reset_vertex()
|
||||
|
||||
|
||||
def add_header_apply_button(layout, text, apply_operator, cancel_operator, ui_context=""):
|
||||
def add_header_apply_button(
|
||||
layout: bpy.types.UILayout, text: str, apply_operator: str, cancel_operator: str, ui_context: str = ""
|
||||
) -> None:
|
||||
custom_icon = custom_icon_previews.get(text.upper().replace(" ", "_"), custom_icon_previews["IFC"]).icon_id
|
||||
row = layout.row(align=True)
|
||||
row.label(text=f"{text} Mode", icon_value=custom_icon)
|
||||
@@ -307,34 +328,8 @@ def add_header_apply_button(layout, text, apply_operator, cancel_operator, ui_co
|
||||
row.label(text="Tools")
|
||||
|
||||
|
||||
def add_layout_hotkey_operator(
|
||||
layout: bpy.types.UILayout, text: str, hotkey: str, description: Union[str, None], ui_context: str = ""
|
||||
) -> bpy.types.OperatorProperties:
|
||||
parts = hotkey.split("_")
|
||||
modifier, key = parts
|
||||
op_text = "" if ui_context == "TOOL_HEADER" else text
|
||||
custom_icon = custom_icon_previews.get(text.upper().replace(" ", "_"), custom_icon_previews["IFC"]).icon_id
|
||||
modifier_icon, modifier_str = MODIFIERS.get(modifier, ("NONE", ""))
|
||||
|
||||
row = layout if ui_context == "TOOL_HEADER" else layout.row(align=True)
|
||||
op = row.operator("bim.cad_hotkey", text=op_text, icon_value=custom_icon)
|
||||
|
||||
if ui_context != "TOOL_HEADER" and len(parts) == 2:
|
||||
layout = layout.row(align=True) # Create a new line for hotkey display
|
||||
layout.label(text="", icon=modifier_icon)
|
||||
layout.label(text="", icon=f"EVENT_{key}")
|
||||
|
||||
hotkey_description = f"Hotkey: {modifier_str} {key}"
|
||||
description = "\n\n".join(filter(None, [hotkey_description if description else ""]))
|
||||
op.hotkey = hotkey
|
||||
op.description = description or hotkey_description
|
||||
|
||||
return op
|
||||
add_layout_hotkey_operator = partial(tool.Blender.add_layout_hotkey_operator, tool_name="cad", module_name=__name__)
|
||||
|
||||
|
||||
custom_icon_previews = None
|
||||
display_mode = None
|
||||
|
||||
MODIFIERS = {
|
||||
"S": ("EVENT_SHIFT", "⇧"),
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@ class ClashDecorator:
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
@@ -60,8 +62,9 @@ class ClashDecorator:
|
||||
unselected_elements_color = self.addon_prefs.decorator_color_unselected
|
||||
special_elements_color = self.addon_prefs.decorator_color_special
|
||||
|
||||
text = context.scene.BIMClashProperties.active_clash_text
|
||||
p = context.scene.BIMClashProperties.p1.lerp(context.scene.BIMClashProperties.p2, 0.5)
|
||||
props = tool.Clash.get_clash_props()
|
||||
text = props.active_clash_text
|
||||
p = props.p1.lerp(props.p2, 0.5)
|
||||
|
||||
font_id = 0
|
||||
blf.size(font_id, 12)
|
||||
@@ -92,7 +95,8 @@ class ClashDecorator:
|
||||
# general shader
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
|
||||
selected_vertices = [context.scene.BIMClashProperties.p1, context.scene.BIMClashProperties.p2]
|
||||
props = tool.Clash.get_clash_props()
|
||||
selected_vertices = [props.p1, props.p2]
|
||||
selected_edges = []
|
||||
if selected_vertices[0] != selected_vertices[1]:
|
||||
selected_edges = [[0, 1]]
|
||||
|
||||
@@ -24,26 +24,20 @@ import logging
|
||||
import numpy as np
|
||||
import ifcopenshell
|
||||
import bonsai.tool as tool
|
||||
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
||||
from math import radians
|
||||
from mathutils import Matrix, Vector
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.module.clash.decorator import ClashDecorator
|
||||
|
||||
|
||||
class ExportClashSets(bpy.types.Operator):
|
||||
class ExportClashSets(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "bim.export_clash_sets"
|
||||
bl_label = "Export Clash Sets"
|
||||
bl_description = "Export clash sets to a selected file"
|
||||
filename_ext = ".json"
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def execute(self, context):
|
||||
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
|
||||
clash_sets = tool.Clash.export_clash_sets()
|
||||
@@ -52,26 +46,24 @@ class ExportClashSets(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ImportClashSets(bpy.types.Operator):
|
||||
class ImportClashSets(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.import_clash_sets"
|
||||
bl_label = "Import Clash Sets"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Import clash sets from a selected file"
|
||||
filename_ext = ".json"
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
return ImportHelper.invoke(self, context, event)
|
||||
|
||||
def execute(self, context):
|
||||
tool.Clash.load_clash_sets(self.filepath)
|
||||
context.scene.BIMClashProperties.clash_sets.clear()
|
||||
props = tool.Clash.get_clash_props()
|
||||
props.clash_sets.clear()
|
||||
for clash_set in tool.Clash.get_clash_sets():
|
||||
new = context.scene.BIMClashProperties.clash_sets.add()
|
||||
new = props.clash_sets.add()
|
||||
new.name = clash_set["name"]
|
||||
new.mode = clash_set["mode"]
|
||||
if new.mode == "intersection":
|
||||
@@ -106,7 +98,8 @@ class AddClashSet(bpy.types.Operator):
|
||||
bl_description = "Add a clash set"
|
||||
|
||||
def execute(self, context):
|
||||
new = context.scene.BIMClashProperties.clash_sets.add()
|
||||
props = tool.Clash.get_clash_props()
|
||||
new = props.clash_sets.add()
|
||||
new.name = "New Clash Set"
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -119,7 +112,8 @@ class RemoveClashSet(bpy.types.Operator):
|
||||
index: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.BIMClashProperties.clash_sets.remove(self.index)
|
||||
props = tool.Clash.get_clash_props()
|
||||
props.clash_sets.remove(self.index)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -131,7 +125,8 @@ class AddClashSource(bpy.types.Operator):
|
||||
group: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
clash_set = context.scene.BIMClashProperties.active_clash_set
|
||||
props = tool.Clash.get_clash_props()
|
||||
clash_set = props.active_clash_set
|
||||
source = getattr(clash_set, self.group).add()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -145,78 +140,74 @@ class RemoveClashSource(bpy.types.Operator):
|
||||
group: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
clash_set = context.scene.BIMClashProperties.active_clash_set
|
||||
props = tool.Clash.get_clash_props()
|
||||
clash_set = props.active_clash_set
|
||||
getattr(clash_set, self.group).remove(self.index)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectClashSource(bpy.types.Operator):
|
||||
class SelectClashSource(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.select_clash_source"
|
||||
bl_label = "Select Clash Source"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Select an IFC file to add as a clash source"
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
|
||||
index: bpy.props.IntProperty()
|
||||
group: bpy.props.StringProperty()
|
||||
filename_ext = ".ifc"
|
||||
|
||||
def execute(self, context):
|
||||
clash_set = context.scene.BIMClashProperties.active_clash_set
|
||||
props = tool.Clash.get_clash_props()
|
||||
clash_set = props.active_clash_set
|
||||
getattr(clash_set, self.group)[self.index].name = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class SelectClashResults(bpy.types.Operator):
|
||||
class SelectClashResults(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.select_clash_results"
|
||||
bl_label = "Select Clash Results"
|
||||
bl_description = "Select filepath for clash results."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.BIMClashProperties.clash_results_path = self.filepath
|
||||
props = tool.Clash.get_clash_props()
|
||||
props.clash_results_path = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class SelectSmartGroupedClashesPath(bpy.types.Operator):
|
||||
class SelectSmartGroupedClashesPath(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.select_smart_grouped_clashes_path"
|
||||
bl_label = "Select Smart-Grouped Clashes Path"
|
||||
bl_description = "Select filepath for smart-grouped clashes."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.BIMClashProperties.smart_grouped_clashes_path = self.filepath
|
||||
props = tool.Clash.get_clash_props()
|
||||
props.smart_grouped_clashes_path = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class ExecuteIfcClash(bpy.types.Operator):
|
||||
class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "bim.execute_ifc_clash"
|
||||
bl_label = "Execute IFC Clash"
|
||||
bl_description = "Execute clash detection and save the information to a .bcf or .json file"
|
||||
filter_glob: bpy.props.StringProperty(default="*.bcf;*.json", options={"HIDDEN"})
|
||||
format: bpy.props.EnumProperty(name="Format", items=[(i, i, "") for i in ("bcf", "json")])
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
|
||||
|
||||
@property
|
||||
def filename_ext(self) -> str:
|
||||
return f".{self.format.lower()}"
|
||||
|
||||
def invoke(self, context, event):
|
||||
if self.filepath:
|
||||
return self.execute(context)
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
return ExportHelper.invoke(self, context, event)
|
||||
|
||||
def execute(self, context):
|
||||
from ifcclash import ifcclash
|
||||
|
||||
self.props = context.scene.BIMClashProperties
|
||||
self.props = tool.Clash.get_clash_props()
|
||||
|
||||
_, extension = os.path.splitext(self.filepath)
|
||||
if extension != ".bcf":
|
||||
@@ -284,27 +275,27 @@ class ExecuteIfcClash(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectIfcClashResults(bpy.types.Operator):
|
||||
class SelectIfcClashResults(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.select_ifc_clash_results"
|
||||
bl_label = "Select IFC Clash Results"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Select the clashing IFC geometry stored in a file"
|
||||
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
|
||||
filename_ext = ".json"
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
return ImportHelper.invoke(self, context, event)
|
||||
|
||||
def execute(self, context):
|
||||
# TODO refactor into new clash results system
|
||||
self.file = IfcStore.get_file()
|
||||
self.file = tool.Ifc.get()
|
||||
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
|
||||
with open(self.filepath) as f:
|
||||
clash_sets = json.load(f)
|
||||
clash_set_name = context.scene.BIMClashProperties.active_clash_set.name
|
||||
clash_props = tool.Clash.get_clash_props()
|
||||
assert clash_props.active_clash_set
|
||||
clash_set_name = clash_props.active_clash_set.name
|
||||
global_ids = []
|
||||
for clash_set in clash_sets:
|
||||
if clash_set["name"] != clash_set_name:
|
||||
@@ -316,13 +307,15 @@ class SelectIfcClashResults(bpy.types.Operator):
|
||||
global_ids.extend([clash["a_global_id"], clash["b_global_id"]])
|
||||
|
||||
for obj in context.visible_objects:
|
||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
if not props.ifc_definition_id:
|
||||
continue
|
||||
|
||||
ifc_file = ""
|
||||
for scene in obj.users_scene:
|
||||
if scene.BIMProperties.ifc_file:
|
||||
ifc_file = scene.BIMProperties.ifc_file
|
||||
bim_props = tool.Blender.get_bim_props(scene)
|
||||
if bim_props.ifc_file:
|
||||
ifc_file = bim_props.ifc_file
|
||||
if scene.library:
|
||||
break
|
||||
|
||||
@@ -334,7 +327,7 @@ class SelectIfcClashResults(bpy.types.Operator):
|
||||
element_file = self.file
|
||||
|
||||
try:
|
||||
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
element = element_file.by_id(props.ifc_definition_id)
|
||||
except:
|
||||
continue
|
||||
|
||||
@@ -354,7 +347,7 @@ class SelectClash(bpy.types.Operator):
|
||||
index: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
self.props = context.scene.BIMClashProperties
|
||||
self.props = tool.Clash.get_clash_props()
|
||||
clash_set = tool.Clash.get_clash_set(self.props.active_clash_set.name)
|
||||
active_clash = self.props.active_clash
|
||||
clash = tool.Clash.get_clash(clash_set, active_clash.a_global_id, active_clash.b_global_id)
|
||||
@@ -388,13 +381,15 @@ class SmartClashGroup(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.scene.BIMClashProperties.clash_results_path
|
||||
props = tool.Clash.get_clash_props()
|
||||
return bool(props.clash_results_path)
|
||||
|
||||
def execute(self, context):
|
||||
from ifcclash import ifcclash
|
||||
|
||||
settings = ifcclash.ClashSettings()
|
||||
self.filepath = bpy.path.ensure_ext(context.scene.BIMClashProperties.clash_results_path, ".json")
|
||||
props = tool.Clash.get_clash_props()
|
||||
self.filepath = bpy.path.ensure_ext(props.clash_results_path, ".json")
|
||||
settings.output = self.filepath
|
||||
settings.logger = logging.getLogger("Clash")
|
||||
settings.logger.setLevel(logging.DEBUG)
|
||||
@@ -404,19 +399,18 @@ class SmartClashGroup(bpy.types.Operator):
|
||||
clash_sets = json.load(f)
|
||||
|
||||
# execute the smart grouping
|
||||
save_path = bpy.path.ensure_ext(context.scene.BIMClashProperties.smart_grouped_clashes_path, ".json")
|
||||
smart_grouped_clashes = ifc_clasher.smart_group_clashes(
|
||||
clash_sets, context.scene.BIMClashProperties.smart_clash_grouping_max_distance
|
||||
)
|
||||
save_path = bpy.path.ensure_ext(props.smart_grouped_clashes_path, ".json")
|
||||
smart_grouped_clashes = ifc_clasher.smart_group_clashes(clash_sets, props.smart_clash_grouping_max_distance)
|
||||
|
||||
# save smart_groups to json
|
||||
with open(save_path, "w") as f:
|
||||
f.write(json.dumps(smart_grouped_clashes))
|
||||
|
||||
clash_set_name = context.scene.BIMClashProperties.active_clash_set.name
|
||||
assert props.active_clash_set
|
||||
clash_set_name = props.active_clash_set.name
|
||||
|
||||
# Reset the list of smart_clash_groups for the UI
|
||||
context.scene.BIMClashProperties.smart_clash_groups.clear()
|
||||
props.smart_clash_groups.clear()
|
||||
|
||||
for clash_set, smart_groups in smart_grouped_clashes.items():
|
||||
# Only select the clashes that correspond to the actively selected IFC Clash Set
|
||||
@@ -424,7 +418,7 @@ class SmartClashGroup(bpy.types.Operator):
|
||||
continue
|
||||
else:
|
||||
for smart_group, global_id_pairs in smart_groups[0].items():
|
||||
new_group = context.scene.BIMClashProperties.smart_clash_groups.add()
|
||||
new_group = props.smart_clash_groups.add()
|
||||
new_group.number = f"{smart_group}"
|
||||
|
||||
for pair in global_id_pairs:
|
||||
@@ -442,18 +436,21 @@ class LoadSmartGroupsForActiveClashSet(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.scene.BIMClashProperties.active_clash_set
|
||||
props = tool.Clash.get_clash_props()
|
||||
return bool(props.active_clash_set)
|
||||
|
||||
def execute(self, context):
|
||||
smart_groups_path = bpy.path.ensure_ext(context.scene.BIMClashProperties.smart_grouped_clashes_path, ".json")
|
||||
props = tool.Clash.get_clash_props()
|
||||
smart_groups_path = bpy.path.ensure_ext(props.smart_grouped_clashes_path, ".json")
|
||||
|
||||
clash_set_name = context.scene.BIMClashProperties.active_clash_set.name
|
||||
assert props.active_clash_set
|
||||
clash_set_name = props.active_clash_set.name
|
||||
|
||||
with open(smart_groups_path) as f:
|
||||
smart_grouped_clashes = json.load(f)
|
||||
|
||||
# Reset the list of smart_clash_groups for the UI
|
||||
context.scene.BIMClashProperties.smart_clash_groups.clear()
|
||||
props.smart_clash_groups.clear()
|
||||
|
||||
for clash_set, smart_groups in smart_grouped_clashes.items():
|
||||
# Only select the clashes that correspond to the actively selected IFC Clash Set
|
||||
@@ -461,12 +458,12 @@ class LoadSmartGroupsForActiveClashSet(bpy.types.Operator):
|
||||
continue
|
||||
else:
|
||||
for smart_group, global_id_pairs in smart_groups[0].items():
|
||||
new_group = context.scene.BIMClashProperties.smart_clash_groups.add()
|
||||
new_group = props.smart_clash_groups.add()
|
||||
new_group.number = f"{smart_group}"
|
||||
for pair in global_id_pairs:
|
||||
for guid in pair:
|
||||
new_global_id = new_group.global_ids.add()
|
||||
new_global_id.guid = guid
|
||||
new_global_id.name = guid
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -478,15 +475,19 @@ class SelectSmartGroup(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return IfcStore.get_file() and context.visible_objects and context.scene.BIMClashProperties.active_smart_group
|
||||
props = tool.Clash.get_clash_props()
|
||||
return tool.Ifc.get() and context.visible_objects and props.active_smart_group
|
||||
|
||||
def execute(self, context):
|
||||
selected_smart_group = context.scene.BIMClashProperties.active_smart_group
|
||||
products = []
|
||||
ifc_file = tool.Ifc.get()
|
||||
props = tool.Clash.get_clash_props()
|
||||
selected_smart_group = props.active_smart_group
|
||||
assert selected_smart_group
|
||||
products: list[ifcopenshell.entity_instance] = []
|
||||
for global_id in selected_smart_group.global_ids:
|
||||
try:
|
||||
products.append(tool.Ifc.get().by_guid(global_id.guid))
|
||||
except:
|
||||
products.append(ifc_file.by_guid(global_id.name))
|
||||
except RuntimeError:
|
||||
continue
|
||||
tool.Spatial.select_products(products, unhide=True)
|
||||
context_override = tool.Blender.get_viewport_context()
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.prop import StrProperty, Attribute, BIMFilterGroup
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
@@ -29,6 +30,8 @@ from bpy.props import (
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
from mathutils import Vector
|
||||
from typing import TYPE_CHECKING, Literal, Union
|
||||
|
||||
|
||||
class ClashSource(PropertyGroup):
|
||||
@@ -43,6 +46,10 @@ class ClashSource(PropertyGroup):
|
||||
name="Mode",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
|
||||
mode: Literal["a", "i", "e"]
|
||||
|
||||
|
||||
class Clash(PropertyGroup):
|
||||
a_global_id: StringProperty(name="A")
|
||||
@@ -51,9 +58,15 @@ class Clash(PropertyGroup):
|
||||
b_name: StringProperty(name="B Name")
|
||||
status: BoolProperty(name="Status", default=False)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
a_global_id: str
|
||||
b_global_id: str
|
||||
a_name: str
|
||||
b_name: str
|
||||
status: bool
|
||||
|
||||
|
||||
class ClashSet(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
mode: EnumProperty(
|
||||
items=[
|
||||
(
|
||||
@@ -76,11 +89,25 @@ class ClashSet(PropertyGroup):
|
||||
b: CollectionProperty(name="Group B", type=ClashSource)
|
||||
clashes: CollectionProperty(name="Clashes", type=Clash)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
mode: Literal["intersection", "collision", "clearance"]
|
||||
tolerance: float
|
||||
clearance: float
|
||||
allow_touching: bool
|
||||
check_all: bool
|
||||
a: bpy.types.bpy_prop_collection_idprop[ClashSource]
|
||||
b: bpy.types.bpy_prop_collection_idprop[ClashSource]
|
||||
clashes: bpy.types.bpy_prop_collection_idprop[Clash]
|
||||
|
||||
|
||||
class SmartClashGroup(PropertyGroup):
|
||||
number: StringProperty(name="Number")
|
||||
global_ids: CollectionProperty(name="GlobalIDs", type=StrProperty)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
number: str
|
||||
global_ids: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
|
||||
|
||||
class BIMClashProperties(PropertyGroup):
|
||||
blender_clash_set_a: CollectionProperty(name="Blender Clash Set A", type=StrProperty)
|
||||
@@ -107,17 +134,33 @@ class BIMClashProperties(PropertyGroup):
|
||||
subtype="FILE_PATH",
|
||||
)
|
||||
|
||||
@property
|
||||
def active_clash_set(self):
|
||||
if self.active_clash_set_index < len(self.clash_sets):
|
||||
return self.clash_sets[self.active_clash_set_index]
|
||||
if TYPE_CHECKING:
|
||||
blender_clash_set_a: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
blender_clash_set_b: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
clash_sets: bpy.types.bpy_prop_collection_idprop[ClashSet]
|
||||
should_create_clash_snapshots: bool
|
||||
clash_results_path: str
|
||||
smart_grouped_clashes_path: str
|
||||
active_clash_set_index: int
|
||||
active_clash_index: int
|
||||
smart_clash_groups: bpy.types.bpy_prop_collection_idprop[SmartClashGroup]
|
||||
active_smart_group_index: int
|
||||
smart_clash_grouping_max_distance: int
|
||||
p1: Vector
|
||||
p2: Vector
|
||||
active_clash_text: str
|
||||
export_path: str
|
||||
|
||||
@property
|
||||
def active_smart_group(self):
|
||||
if self.active_smart_group_index < len(self.smart_clash_groups):
|
||||
return self.smart_clash_groups[self.active_smart_group_index]
|
||||
def active_clash_set(self) -> Union[ClashSet, None]:
|
||||
return tool.Blender.get_active_uilist_element(self.clash_sets, self.active_clash_set_index)
|
||||
|
||||
@property
|
||||
def active_clash(self):
|
||||
if self.active_clash_index < len(self.active_clash_set.clashes):
|
||||
return self.active_clash_set.clashes[self.active_clash_index]
|
||||
def active_smart_group(self) -> Union[SmartClashGroup, None]:
|
||||
return tool.Blender.get_active_uilist_element(self.smart_clash_groups, self.active_smart_group_index)
|
||||
|
||||
@property
|
||||
def active_clash(self) -> Union[Clash, None]:
|
||||
if not (clash_set := self.active_clash_set):
|
||||
return None
|
||||
return tool.Blender.get_active_uilist_element(clash_set.clashes, self.active_clash_index)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import bonsai.tool as tool
|
||||
import bonsai.bim.helper
|
||||
from bpy.types import Panel
|
||||
from bonsai.bim.module.clash.data import ClashData
|
||||
@@ -36,9 +37,7 @@ class BIM_PT_ifcclash(Panel):
|
||||
ClashData.load()
|
||||
|
||||
layout = self.layout
|
||||
|
||||
scene = context.scene
|
||||
props = scene.BIMClashProperties
|
||||
props = tool.Clash.get_clash_props()
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.add_clash_set")
|
||||
@@ -157,7 +156,7 @@ class BIM_PT_smart_clash_manager(Panel):
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
props = context.scene.BIMClashProperties
|
||||
props = tool.Clash.get_clash_props()
|
||||
|
||||
row = layout.row()
|
||||
layout.label(text="Select clash results to group:")
|
||||
|
||||
@@ -118,7 +118,7 @@ class MaterialClassificationsData(ReferencesData):
|
||||
def references(cls):
|
||||
results = []
|
||||
|
||||
props = bpy.context.scene.BIMMaterialProperties
|
||||
props = tool.Material.get_material_props()
|
||||
if props.materials and props.active_material_index < len(props.materials):
|
||||
material = props.materials[props.active_material_index]
|
||||
if material.ifc_definition_id:
|
||||
@@ -145,11 +145,8 @@ class CostClassificationsData(ReferencesData):
|
||||
@classmethod
|
||||
def references(cls):
|
||||
results = []
|
||||
element = tool.Ifc.get().by_id(
|
||||
bpy.context.scene.BIMCostProperties.cost_items[
|
||||
bpy.context.scene.BIMCostProperties.active_cost_item_index
|
||||
].ifc_definition_id
|
||||
)
|
||||
props = tool.Cost.get_cost_props()
|
||||
element = tool.Ifc.get().by_id(props.cost_items[props.active_cost_item_index].ifc_definition_id)
|
||||
if element:
|
||||
for reference in ifcopenshell.util.classification.get_references(element):
|
||||
data = reference.get_info()
|
||||
|
||||
@@ -25,19 +25,16 @@ import ifcopenshell.util.classification
|
||||
import ifcopenshell.util.element
|
||||
import bonsai.tool as tool
|
||||
import bonsai.bim.helper
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
|
||||
|
||||
class LoadClassificationLibrary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class LoadClassificationLibrary(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
bl_idname = "bim.load_classification_library"
|
||||
bl_label = "Load Classification Library"
|
||||
bl_description = "Load classification library from the provided filepath."
|
||||
filename_ext = ".ifc"
|
||||
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def _execute(self, context):
|
||||
IfcStore.classification_file = ifcopenshell.open(self.filepath)
|
||||
@@ -201,7 +198,8 @@ class EnableEditingClassification(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
def callback(name, prop, data):
|
||||
if name == "ReferenceTokens":
|
||||
new = bpy.context.scene.BIMGeoreferenceProperties.projected_crs.add()
|
||||
geo_props = tool.Georeference.get_georeference_props()
|
||||
new = geo_props.projected_crs.add()
|
||||
new.name = name
|
||||
new.data_type = "string"
|
||||
new.is_null = data[name] is None
|
||||
@@ -239,7 +237,7 @@ class RemoveClassification(bpy.types.Operator, tool.Ifc.Operator):
|
||||
classification: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
self.file = tool.Ifc.get()
|
||||
ifcopenshell.api.run(
|
||||
"classification.remove_classification",
|
||||
tool.Ifc.get(),
|
||||
@@ -459,6 +457,7 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
|
||||
for prop in blender_properties:
|
||||
properties[prop.name] = prop.get_value()
|
||||
|
||||
# TODO: is this still the correct approach?
|
||||
if classification_pset.name == "undefined_set":
|
||||
if "ObjectType" in properties:
|
||||
if hasattr(element, "ObjectType"):
|
||||
|
||||
@@ -21,7 +21,6 @@ import bonsai.bim.helper
|
||||
import bonsai.tool as tool
|
||||
import bonsai.bim.module.classification.prop as classification_prop
|
||||
from bpy.types import Panel, UIList
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.module.classification.data import (
|
||||
ClassificationsData,
|
||||
ClassificationReferencesData,
|
||||
@@ -124,7 +123,7 @@ class ReferenceUI:
|
||||
self.sprops = context.scene.BIMClassificationProperties
|
||||
self.bprops = context.scene.BIMBSDDProperties
|
||||
self.props = context.scene.BIMClassificationReferenceProperties
|
||||
self.file = IfcStore.get_file()
|
||||
self.file = tool.Ifc.get()
|
||||
|
||||
self.draw_add_ui(context)
|
||||
|
||||
@@ -312,7 +311,7 @@ class BIM_PT_material_classifications(Panel, ReferenceUI):
|
||||
def poll(cls, context):
|
||||
if not tool.Ifc.get():
|
||||
return False
|
||||
props = context.scene.BIMMaterialProperties
|
||||
props = tool.Material.get_material_props()
|
||||
if props.is_editing and (material := props.active_material) and material.ifc_definition_id:
|
||||
return True
|
||||
return False
|
||||
@@ -339,7 +338,8 @@ class BIM_PT_cost_classifications(Panel, ReferenceUI):
|
||||
def poll(cls, context):
|
||||
if not tool.Ifc.get():
|
||||
return False
|
||||
return bool(context.scene.BIMCostProperties.cost_items)
|
||||
props = tool.Cost.get_cost_props()
|
||||
return bool(props.cost_items)
|
||||
|
||||
def draw(self, context):
|
||||
if not CostClassificationsData.is_loaded:
|
||||
|
||||
@@ -23,7 +23,6 @@ import ifcopenshell.api.constraint
|
||||
import ifcopenshell.util.attribute
|
||||
import bonsai.bim.helper
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
|
||||
|
||||
class LoadObjectives(bpy.types.Operator):
|
||||
@@ -84,7 +83,7 @@ class AddObjective(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
result = ifcopenshell.api.run("constraint.add_objective", IfcStore.get_file())
|
||||
result = ifcopenshell.api.run("constraint.add_objective", tool.Ifc.get())
|
||||
bpy.ops.bim.load_objectives()
|
||||
bpy.ops.bim.enable_editing_constraint(constraint=result.id())
|
||||
return {"FINISHED"}
|
||||
@@ -116,7 +115,7 @@ class RemoveConstraint(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMConstraintProperties
|
||||
self.file = IfcStore.get_file()
|
||||
self.file = tool.Ifc.get()
|
||||
ifcopenshell.api.run(
|
||||
"constraint.remove_constraint", self.file, **{"constraint": self.file.by_id(self.constraint)}
|
||||
)
|
||||
@@ -162,8 +161,12 @@ class AssignConstraint(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
self.file = tool.Ifc.get()
|
||||
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
|
||||
products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)]
|
||||
objs = [bpy.data.objects[self.obj]] if self.obj else context.selected_objects
|
||||
products = [
|
||||
self.file.by_id(obj_id)
|
||||
for obj in objs
|
||||
if (obj_id := tool.Blender.get_object_bim_props(obj).ifc_definition_id)
|
||||
]
|
||||
if products:
|
||||
ifcopenshell.api.run(
|
||||
"constraint.assign_constraint",
|
||||
@@ -185,8 +188,12 @@ class UnassignConstraint(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
self.file = tool.Ifc.get()
|
||||
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
|
||||
products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)]
|
||||
objs = [bpy.data.objects[self.obj]] if self.obj else context.selected_objects
|
||||
products = [
|
||||
self.file.by_id(obj_id)
|
||||
for obj in objs
|
||||
if (obj_id := tool.Blender.get_object_bim_props(obj).ifc_definition_id)
|
||||
]
|
||||
if products:
|
||||
ifcopenshell.api.run(
|
||||
"constraint.unassign_constraint",
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bonsai.tool as tool
|
||||
from bpy.types import Panel, UIList
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.helper import draw_attributes
|
||||
from bonsai.bim.module.constraint.data import ConstraintsData, ObjectConstraintsData
|
||||
|
||||
@@ -33,7 +33,7 @@ class BIM_PT_constraints(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return IfcStore.get_file()
|
||||
return tool.Ifc.get()
|
||||
|
||||
def draw(self, context):
|
||||
if not ConstraintsData.is_loaded:
|
||||
@@ -79,21 +79,21 @@ class BIM_PT_object_constraints(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
if not (obj := context.active_object):
|
||||
return False
|
||||
if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id):
|
||||
return False
|
||||
return bool(context.active_object.BIMObjectProperties.ifc_definition_id)
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
return bool(tool.Ifc.get_object_by_identifier(props.ifc_definition_id))
|
||||
|
||||
def draw(self, context):
|
||||
if not ObjectConstraintsData.is_loaded:
|
||||
ObjectConstraintsData.load()
|
||||
|
||||
obj = context.active_object
|
||||
self.oprops = obj.BIMObjectProperties
|
||||
assert obj
|
||||
self.oprops = tool.Blender.get_object_bim_props(obj)
|
||||
self.sprops = context.scene.BIMConstraintProperties
|
||||
self.props = obj.BIMObjectConstraintProperties
|
||||
self.file = IfcStore.get_file()
|
||||
self.file = tool.Ifc.get()
|
||||
|
||||
self.draw_add_ui()
|
||||
|
||||
|
||||
@@ -66,8 +66,9 @@ class CostSchedulesData:
|
||||
@classmethod
|
||||
def schedules(cls):
|
||||
results = []
|
||||
if bpy.context.scene.BIMCostProperties.active_cost_schedule_id:
|
||||
schedule = tool.Ifc.get().by_id(bpy.context.scene.BIMCostProperties.active_cost_schedule_id)
|
||||
props = tool.Cost.get_cost_props()
|
||||
if props.active_cost_schedule_id:
|
||||
schedule = tool.Ifc.get().by_id(props.active_cost_schedule_id)
|
||||
results.append(
|
||||
{
|
||||
"id": schedule.id(),
|
||||
@@ -88,7 +89,8 @@ class CostSchedulesData:
|
||||
|
||||
@classmethod
|
||||
def is_editing_rates(cls):
|
||||
ifc_id = bpy.context.scene.BIMCostProperties.active_cost_schedule_id
|
||||
props = tool.Cost.get_cost_props()
|
||||
ifc_id = props.active_cost_schedule_id
|
||||
if not ifc_id:
|
||||
return
|
||||
return tool.Ifc.get().by_id(ifc_id).PredefinedType == "SCHEDULEOFRATES"
|
||||
@@ -256,7 +258,8 @@ class CostSchedulesData:
|
||||
@classmethod
|
||||
def cost_quantities(cls):
|
||||
results = []
|
||||
ifc_id = bpy.context.scene.BIMCostProperties.active_cost_item_id
|
||||
props = tool.Cost.get_cost_props()
|
||||
ifc_id = props.active_cost_item_id
|
||||
if not ifc_id:
|
||||
return results
|
||||
for quantity in tool.Ifc.get().by_id(ifc_id).CostQuantities or []:
|
||||
@@ -265,7 +268,8 @@ class CostSchedulesData:
|
||||
|
||||
@classmethod
|
||||
def cost_values(cls):
|
||||
ifc_id = bpy.context.scene.BIMCostProperties.active_cost_item_id
|
||||
props = tool.Cost.get_cost_props()
|
||||
ifc_id = props.active_cost_item_id
|
||||
if not ifc_id:
|
||||
return []
|
||||
return ifcopenshell.util.cost.get_cost_values(tool.Ifc.get().by_id(ifc_id))
|
||||
@@ -324,7 +328,8 @@ class CostItemQuantitiesData:
|
||||
|
||||
@classmethod
|
||||
def process_quantity_names(cls):
|
||||
active_task_index = bpy.context.scene.BIMWorkScheduleProperties.active_task_index
|
||||
props = tool.Sequence.get_work_schedule_props()
|
||||
active_task_index = props.active_task_index
|
||||
tprops = tool.Sequence.get_task_tree_props()
|
||||
total_tasks = len(tprops.tasks)
|
||||
if not total_tasks or active_task_index >= total_tasks:
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
import bpy
|
||||
import ifcopenshell.api
|
||||
import bonsai.tool as tool
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.cost as core
|
||||
from typing import get_args, TYPE_CHECKING
|
||||
from typing import get_args, TYPE_CHECKING, Literal
|
||||
|
||||
|
||||
class AddCostSchedule(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -36,16 +36,18 @@ class AddCostSchedule(bpy.types.Operator, tool.Ifc.Operator):
|
||||
object_type: bpy.props.StringProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
predefined_type = context.scene.BIMCostProperties.cost_schedule_predefined_types
|
||||
props = tool.Cost.get_cost_props()
|
||||
predefined_type = props.cost_schedule_predefined_types
|
||||
if predefined_type == "USERDEFINED":
|
||||
predefined_type = self.object_type
|
||||
core.add_cost_schedule(tool.Ifc, name=self.name, predefined_type=predefined_type)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
props = tool.Cost.get_cost_props()
|
||||
layout.prop(self, "name", text="Name")
|
||||
layout.prop(context.scene.BIMCostProperties, "cost_schedule_predefined_types", text="Type")
|
||||
if context.scene.BIMCostProperties.cost_schedule_predefined_types == "USERDEFINED":
|
||||
layout.prop(props, "cost_schedule_predefined_types", text="Type")
|
||||
if props.cost_schedule_predefined_types == "USERDEFINED":
|
||||
layout.prop(self, "object_type", text="Object type")
|
||||
|
||||
def invoke(self, context, event):
|
||||
@@ -58,10 +60,11 @@ class EditCostSchedule(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = tool.Cost.get_cost_props()
|
||||
core.edit_cost_schedule(
|
||||
tool.Ifc,
|
||||
tool.Cost,
|
||||
cost_schedule=tool.Ifc.get().by_id(context.scene.BIMCostProperties.active_cost_schedule_id),
|
||||
cost_schedule=tool.Ifc.get().by_id(props.active_cost_schedule_id),
|
||||
)
|
||||
|
||||
|
||||
@@ -559,7 +562,8 @@ class AddCostColumn(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.scene.BIMCostProperties.cost_column:
|
||||
props = tool.Cost.get_cost_props()
|
||||
if not props.cost_column:
|
||||
cls.poll_message_set("Cost column name is empty")
|
||||
return False
|
||||
return True
|
||||
@@ -678,7 +682,7 @@ class CalculateCostItemResourceValue(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ExportCostSchedules(bpy.types.Operator):
|
||||
class ExportCostSchedules(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "bim.export_cost_schedules"
|
||||
bl_label = "Export Cost Schedule"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
@@ -691,6 +695,13 @@ class ExportCostSchedules(bpy.types.Operator):
|
||||
default=True,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
format: Literal["CSV", "XLSX", "ODS"]
|
||||
|
||||
@property
|
||||
def filename_ext(self) -> str:
|
||||
return f".{self.format.lower()}"
|
||||
|
||||
def execute(self, context):
|
||||
cost_schedule = tool.Ifc.get().by_id(self.cost_schedule) if self.cost_schedule else None
|
||||
r = core.export_cost_schedules(
|
||||
@@ -701,9 +712,7 @@ class ExportCostSchedules(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
wm = context.window_manager
|
||||
wm.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
return ExportHelper.invoke(self, context, event)
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.label(text="Choose a format")
|
||||
@@ -740,15 +749,14 @@ class LoadProductCostItems(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not tool.Ifc.get() or not (obj := context.active_object) or not (obj.BIMObjectProperties.ifc_definition_id):
|
||||
if not tool.Ifc.get() or not (obj := context.active_object) or not (tool.Blender.get_ifc_definition_id(obj)):
|
||||
cls.poll_message_set("No IFC object is active.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
core.load_product_cost_items(
|
||||
tool.Cost, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id)
|
||||
)
|
||||
obj = context.active_object
|
||||
core.load_product_cost_items(tool.Cost, product=tool.Ifc.get_entity(obj))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
import bpy
|
||||
import ifcopenshell.api
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.module.classification.data import CostClassificationsData
|
||||
from bonsai.bim.module.cost.data import CostSchedulesData, CostItemRatesData, CostItemQuantitiesData
|
||||
from bonsai.bim.prop import StrProperty, Attribute
|
||||
@@ -34,6 +33,7 @@ from bpy.props import (
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
|
||||
def get_schedule_of_rates(self, context):
|
||||
@@ -79,11 +79,11 @@ def update_active_cost_item_index(self, context):
|
||||
CostClassificationsData.load()
|
||||
|
||||
|
||||
def update_cost_item_identification(self, context):
|
||||
props = context.scene.BIMCostProperties
|
||||
def update_cost_item_identification(self: "CostItem", context: bpy.types.Context):
|
||||
props = tool.Cost.get_cost_props()
|
||||
if not props.is_cost_update_enabled or self.identification == "XXX":
|
||||
return
|
||||
self.file = IfcStore.get_file()
|
||||
self.file = tool.Ifc.get()
|
||||
ifcopenshell.api.run(
|
||||
"cost.edit_cost_item",
|
||||
self.file,
|
||||
@@ -94,11 +94,11 @@ def update_cost_item_identification(self, context):
|
||||
attribute.string_value = self.identification
|
||||
|
||||
|
||||
def update_cost_item_name(self, context):
|
||||
props = context.scene.BIMCostProperties
|
||||
def update_cost_item_name(self: "CostItem", context: bpy.types.Context) -> None:
|
||||
props = tool.Cost.get_cost_props()
|
||||
if not props.is_cost_update_enabled or self.name == "Unnamed":
|
||||
return
|
||||
self.file = IfcStore.get_file()
|
||||
self.file = tool.Ifc.get()
|
||||
ifcopenshell.api.run(
|
||||
"cost.edit_cost_item",
|
||||
self.file,
|
||||
@@ -143,6 +143,14 @@ class CostItem(PropertyGroup):
|
||||
is_expanded: BoolProperty(name="Is Expanded")
|
||||
level_index: IntProperty(name="Level Index")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
name: str
|
||||
identification: str
|
||||
ifc_definition_id: int
|
||||
has_children: bool
|
||||
is_expanded: bool
|
||||
level_index: int
|
||||
|
||||
|
||||
class CostItemQuantity(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
@@ -151,11 +159,22 @@ class CostItemQuantity(PropertyGroup):
|
||||
unit_symbol: StringProperty(name="Unit Symbol")
|
||||
total_cost_quantity: FloatProperty(name="Total Quantity")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
name: str
|
||||
ifc_definition_id: int
|
||||
total_quantity: float
|
||||
unit_symbol: str
|
||||
total_cost_quantity: float
|
||||
|
||||
|
||||
class CostItemType(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
name: str
|
||||
ifc_definition_id: int
|
||||
|
||||
|
||||
def update_cost_item_parent(self, context):
|
||||
cost_item = tool.Cost.get_highlighted_cost_item()
|
||||
@@ -177,6 +196,9 @@ def update_active_cost_item_resources(self, context):
|
||||
class ScheduleColumn(PropertyGroup):
|
||||
schedule_id: IntProperty()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
schedule_id: int
|
||||
|
||||
|
||||
class BIMCostProperties(PropertyGroup):
|
||||
cost_schedule_predefined_types: EnumProperty(
|
||||
@@ -250,3 +272,57 @@ class BIMCostProperties(PropertyGroup):
|
||||
custom_currency: StringProperty(
|
||||
name="Custom Currency", default="USD", description="Custom Currency in ISO 4217 format"
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
cost_schedule_predefined_types: str
|
||||
is_cost_update_enabled: bool
|
||||
cost_schedule_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
is_editing: str
|
||||
active_cost_schedule_id: int
|
||||
cost_items: bpy.types.bpy_prop_collection_idprop[CostItem]
|
||||
active_cost_item_id: int
|
||||
cost_item_editing_type: str
|
||||
active_cost_item_index: int
|
||||
cost_item_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
contracted_cost_items: str
|
||||
quantity_types: str
|
||||
product_quantity_names: str
|
||||
process_quantity_names: str
|
||||
resource_quantity_names: str
|
||||
active_cost_item_quantity_id: int
|
||||
quantity_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
cost_types: Literal["FIXED", "SUM", "CATEGORY"]
|
||||
cost_category: str
|
||||
fixed_cost_value: float
|
||||
active_cost_value_id: int
|
||||
cost_value_editing_type: str
|
||||
cost_value_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
cost_value_formula: str
|
||||
cost_column: str
|
||||
should_show_column_ui: bool
|
||||
should_show_currency_ui: bool
|
||||
columns: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
columns_storage: bpy.types.bpy_prop_collection_idprop[ScheduleColumn]
|
||||
active_column_index: int
|
||||
cost_item_products: bpy.types.bpy_prop_collection_idprop[CostItemQuantity]
|
||||
active_cost_item_product_index: int
|
||||
cost_item_processes: bpy.types.bpy_prop_collection_idprop[CostItemQuantity]
|
||||
active_cost_item_process_index: int
|
||||
cost_item_resources: bpy.types.bpy_prop_collection_idprop[CostItemQuantity]
|
||||
active_cost_item_resource_index: int
|
||||
cost_item_type_products: bpy.types.bpy_prop_collection_idprop[CostItemType]
|
||||
active_cost_item_type_product_index: int
|
||||
schedule_of_rates: str
|
||||
cost_item_rates: bpy.types.bpy_prop_collection_idprop[CostItem]
|
||||
active_cost_item_rate_index: int
|
||||
contracted_cost_item_rates: str
|
||||
product_cost_items: bpy.types.bpy_prop_collection_idprop[CostItemQuantity]
|
||||
active_product_cost_item_index: int
|
||||
enable_reorder: bool
|
||||
show_nested_elements: bool
|
||||
show_nested_tasks: bool
|
||||
show_nested_resources: bool
|
||||
change_cost_item_parent: bool
|
||||
show_cost_item_operators: bool
|
||||
currency: str
|
||||
custom_currency: str
|
||||
|
||||
@@ -16,14 +16,17 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
import bpy
|
||||
import bonsai.bim.helper
|
||||
import bonsai.bim.module.cost.prop as CostProp
|
||||
import bonsai.tool as tool
|
||||
from bpy.types import Panel, UIList
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.module.cost.data import CostSchedulesData
|
||||
from typing import Any
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.module.cost.prop import BIMCostProperties, CostItemQuantity
|
||||
|
||||
|
||||
class BIM_PT_cost_schedules(Panel):
|
||||
@@ -37,14 +40,14 @@ class BIM_PT_cost_schedules(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
file = IfcStore.get_file()
|
||||
file = tool.Ifc.get()
|
||||
return file and hasattr(file, "schema") and file.schema != "IFC2X3"
|
||||
|
||||
def draw(self, context):
|
||||
if not CostSchedulesData.is_loaded:
|
||||
CostSchedulesData.load()
|
||||
|
||||
self.props = context.scene.BIMCostProperties
|
||||
self.props = tool.Cost.get_cost_props()
|
||||
if not self.props.active_cost_schedule_id:
|
||||
row = self.layout.row(align=True)
|
||||
if CostSchedulesData.data["total_cost_schedules"]:
|
||||
@@ -300,7 +303,7 @@ class BIM_PT_cost_item_types(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.BIMCostProperties
|
||||
props = tool.Cost.get_cost_props()
|
||||
total_cost_items = len(props.cost_items)
|
||||
if not props.active_cost_schedule_id:
|
||||
return False
|
||||
@@ -313,7 +316,7 @@ class BIM_PT_cost_item_types(Panel):
|
||||
return False
|
||||
|
||||
def draw(self, context):
|
||||
self.props = context.scene.BIMCostProperties
|
||||
self.props = tool.Cost.get_cost_props()
|
||||
cost_item = self.props.cost_items[self.props.active_cost_item_index]
|
||||
grid = self.layout.grid_flow(columns=3, even_columns=True)
|
||||
|
||||
@@ -349,8 +352,8 @@ class BIM_PT_cost_item_types(Panel):
|
||||
|
||||
row2 = col.row()
|
||||
row2.template_list(
|
||||
"BIM_UL_cost_item_quantities",
|
||||
"",
|
||||
BIM_UL_cost_item_quantities.__name__,
|
||||
BIM_UL_cost_item_quantities.__name__ + "_cost_item_processes",
|
||||
self.props,
|
||||
"cost_item_processes",
|
||||
self.props,
|
||||
@@ -366,8 +369,8 @@ class BIM_PT_cost_item_types(Panel):
|
||||
|
||||
row2 = col.row()
|
||||
row2.template_list(
|
||||
"BIM_UL_cost_item_quantities",
|
||||
"",
|
||||
BIM_UL_cost_item_quantities.__name__,
|
||||
BIM_UL_cost_item_quantities.__name__ + "_cost_item_resources",
|
||||
self.props,
|
||||
"cost_item_resources",
|
||||
self.props,
|
||||
@@ -386,7 +389,7 @@ class BIM_PT_cost_item_quantities(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.BIMCostProperties
|
||||
props = tool.Cost.get_cost_props()
|
||||
total_cost_items = len(props.cost_items)
|
||||
if not props.active_cost_schedule_id:
|
||||
return False
|
||||
@@ -399,7 +402,7 @@ class BIM_PT_cost_item_quantities(Panel):
|
||||
return False
|
||||
|
||||
def draw(self, context):
|
||||
self.props = context.scene.BIMCostProperties
|
||||
self.props = tool.Cost.get_cost_props()
|
||||
|
||||
cost_item = self.props.cost_items[self.props.active_cost_item_index]
|
||||
|
||||
@@ -436,8 +439,8 @@ class BIM_PT_cost_item_quantities(Panel):
|
||||
row2.prop(self.props, "show_nested_elements", text="Show nested")
|
||||
row2 = col.row()
|
||||
row2.template_list(
|
||||
"BIM_UL_cost_item_quantities",
|
||||
"",
|
||||
BIM_UL_cost_item_quantities.__name__,
|
||||
BIM_UL_cost_item_quantities.__name__ + "_cost_item_products",
|
||||
self.props,
|
||||
"cost_item_products",
|
||||
self.props,
|
||||
@@ -463,7 +466,7 @@ class BIM_PT_cost_item_quantities(Panel):
|
||||
row2.label(text="Tasks ({})".format(total_cost_item_processes))
|
||||
|
||||
tprops = tool.Sequence.get_task_tree_props()
|
||||
wprops = context.scene.BIMWorkScheduleProperties
|
||||
wprops = tool.Sequence.get_work_schedule_props()
|
||||
if tprops.tasks and wprops.active_task_index < len(tprops.tasks):
|
||||
if has_quantity_names:
|
||||
op = row2.operator("bim.assign_cost_item_quantity", text="", icon="PROPERTIES")
|
||||
@@ -481,8 +484,8 @@ class BIM_PT_cost_item_quantities(Panel):
|
||||
|
||||
row2 = col.row()
|
||||
row2.template_list(
|
||||
"BIM_UL_cost_item_quantities",
|
||||
"",
|
||||
BIM_UL_cost_item_quantities.__name__,
|
||||
BIM_UL_cost_item_quantities.__name__ + "_cost_item_processes",
|
||||
self.props,
|
||||
"cost_item_processes",
|
||||
self.props,
|
||||
@@ -529,8 +532,8 @@ class BIM_PT_cost_item_quantities(Panel):
|
||||
|
||||
row2 = col.row()
|
||||
row2.template_list(
|
||||
"BIM_UL_cost_item_quantities",
|
||||
"",
|
||||
BIM_UL_cost_item_quantities.__name__,
|
||||
BIM_UL_cost_item_quantities.__name__ + "_cost_item_resources",
|
||||
self.props,
|
||||
"cost_item_resources",
|
||||
self.props,
|
||||
@@ -558,7 +561,7 @@ class BIM_PT_cost_item_rates(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.BIMCostProperties
|
||||
props = tool.Cost.get_cost_props()
|
||||
total_cost_items = len(props.cost_items)
|
||||
if not props.active_cost_schedule_id:
|
||||
return False
|
||||
@@ -571,7 +574,7 @@ class BIM_PT_cost_item_rates(Panel):
|
||||
return False
|
||||
|
||||
def draw(self, context):
|
||||
self.props = context.scene.BIMCostProperties
|
||||
self.props = tool.Cost.get_cost_props()
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "schedule_of_rates", text="")
|
||||
if self.props.active_cost_item_rate_index < len(self.props.cost_item_rates):
|
||||
@@ -607,13 +610,14 @@ class BIM_UL_cost_items_trait:
|
||||
split2.label(text="Quantity")
|
||||
split2.label(text="Value")
|
||||
|
||||
for column in bpy.context.scene.BIMCostProperties.columns:
|
||||
props = tool.Cost.get_cost_props()
|
||||
for column in props.columns:
|
||||
split2.label(text=column.name)
|
||||
split2.label(text="Total Cost")
|
||||
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
self.props = context.scene.BIMCostProperties
|
||||
self.props = tool.Cost.get_cost_props()
|
||||
cost_item = CostSchedulesData.data["cost_items"][item.ifc_definition_id]
|
||||
row = layout.row(align=True)
|
||||
|
||||
@@ -709,18 +713,16 @@ class BIM_UL_cost_items_trait:
|
||||
|
||||
|
||||
class BIM_UL_cost_items(BIM_UL_cost_items_trait, UIList):
|
||||
def __init__(self):
|
||||
self.contract_operator = "bim.contract_cost_item"
|
||||
self.expand_operator = "bim.expand_cost_item"
|
||||
contract_operator = "bim.contract_cost_item"
|
||||
expand_operator = "bim.expand_cost_item"
|
||||
|
||||
|
||||
class BIM_UL_cost_item_rates(BIM_UL_cost_items_trait, UIList):
|
||||
# A schedule of rates UIList is identical to a regular cost items UIList but
|
||||
# we want a separate UIList instance so that you can browse both lists
|
||||
# independently in Blender. So we use a trait.
|
||||
def __init__(self):
|
||||
self.contract_operator = "bim.contract_cost_item_rate"
|
||||
self.expand_operator = "bim.expand_cost_item_rate"
|
||||
contract_operator = "bim.contract_cost_item_rate"
|
||||
expand_operator = "bim.expand_cost_item_rate"
|
||||
|
||||
def draw_quantity_column(self, layout, cost_item):
|
||||
self.draw_uom_column(layout, cost_item)
|
||||
@@ -731,7 +733,6 @@ class BIM_UL_cost_item_rates(BIM_UL_cost_items_trait, UIList):
|
||||
|
||||
class BIM_UL_cost_columns(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
props = context.scene.BIMCostProperties
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
row.prop(item, "name", emboss=False, text="")
|
||||
@@ -740,7 +741,7 @@ class BIM_UL_cost_columns(UIList):
|
||||
|
||||
class BIM_UL_cost_item_types(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
props = context.scene.BIMCostProperties
|
||||
props = tool.Cost.get_cost_props()
|
||||
cost_item = props.cost_items[props.active_cost_item_index]
|
||||
|
||||
if item:
|
||||
@@ -752,9 +753,17 @@ class BIM_UL_cost_item_types(UIList):
|
||||
|
||||
|
||||
class BIM_UL_cost_item_quantities(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
props = context.scene.BIMCostProperties
|
||||
cost_item = props.cost_items[props.active_cost_item_index]
|
||||
def draw_item(
|
||||
self,
|
||||
context: bpy.types.Context,
|
||||
layout: bpy.types.UILayout,
|
||||
data: BIMCostProperties,
|
||||
item: CostItemQuantity,
|
||||
icon,
|
||||
active_data,
|
||||
active_propname,
|
||||
):
|
||||
cost_item = data.cost_items[data.active_cost_item_index]
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
op = row.operator("bim.select_product", text="", icon="RESTRICT_SELECT_OFF")
|
||||
@@ -791,15 +800,15 @@ class BIM_PT_Costing_Tools(Panel):
|
||||
bl_parent_id = "BIM_PT_tab_cost"
|
||||
|
||||
def draw(self, context):
|
||||
self.props = context.scene.BIMCostProperties
|
||||
props = tool.Cost.get_cost_props()
|
||||
row = self.layout.row()
|
||||
row.operator("bim.load_product_cost_items", icon="FILE_REFRESH")
|
||||
row = self.layout.row()
|
||||
row.template_list(
|
||||
"BIM_UL_product_cost_items",
|
||||
"",
|
||||
self.props,
|
||||
props,
|
||||
"product_cost_items",
|
||||
self.props,
|
||||
props,
|
||||
"active_product_cost_item_index",
|
||||
)
|
||||
|
||||
@@ -18,9 +18,13 @@
|
||||
|
||||
import bpy
|
||||
from bpy.types import PropertyGroup
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
class BIMCoveringProperties(PropertyGroup):
|
||||
ceiling_height: bpy.props.FloatProperty(
|
||||
name="ceiling_height", default=2.7, subtype="DISTANCE", description="Ceiling height"
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
ceiling_height: float
|
||||
|
||||
@@ -24,6 +24,7 @@ import bonsai.tool as tool
|
||||
from bonsai.bim.helper import prop_with_search
|
||||
from bonsai.bim.module.model.data import AuthoringData
|
||||
from bpy.types import WorkSpaceTool
|
||||
from functools import partial
|
||||
|
||||
|
||||
class CoveringTool(WorkSpaceTool):
|
||||
@@ -45,17 +46,15 @@ class CoveringTool(WorkSpaceTool):
|
||||
CoveringToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
|
||||
|
||||
|
||||
def add_layout_hotkey(layout: bpy.types.UILayout, text: str, hotkey: str, description: str) -> None:
|
||||
args = ("covering", layout, text, hotkey, description)
|
||||
tool.Blender.add_layout_hotkey_operator(*args)
|
||||
add_layout_hotkey = partial(tool.Blender.add_layout_hotkey_operator, tool_name="covering", module_name=__name__)
|
||||
|
||||
|
||||
class CoveringToolUI:
|
||||
@classmethod
|
||||
def draw(cls, context, layout, ifc_element_type=None):
|
||||
cls.layout = layout
|
||||
cls.props = context.scene.BIMModelProperties
|
||||
cls.covering_props = context.scene.BIMCoveringProperties
|
||||
cls.props = tool.Model.get_model_props()
|
||||
cls.covering_props = tool.Covering.get_covering_props()
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
if not tool.Ifc.get():
|
||||
@@ -88,7 +87,7 @@ class CoveringToolUI:
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_A")
|
||||
row.operator("bim.add_constr_type_instance", text="Add")
|
||||
row.operator("bim.add_occurrence", text="Add")
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
@@ -136,8 +135,8 @@ class CoveringToolUI:
|
||||
if AuthoringData.data["ifc_classes"]:
|
||||
if cls.props.ifc_class:
|
||||
box = cls.layout.box()
|
||||
if AuthoringData.data["type_thumbnail"]:
|
||||
box.template_icon(icon_value=AuthoringData.data["type_thumbnail"], scale=5)
|
||||
if thumbnail := AuthoringData.data["relating_type_data"].get("thumbnail"):
|
||||
box.template_icon(icon_value=thumbnail, scale=5)
|
||||
else:
|
||||
op = box.operator("bim.load_type_thumbnails", text="Load Thumbnails", icon="FILE_REFRESH")
|
||||
op.ifc_class = cls.props.ifc_class
|
||||
@@ -145,8 +144,8 @@ class CoveringToolUI:
|
||||
|
||||
class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.covering_hotkey"
|
||||
bl_label = "Hotkey"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_label = ""
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
hotkey: bpy.props.StringProperty()
|
||||
description: bpy.props.StringProperty()
|
||||
|
||||
@@ -159,12 +158,11 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return operator.description or ""
|
||||
|
||||
def _execute(self, context):
|
||||
# self.props = context.scene.BIMCoveringProperties
|
||||
self.props = tool.Covering.get_covering_props()
|
||||
getattr(self, f"hotkey_{self.hotkey}")()
|
||||
|
||||
def invoke(self, context, event):
|
||||
# https://blender.stackexchange.com/questions/276035/how-do-i-make-operators-remember-their-property-values-when-called-from-a-hotkey
|
||||
# self.props = context.scene.BIMSpatialProperties
|
||||
return self.execute(context)
|
||||
|
||||
def draw(self, context):
|
||||
@@ -175,22 +173,22 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
container = tool.Root.get_default_container()
|
||||
|
||||
if AuthoringData.data["predefined_type"] == "FLOORING":
|
||||
if AuthoringData.data["relating_type_data"].get("predefined_type") == "FLOORING":
|
||||
if element and bpy.context.selected_objects and element.is_a("IfcWall"):
|
||||
bpy.ops.bim.add_instance_flooring_coverings_from_walls()
|
||||
elif container:
|
||||
bpy.ops.bim.add_instance_flooring_covering_from_cursor()
|
||||
else:
|
||||
bpy.ops.bim.add_constr_type_instance()
|
||||
elif AuthoringData.data["predefined_type"] == "CEILING":
|
||||
bpy.ops.bim.add_occurrence()
|
||||
elif AuthoringData.data["relating_type_data"].get("predefined_type") == "CEILING":
|
||||
if element and bpy.context.selected_objects and element.is_a("IfcWall"):
|
||||
bpy.ops.bim.add_instance_ceiling_coverings_from_walls()
|
||||
elif container:
|
||||
bpy.ops.bim.add_instance_ceiling_covering_from_cursor()
|
||||
else:
|
||||
bpy.ops.bim.add_constr_type_instance()
|
||||
bpy.ops.bim.add_occurrence()
|
||||
else:
|
||||
bpy.ops.bim.add_constr_type_instance()
|
||||
bpy.ops.bim.add_occurrence()
|
||||
|
||||
def hotkey_S_G(self):
|
||||
active_obj = bpy.context.active_object
|
||||
|
||||
@@ -20,8 +20,8 @@ import bpy
|
||||
import json
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.element
|
||||
import bonsai.tool as tool
|
||||
from math import degrees, atan2
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
|
||||
from .api import Api
|
||||
|
||||
@@ -92,7 +92,7 @@ class RunAnalysis(bpy.types.Operator):
|
||||
bl_label = "Run Analysis"
|
||||
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
self.file = tool.Ifc.get()
|
||||
self.inputs = {
|
||||
"floors": [],
|
||||
"walls": [],
|
||||
@@ -181,12 +181,12 @@ class RunAnalysis(bpy.types.Operator):
|
||||
if modifier.type == "TRIANGULATE":
|
||||
return True
|
||||
|
||||
def get_covetool_category(self, obj):
|
||||
def get_covetool_category(self, obj: bpy.types.Object):
|
||||
if not hasattr(obj, "data") or not isinstance(obj.data, bpy.types.Mesh):
|
||||
return
|
||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
||||
if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)):
|
||||
return
|
||||
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
element = self.file.by_id(ifc_id)
|
||||
ifc_class = element.is_a()
|
||||
if "IfcSlab" in ifc_class:
|
||||
return "floors"
|
||||
|
||||
@@ -27,7 +27,7 @@ import ifcopenshell
|
||||
import ifcopenshell.util.selector
|
||||
import bonsai.tool as tool
|
||||
import bonsai.bim.module.drawing.scheduler as scheduler
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
||||
from bonsai.bim.handler import refresh_ui_data
|
||||
from typing import TYPE_CHECKING
|
||||
from collections import Counter
|
||||
@@ -43,7 +43,8 @@ class AddCsvAttribute(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
attribute = context.scene.CsvProperties.csv_attributes.add()
|
||||
props = tool.Blender.get_csv_props()
|
||||
props.csv_attributes.add()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -54,7 +55,8 @@ class RemoveCsvAttribute(bpy.types.Operator):
|
||||
index: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.CsvProperties.csv_attributes.remove(self.index)
|
||||
props = tool.Blender.get_csv_props()
|
||||
props.csv_attributes.remove(self.index)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -65,7 +67,8 @@ class RemoveAllCsvAttributes(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.CsvProperties.csv_attributes.clear()
|
||||
props = tool.Blender.get_csv_props()
|
||||
props.csv_attributes.clear()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -77,8 +80,9 @@ class ReorderCsvAttribute(bpy.types.Operator):
|
||||
new_index: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
old = context.scene.CsvProperties.csv_attributes[self.old_index]
|
||||
new = context.scene.CsvProperties.csv_attributes[self.new_index]
|
||||
props = tool.Blender.get_csv_props()
|
||||
old = props.csv_attributes[self.old_index]
|
||||
new = props.csv_attributes[self.new_index]
|
||||
props = ["name", "header", "sort", "group", "varies_value", "summary", "formatting"]
|
||||
for prop in props:
|
||||
value = getattr(new, prop)
|
||||
@@ -87,16 +91,16 @@ class ReorderCsvAttribute(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ImportCsvAttributes(bpy.types.Operator):
|
||||
class ImportCsvAttributes(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.import_csv_attributes"
|
||||
bl_label = "Load CSV Settings"
|
||||
bl_description = "Import a json template for CSV export"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filename_ext = ".json"
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.CsvProperties
|
||||
props = tool.Blender.get_csv_props()
|
||||
data = json.load(open(self.filepath))
|
||||
tool.Search.import_filter_query(data["query"], props.filter_groups)
|
||||
|
||||
@@ -122,22 +126,17 @@ class ImportCsvAttributes(bpy.types.Operator):
|
||||
setattr(new, prop, attribute[prop])
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class ExportCsvAttributes(bpy.types.Operator):
|
||||
class ExportCsvAttributes(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "bim.export_csv_attributes"
|
||||
bl_label = "Save CSV Settings"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Save a json template for CSV export"
|
||||
filename_ext = ".json"
|
||||
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.CsvProperties
|
||||
props = tool.Blender.get_csv_props()
|
||||
|
||||
settings = {}
|
||||
for prop in [
|
||||
@@ -176,43 +175,45 @@ class ExportCsvAttributes(bpy.types.Operator):
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class ExportIfcCsv(bpy.types.Operator):
|
||||
class ExportIfcCsv(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "bim.export_ifccsv"
|
||||
bl_label = "Export IFC"
|
||||
bl_description = "Export IFC data as a spreadsheet."
|
||||
filename_ext = ".csv"
|
||||
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.CsvProperties
|
||||
props = tool.Blender.get_csv_props()
|
||||
if not props.should_load_from_memory and not props.csv_ifc_file:
|
||||
cls.poll_message_set("Select an IFC file or use 'load from memory' if it's loaded in Bonsai.")
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
props = tool.Blender.get_csv_props()
|
||||
if props.format == "web":
|
||||
return "Open Web UI for spreadsheet data export."
|
||||
return f"Export IFC data as a spreadsheet by the provided filepath in '{props.format}' format ."
|
||||
|
||||
def invoke(self, context, event):
|
||||
props = context.scene.CsvProperties
|
||||
props = tool.Blender.get_csv_props()
|
||||
if props.format == "web":
|
||||
return self.execute(context)
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, f".{props.format}")
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
self.filter_glob = f"*.{props.format}"
|
||||
self.filename_ext = f".{props.format}"
|
||||
return ExportHelper.invoke(self, context, event)
|
||||
|
||||
def execute(self, context):
|
||||
import ifccsv
|
||||
|
||||
props = context.scene.CsvProperties
|
||||
props = tool.Blender.get_csv_props()
|
||||
self.filepath = bpy.path.ensure_ext(self.filepath, f".{props.format}")
|
||||
if props.should_load_from_memory:
|
||||
ifc_file = IfcStore.get_file()
|
||||
ifc_file = tool.Ifc.get()
|
||||
else:
|
||||
ifc_file = ifcopenshell.open(props.csv_ifc_file)
|
||||
results = ifcopenshell.util.selector.filter_elements(
|
||||
@@ -288,16 +289,16 @@ class ExportIfcCsv(bpy.types.Operator):
|
||||
]
|
||||
|
||||
|
||||
class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
bl_idname = "bim.import_ifccsv"
|
||||
bl_label = "Import to IFC"
|
||||
bl_description = "Import IFC data from a spreadsheet."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filter_glob: bpy.props.StringProperty(default="*.csv;*.ods;*.xlsx", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.CsvProperties
|
||||
props = tool.Blender.get_csv_props()
|
||||
if not props.should_load_from_memory and not props.csv_ifc_file:
|
||||
cls.poll_message_set("Select an IFC file or use 'load from memory' if it's loaded in Bonsai.")
|
||||
return False
|
||||
@@ -305,16 +306,14 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".csv")
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
return ImportHelper.invoke(self, context, event)
|
||||
|
||||
def _execute(self, context):
|
||||
import ifccsv
|
||||
|
||||
props = context.scene.CsvProperties
|
||||
props = tool.Blender.get_csv_props()
|
||||
if props.should_load_from_memory:
|
||||
ifc_file = IfcStore.get_file()
|
||||
ifc_file = tool.Ifc.get()
|
||||
else:
|
||||
ifc_file = ifcopenshell.open(props.csv_ifc_file)
|
||||
ifc_csv = ifccsv.IfcCsv()
|
||||
@@ -338,18 +337,15 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectCsvIfcFile(bpy.types.Operator):
|
||||
class SelectCsvIfcFile(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.select_csv_ifc_file"
|
||||
bl_label = "Select CSV IFC File"
|
||||
bl_description = "Select IFC file for spreadsheet import/export."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filename_ext = ".ifc"
|
||||
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.CsvProperties.csv_ifc_file = self.filepath
|
||||
props = tool.Blender.get_csv_props()
|
||||
props.csv_ifc_file = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
@@ -29,6 +29,7 @@ from bpy.props import (
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
|
||||
class CsvAttribute(PropertyGroup):
|
||||
@@ -59,6 +60,15 @@ class CsvAttribute(PropertyGroup):
|
||||
)
|
||||
formatting: StringProperty(default="{{value}}", name="Formatting")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
name: str
|
||||
header: str
|
||||
sort: Literal["NONE", "ASC", "DESC"]
|
||||
group: Literal["NONE", "GROUP", "CONCAT", "VARIES", "SUM", "AVERAGE", "MIN", "MAX"]
|
||||
varies_value: str
|
||||
summary: Literal["NONE", "SUM", "AVERAGE", "MIN", "MAX"]
|
||||
formatting: str
|
||||
|
||||
|
||||
class CsvProperties(PropertyGroup):
|
||||
csv_ifc_file: StringProperty(default="", name="IFC File")
|
||||
@@ -104,3 +114,26 @@ class CsvProperties(PropertyGroup):
|
||||
name="Load from Memory",
|
||||
description="Use IFC file currently loaded in Bonsai",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
csv_ifc_file: str
|
||||
ifc_selector: str
|
||||
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
|
||||
csv_attributes: bpy.types.bpy_prop_collection_idprop[CsvAttribute]
|
||||
should_generate_svg: bool
|
||||
should_preserve_existing: bool
|
||||
include_global_id: bool
|
||||
null_value: str
|
||||
empty_value: str
|
||||
true_value: str
|
||||
false_value: str
|
||||
concat_value: str
|
||||
csv_delimiter: Literal["NONE", "ASC", "DESC"]
|
||||
format: Literal["csv", "xlsx", "ods", "web"]
|
||||
csv_custom_delimiter: str
|
||||
should_show_settings: bool
|
||||
should_show_sort: bool
|
||||
should_show_group: bool
|
||||
should_show_summary: bool
|
||||
should_show_formatting: bool
|
||||
should_load_from_memory: bool
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bonsai.bim.helper
|
||||
import bonsai.tool as tool
|
||||
from bpy.types import Panel
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.module.search.data import SearchData
|
||||
|
||||
|
||||
@@ -33,11 +33,9 @@ class BIM_PT_ifccsv(Panel):
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
props = tool.Blender.get_csv_props()
|
||||
|
||||
scene = context.scene
|
||||
props = scene.CsvProperties
|
||||
|
||||
if IfcStore.get_file():
|
||||
if tool.Ifc.get():
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "should_load_from_memory")
|
||||
row.operator("bim.import_csv_attributes", icon="IMPORT", text="")
|
||||
@@ -49,7 +47,7 @@ class BIM_PT_ifccsv(Panel):
|
||||
row.operator("bim.export_csv_attributes", icon="EXPORT", text="")
|
||||
row.prop(props, "should_show_settings", icon="PREFERENCES", text="")
|
||||
|
||||
if not IfcStore.get_file() or not props.should_load_from_memory:
|
||||
if not tool.Ifc.get() or not props.should_load_from_memory:
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "csv_ifc_file")
|
||||
row.operator("bim.select_csv_ifc_file", icon="FILE_FOLDER", text="")
|
||||
@@ -126,5 +124,8 @@ class BIM_PT_ifccsv(Panel):
|
||||
row.operator("bim.remove_csv_attribute", icon="X", text="").index = index
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.export_ifccsv", icon="EXPORT", text="Export IFC to " + props.format.upper())
|
||||
if props.format == "web":
|
||||
row.operator("bim.export_ifccsv", icon="EXPORT", text="Open Web UI")
|
||||
else:
|
||||
row.operator("bim.export_ifccsv", icon="EXPORT", text="Export IFC to " + props.format.upper())
|
||||
row.operator("bim.import_ifccsv", icon="IMPORT")
|
||||
|
||||
@@ -23,6 +23,7 @@ import time
|
||||
import random
|
||||
import logging
|
||||
import subprocess
|
||||
import platform
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.geom
|
||||
@@ -36,6 +37,7 @@ import bonsai.core.profile
|
||||
import bonsai.core.type
|
||||
import bonsai.bim.handler
|
||||
import bonsai.bim.import_ifc as import_ifc
|
||||
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
||||
from pathlib import Path
|
||||
from bonsai import get_debug_info, format_debug_info
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
@@ -78,10 +80,10 @@ class PrintIfcFile(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return IfcStore.get_file()
|
||||
return tool.Ifc.get()
|
||||
|
||||
def execute(self, context):
|
||||
print(IfcStore.get_file().wrapped_data.to_string())
|
||||
print(tool.Ifc.get().wrapped_data.to_string())
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -101,13 +103,14 @@ class ConvertToBlender(bpy.types.Operator):
|
||||
if tool.Geometry.has_mesh_properties(data):
|
||||
if data.library:
|
||||
continue
|
||||
data.BIMMeshProperties.ifc_definition_id = 0
|
||||
tool.Geometry.get_mesh_props(data).ifc_definition_id = 0
|
||||
for material in bpy.data.materials:
|
||||
if material.library:
|
||||
continue
|
||||
tool.Ifc.unlink(obj=material)
|
||||
context.scene.BIMProperties.ifc_file = ""
|
||||
context.scene.BIMDebugProperties.attributes.clear()
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
bim_props.ifc_file = ""
|
||||
tool.Debug.get_debug_props().attributes.clear()
|
||||
IfcStore.purge()
|
||||
bonsai.bim.handler.refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
@@ -160,7 +163,8 @@ class ProfileImportIFC(bpy.types.Operator):
|
||||
if not tool.Ifc.get():
|
||||
cls.poll_message_set("No IFC file loaded.")
|
||||
return False
|
||||
if not context.scene.BIMProperties.ifc_file:
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
if not bim_props.ifc_file:
|
||||
cls.poll_message_set("Current IFC file is not saved.")
|
||||
return False
|
||||
return True
|
||||
@@ -184,10 +188,10 @@ class CreateAllShapes(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return IfcStore.get_file()
|
||||
return tool.Ifc.get()
|
||||
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
self.file = tool.Ifc.get()
|
||||
elements = self.file.by_type("IfcElement") + self.file.by_type("IfcSpace")
|
||||
|
||||
total = len(elements)
|
||||
@@ -256,7 +260,7 @@ class CreateShapeFromStepId(bpy.types.Operator):
|
||||
logger = logging.getLogger("ImportIFC")
|
||||
self.ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger)
|
||||
self.file = tool.Ifc.get()
|
||||
element = self.file.by_id(self.step_id or int(context.scene.BIMDebugProperties.step_id))
|
||||
element = self.file.by_id(self.step_id or int(tool.Debug.get_debug_props().step_id))
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("keep-bounding-boxes", True)
|
||||
if self.should_include_curves:
|
||||
@@ -309,7 +313,7 @@ class RewindInspector(bpy.types.Operator):
|
||||
bl_description = "Rewind the Inspector to the previously inspected element"
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMDebugProperties
|
||||
props = tool.Debug.get_debug_props()
|
||||
total_breadcrumbs = len(props.step_id_breadcrumb)
|
||||
if total_breadcrumbs < 2:
|
||||
return {"FINISHED"}
|
||||
@@ -328,13 +332,13 @@ class InspectFromStepId(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return IfcStore.get_file()
|
||||
return tool.Ifc.get()
|
||||
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
debug_props = context.scene.BIMDebugProperties
|
||||
self.file = tool.Ifc.get()
|
||||
debug_props = tool.Debug.get_debug_props()
|
||||
debug_props.active_step_id = self.step_id
|
||||
crumb = context.scene.BIMDebugProperties.step_id_breadcrumb.add()
|
||||
crumb = debug_props.step_id_breadcrumb.add()
|
||||
crumb.name = str(self.step_id)
|
||||
element = self.file.by_id(self.step_id)
|
||||
debug_props.attributes.clear()
|
||||
@@ -380,12 +384,12 @@ class InspectFromObject(bpy.types.Operator):
|
||||
def get_active_object_ifc_definition(cls, context: bpy.types.Context) -> Union[int, None]:
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
if ifc_id := obj.BIMObjectProperties.ifc_definition_id:
|
||||
if ifc_id := tool.Blender.get_ifc_definition_id(obj):
|
||||
return ifc_id
|
||||
if (
|
||||
(data := obj.data)
|
||||
and tool.Geometry.has_mesh_properties(data)
|
||||
and (ifc_id := data.BIMMeshProperties.ifc_definition_id)
|
||||
and (ifc_id := tool.Geometry.get_mesh_props(data).ifc_definition_id)
|
||||
):
|
||||
return ifc_id
|
||||
|
||||
@@ -422,7 +426,7 @@ class PrintObjectPlacement(bpy.types.Operator):
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
placement = ifcopenshell.util.placement.get_local_placement(IfcStore.get_file().by_id(self.step_id))
|
||||
placement = ifcopenshell.util.placement.get_local_placement(tool.Ifc.get().by_id(self.step_id))
|
||||
if self.create_empty_object:
|
||||
bpy.ops.object.empty_add(type="ARROWS")
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
@@ -438,28 +442,25 @@ class ParseExpress(bpy.types.Operator):
|
||||
bl_label = "Parse Express"
|
||||
|
||||
def execute(self, context):
|
||||
core.parse_express(tool.Debug, context.scene.BIMDebugProperties.express_file)
|
||||
props = tool.Debug.get_debug_props()
|
||||
core.parse_express(tool.Debug, props.express_file)
|
||||
bonsai.bim.handler.refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectExpressFile(bpy.types.Operator):
|
||||
class SelectExpressFile(bpy.types.Operator, ImportHelper):
|
||||
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):
|
||||
props = tool.Debug.get_debug_props()
|
||||
if os.path.exists(self.filepath) and "exp" in os.path.splitext(self.filepath)[1]:
|
||||
context.scene.BIMDebugProperties.express_file = self.filepath
|
||||
props.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"
|
||||
@@ -497,7 +498,7 @@ class PrintUnusedElementStats(bpy.types.Operator):
|
||||
ignore_styled_items: bpy.props.BoolProperty(name="Ignore Styled Items", default=True)
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMDebugProperties
|
||||
props = tool.Debug.get_debug_props()
|
||||
# ignore some classes that could have zero 0 inverse references by their nature
|
||||
ignore_classes = []
|
||||
if self.ignore_contexts:
|
||||
@@ -509,6 +510,7 @@ class PrintUnusedElementStats(bpy.types.Operator):
|
||||
if self.ignore_styled_items:
|
||||
ignore_classes += ["IfcStyledItem"]
|
||||
ignore_classes += [
|
||||
"IfcDocumentReference", # Document references for sheet elements (drawings, schedules, etc).
|
||||
"IfcIndexedColourMap", # Only referenced by inverse attributes.
|
||||
"IfcIndexedTextureMap", # Only referenced by inverse attributes.
|
||||
]
|
||||
@@ -518,21 +520,22 @@ class PrintUnusedElementStats(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator, ExportHelper):
|
||||
bl_idname = "bim.purge_unused_elements_by_class"
|
||||
bl_label = "Purge Unused Elements By Class"
|
||||
bl_description = (
|
||||
"Will find all elements of class that have no inverse references and will remove them, use very carefully.\n"
|
||||
"If IFC class is provided in neighbour field, will purge only elemnts of the provided class. Otherwise will purge all white-listed elements.\n"
|
||||
"If IFC class is provided in neighbour field, will purge only elements of the provided class. Otherwise will purge all white-listed elements.\n"
|
||||
"ALT+CLICK to provide a path where to save the IFC file with the removed elements (note changes will be applied to the current IFC too)"
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filename_ext = ".ifc"
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
|
||||
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
if event.type == "LEFTMOUSE" and event.alt:
|
||||
context.window_manager.fileselect_add(self)
|
||||
return ExportHelper.invoke(self, context, event)
|
||||
return self.execute(context)
|
||||
|
||||
@classmethod
|
||||
@@ -543,7 +546,7 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMDebugProperties
|
||||
props = tool.Debug.get_debug_props()
|
||||
if props.ifc_class_purge:
|
||||
purged_elements = core.purge_unused_elements(tool.Ifc, tool.Debug, props.ifc_class_purge)
|
||||
self.report({"INFO"}, f"{purged_elements} unused elements found and removed.")
|
||||
@@ -609,18 +612,30 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"IfcUnitAssignment",
|
||||
"IfcVirtualGridIntersection",
|
||||
]
|
||||
whitelist_exceptions = {
|
||||
# Document references for sheet elements (drawings, schedules, etc).
|
||||
"IfcExternalReference": ("IfcDocumentReference",),
|
||||
}
|
||||
|
||||
total_purged = 0
|
||||
schema = tool.Ifc.schema()
|
||||
while True:
|
||||
total_batches = 0
|
||||
print("*" * 100)
|
||||
total_batch_purged = 0
|
||||
for ifc_class in whitelisted_classes:
|
||||
total_class_purged = 0
|
||||
|
||||
# Ensure class is present in the schema.
|
||||
try:
|
||||
elements = tool.Ifc.get().by_type(ifc_class)
|
||||
except:
|
||||
continue # Probably not in this schema?
|
||||
schema.declaration_by_name(ifc_class)
|
||||
except RuntimeError:
|
||||
continue
|
||||
|
||||
elements = tool.Ifc.get().by_type(ifc_class)
|
||||
if ifc_class in whitelist_exceptions:
|
||||
elements = [e for e in elements if not any(e.is_a(c) for c in whitelist_exceptions[ifc_class])]
|
||||
|
||||
to_purge = set()
|
||||
for element in elements:
|
||||
try:
|
||||
@@ -687,15 +702,17 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if purged == 0:
|
||||
return
|
||||
|
||||
scene = context.scene
|
||||
if object_type == "PROFILE":
|
||||
if scene.BIMProfileProperties.is_editing:
|
||||
props = tool.Profile.get_profile_props()
|
||||
if props.is_editing:
|
||||
bpy.ops.bim.load_profiles()
|
||||
elif object_type == "STYLE":
|
||||
if scene.BIMStylesProperties.is_editing:
|
||||
props = tool.Style.get_style_props()
|
||||
if props.is_editing:
|
||||
bpy.ops.bim.load_styles()
|
||||
elif object_type == "MATERIAL":
|
||||
if scene.BIMMaterialProperties.is_editing:
|
||||
props = tool.Material.get_material_props()
|
||||
if props.is_editing:
|
||||
bpy.ops.bim.load_materials()
|
||||
|
||||
|
||||
@@ -735,15 +752,17 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if merged == 0:
|
||||
return
|
||||
|
||||
scene = context.scene
|
||||
if object_type == "PROFILE":
|
||||
if scene.BIMProfileProperties.is_editing:
|
||||
props = tool.Profile.get_profile_props()
|
||||
if props.is_editing:
|
||||
bpy.ops.bim.load_profiles()
|
||||
elif object_type == "STYLE":
|
||||
if scene.BIMStylesProperties.is_editing:
|
||||
props = tool.Style.get_style_props()
|
||||
if props.is_editing:
|
||||
bpy.ops.bim.load_styles()
|
||||
elif object_type == "MATERIAL":
|
||||
if scene.BIMMaterialProperties.is_editing:
|
||||
props = tool.Material.get_material_props()
|
||||
if props.is_editing:
|
||||
bpy.ops.bim.load_materials()
|
||||
|
||||
|
||||
@@ -799,7 +818,7 @@ class DebugActiveDrawing(bpy.types.Operator):
|
||||
)
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
props = context.scene.DocProperties
|
||||
props = tool.Drawing.get_document_props()
|
||||
drawing_item = props.drawings[props.active_drawing_index]
|
||||
drawing = tool.Ifc.get().by_id(drawing_item.ifc_definition_id)
|
||||
|
||||
@@ -934,10 +953,20 @@ class RestartBlender(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
# Save preferences manually since we're restarting Blender using .execv
|
||||
# and it doens't have a chance to save them on exit.
|
||||
if context.preferences.use_preferences_save:
|
||||
bpy.ops.wm.save_userpref()
|
||||
|
||||
ms_store_app_id = tool.Blender.get_microsoft_store_app_id()
|
||||
if not ms_store_app_id:
|
||||
path = bpy.app.binary_path
|
||||
os.execv(path, sys.argv)
|
||||
if platform.system() == "Windows":
|
||||
args = sys.argv[1:]
|
||||
command_line = subprocess.list2cmdline([path] + args)
|
||||
os.execv(path, [command_line])
|
||||
else:
|
||||
os.execv(path, sys.argv)
|
||||
else:
|
||||
# Microsoft apps do not allow launching blender.exe directly
|
||||
# since Blender folder is kind of private.
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from bonsai.bim.prop import StrProperty, Attribute
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
@@ -28,6 +29,9 @@ from bpy.props import (
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
from typing import TYPE_CHECKING, Literal, get_args
|
||||
|
||||
DisplayType = Literal["BOUNDS", "WIRE", "SOLID", "TEXTURED"]
|
||||
|
||||
|
||||
class BIMDebugProperties(PropertyGroup):
|
||||
@@ -41,14 +45,23 @@ class BIMDebugProperties(PropertyGroup):
|
||||
inverse_references: CollectionProperty(name="Inverse References", type=Attribute)
|
||||
express_file: StringProperty(name="Express File")
|
||||
display_type: EnumProperty(
|
||||
items=[
|
||||
("BOUNDS", "Bounds", ""),
|
||||
("WIRE", "Wire", ""),
|
||||
("SOLID", "Solid", ""),
|
||||
("TEXTURED", "Textured", ""),
|
||||
],
|
||||
items=[(display_type, display_type.capitalize(), "") for display_type in get_args(DisplayType)],
|
||||
name="Display Type",
|
||||
default="BOUNDS",
|
||||
)
|
||||
ifc_class_purge: StringProperty(name="Unused Elements IFC Class", default="")
|
||||
package_name: StringProperty(name="Package Name", default="")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
step_id: int
|
||||
number_of_polygons: int
|
||||
percentile_of_polygons: int
|
||||
active_step_id: int
|
||||
step_id_breadcrumb: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
inverse_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
inverse_references: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
express_file: str
|
||||
display_type: str
|
||||
ifc_class_purge: str
|
||||
package_name: str
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import bonsai.tool as tool
|
||||
from bpy.types import Panel
|
||||
|
||||
|
||||
@@ -32,10 +33,11 @@ class BIM_PT_debug(Panel):
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
props = context.scene.BIMDebugProperties
|
||||
props = tool.Debug.get_debug_props()
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(context.scene.BIMProperties, "ifc_file", text="")
|
||||
row.prop(bim_props, "ifc_file", text="")
|
||||
row.operator("bim.validate_ifc_file", icon="CHECKMARK", text="")
|
||||
row.operator("bim.select_ifc_file", icon="FILE_FOLDER", text="")
|
||||
|
||||
@@ -87,27 +89,25 @@ 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").threshold = context.scene.BIMDebugProperties.number_of_polygons
|
||||
row.operator("bim.select_high_polygon_meshes").threshold = props.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.operator("bim.select_highest_polygon_meshes").percentile = props.percentile_of_polygons
|
||||
row.prop(props, "percentile_of_polygons", text="")
|
||||
|
||||
row = layout.split(factor=0.5, align=True)
|
||||
row.prop(props, "display_type", text="")
|
||||
row.operator("bim.override_display_type").display = context.scene.BIMDebugProperties.display_type
|
||||
row.operator("bim.override_display_type").display = props.display_type
|
||||
|
||||
layout.operator("bim.purge_unused_representations")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(context.scene.BIMDebugProperties, "ifc_class_purge", text="")
|
||||
row.prop(props, "ifc_class_purge", text="")
|
||||
row.operator("bim.purge_unused_elements_by_class", text="Purge Orphaned", icon="TRASH")
|
||||
row.operator("bim.print_unused_elements_stats", text="", icon="INFO")
|
||||
|
||||
if context.active_object and context.active_object.data:
|
||||
mprops = context.active_object.data.BIMMeshProperties
|
||||
if context.active_object and (data := context.active_object.data):
|
||||
mprops = tool.Geometry.get_mesh_props(data)
|
||||
row = layout.row()
|
||||
row.operator("bim.get_representation_ifc_parameters")
|
||||
for index, ifc_parameter in enumerate(mprops.ifc_parameters):
|
||||
@@ -123,7 +123,7 @@ class BIM_PT_debug(Panel):
|
||||
row.operator("bim.rewind_inspector", icon="FRAME_PREV", text="")
|
||||
row.prop(props, "active_step_id", text="")
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.inspect_from_step_id").step_id = context.scene.BIMDebugProperties.active_step_id
|
||||
row.operator("bim.inspect_from_step_id").step_id = props.active_step_id
|
||||
row.operator("bim.inspect_from_object")
|
||||
|
||||
if props.attributes:
|
||||
|
||||
@@ -43,7 +43,7 @@ class DiffData:
|
||||
|
||||
@classmethod
|
||||
def diff_json(cls):
|
||||
props = bpy.context.scene.DiffProperties
|
||||
props = tool.Blender.get_diff_props()
|
||||
if not props.diff_json_file:
|
||||
cls.diff = None
|
||||
return
|
||||
|
||||
@@ -24,24 +24,23 @@ import ifcopenshell
|
||||
import bonsai.bim.handler
|
||||
import bonsai.bim.import_ifc
|
||||
import bonsai.tool as tool
|
||||
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
|
||||
|
||||
class SelectDiffJsonFile(bpy.types.Operator):
|
||||
class SelectDiffJsonFile(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.select_diff_json_file"
|
||||
bl_label = "Select Diff JSON File"
|
||||
bl_description = "Select filepath for IFC diff results."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
|
||||
filename_ext = ".json"
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.DiffProperties.diff_json_file = self.filepath
|
||||
props = tool.Blender.get_diff_props()
|
||||
props.diff_json_file = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class VisualiseDiff(bpy.types.Operator):
|
||||
bl_idname = "bim.visualise_diff"
|
||||
@@ -50,7 +49,8 @@ class VisualiseDiff(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
ifc_file = tool.Ifc.get()
|
||||
with open(context.scene.DiffProperties.diff_json_file, "r") as file:
|
||||
props = tool.Blender.get_diff_props()
|
||||
with open(props.diff_json_file, "r") as file:
|
||||
diff = json.load(file)
|
||||
for obj in context.visible_objects:
|
||||
obj.color = (1.0, 1.0, 1.0, 1.0)
|
||||
@@ -65,13 +65,14 @@ class VisualiseDiff(bpy.types.Operator):
|
||||
obj.color = (0.0, 0.0, 0.7, 1.0)
|
||||
continue
|
||||
|
||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
||||
if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)):
|
||||
continue
|
||||
|
||||
ifc_file = ""
|
||||
for scene in obj.users_scene:
|
||||
if scene.BIMProperties.ifc_file:
|
||||
ifc_file = scene.BIMProperties.ifc_file
|
||||
bim_props = tool.Blender.get_bim_props(scene)
|
||||
if bim_props.ifc_file:
|
||||
ifc_file = bim_props.ifc_file
|
||||
if scene.library:
|
||||
break
|
||||
|
||||
@@ -83,7 +84,7 @@ class VisualiseDiff(bpy.types.Operator):
|
||||
element_file = ifc_file
|
||||
|
||||
try:
|
||||
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
element = element_file.by_id(ifc_id)
|
||||
except:
|
||||
continue
|
||||
global_id = getattr(element, "GlobalId", None)
|
||||
@@ -100,55 +101,45 @@ class VisualiseDiff(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectDiffOldFile(bpy.types.Operator):
|
||||
class SelectDiffOldFile(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.select_diff_old_file"
|
||||
bl_label = "Select Diff Old File"
|
||||
bl_description = "Select filepath for an old IFC file to compare."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
|
||||
filename_ext = ".ifc"
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.DiffProperties.old_file = self.filepath
|
||||
props = tool.Blender.get_diff_props()
|
||||
props.old_file = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class SelectDiffNewFile(bpy.types.Operator):
|
||||
class SelectDiffNewFile(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.select_diff_new_file"
|
||||
bl_label = "Select Diff New File"
|
||||
bl_description = "Select filepath for a new IFC file to compare."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
|
||||
filename_ext = ".ifc"
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.DiffProperties.new_file = self.filepath
|
||||
props = tool.Blender.get_diff_props()
|
||||
props.new_file = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class ExecuteIfcDiff(bpy.types.Operator):
|
||||
class ExecuteIfcDiff(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "bim.execute_ifc_diff"
|
||||
bl_label = "Execute IFC Diff"
|
||||
bl_description = "Compare two IFC files and save a json diff report by the provided filepath."
|
||||
filename_ext = ".json"
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def execute(self, context):
|
||||
import ifcdiff
|
||||
|
||||
self.props = context.scene.DiffProperties
|
||||
self.props = tool.Blender.get_diff_props()
|
||||
|
||||
if tool.Ifc.get():
|
||||
if self.props.active_file == "NONE":
|
||||
@@ -237,7 +228,8 @@ class SelectDiffObjects(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
ifc_file = tool.Ifc.get()
|
||||
with open(context.scene.DiffProperties.diff_json_file, "r") as file:
|
||||
props = tool.Blender.get_diff_props()
|
||||
with open(props.diff_json_file, "r") as file:
|
||||
diff = json.load(file)
|
||||
for obj in context.visible_objects:
|
||||
obj.select_set(False)
|
||||
@@ -252,13 +244,14 @@ class SelectDiffObjects(bpy.types.Operator):
|
||||
obj.select_set(True)
|
||||
continue
|
||||
|
||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
||||
if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)):
|
||||
continue
|
||||
|
||||
ifc_file = ""
|
||||
for scene in obj.users_scene:
|
||||
if scene.BIMProperties.ifc_file:
|
||||
ifc_file = scene.BIMProperties.ifc_file
|
||||
bim_props = tool.Blender.get_bim_props(scene)
|
||||
if bim_props.ifc_file:
|
||||
ifc_file = bim_props.ifc_file
|
||||
if scene.library:
|
||||
break
|
||||
|
||||
@@ -270,7 +263,7 @@ class SelectDiffObjects(bpy.types.Operator):
|
||||
element_file = ifc_file
|
||||
|
||||
try:
|
||||
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
element = element_file.by_id(ifc_id)
|
||||
except:
|
||||
continue
|
||||
global_id = getattr(element, "GlobalId", None)
|
||||
|
||||
@@ -30,18 +30,25 @@ from bpy.props import (
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
from typing import TYPE_CHECKING, Literal, get_args
|
||||
|
||||
|
||||
def update_diff_json_file(self, context):
|
||||
def update_diff_json_file(self: "DiffProperties", context: bpy.types.Context) -> None:
|
||||
DiffData.data["diff_json"] = DiffData.diff_json()
|
||||
|
||||
|
||||
RelationshipType = Literal["type", "property", "container", "aggregate", "classification"]
|
||||
|
||||
|
||||
class Relationships(PropertyGroup):
|
||||
relationship: EnumProperty(
|
||||
name="Relationship",
|
||||
items=[(r, r.capitalize(), r) for r in ["type", "property", "container", "aggregate", "classification"]],
|
||||
items=[(r, r.capitalize(), r) for r in get_args(RelationshipType)],
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
relationship: RelationshipType
|
||||
|
||||
|
||||
class DiffProperties(PropertyGroup):
|
||||
diff_json_file: StringProperty(default="", name="JSON Output", update=update_diff_json_file)
|
||||
@@ -59,3 +66,12 @@ class DiffProperties(PropertyGroup):
|
||||
name="Active File",
|
||||
default="NEW",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
diff_json_file: str
|
||||
old_file: str
|
||||
new_file: str
|
||||
diff_relationships: bpy.types.bpy_prop_collection_idprop[Relationships]
|
||||
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
|
||||
should_load_changed_elements: bool
|
||||
active_file: Literal["NONE", "OLD", "NEW"]
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from bpy.types import Panel
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.module.diff.data import DiffData
|
||||
import bonsai.bim.helper
|
||||
import bonsai.tool as tool
|
||||
@@ -38,9 +37,7 @@ class BIM_PT_diff(Panel):
|
||||
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
|
||||
scene = context.scene
|
||||
props = scene.DiffProperties
|
||||
props = tool.Blender.get_diff_props()
|
||||
|
||||
layout.label(text="IFC Diff Setup:")
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ class DocumentData:
|
||||
|
||||
@classmethod
|
||||
def parent_document(cls):
|
||||
props = bpy.context.scene.BIMDocumentProperties
|
||||
props = tool.Document.get_document_props()
|
||||
if len(props.breadcrumbs):
|
||||
parent = tool.Ifc.get().by_id(int(props.breadcrumbs[-1].name))
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
|
||||
@@ -24,7 +24,6 @@ import ifcopenshell.util.element
|
||||
import bonsai.bim.handler
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.document as core
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
|
||||
|
||||
class LoadProjectDocuments(bpy.types.Operator):
|
||||
@@ -116,7 +115,7 @@ class EditDocument(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMDocumentProperties
|
||||
props = tool.Document.get_document_props()
|
||||
core.edit_document(tool.Ifc, tool.Document, document=tool.Ifc.get().by_id(props.active_document_id))
|
||||
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ from bpy.props import (
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
def update_document_name(self: "Document", context: bpy.types.Context) -> None:
|
||||
@@ -57,6 +58,11 @@ class Document(PropertyGroup):
|
||||
)
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
identification: str
|
||||
is_information: bool
|
||||
ifc_definition_id: int
|
||||
|
||||
|
||||
class BIMDocumentProperties(PropertyGroup):
|
||||
document_attributes: CollectionProperty(name="Document Attributes", type=Attribute)
|
||||
@@ -65,3 +71,11 @@ class BIMDocumentProperties(PropertyGroup):
|
||||
breadcrumbs: CollectionProperty(name="Breadcrumbs", type=StrProperty)
|
||||
active_document_index: IntProperty(name="Active Document Index")
|
||||
is_editing: BoolProperty(name="Is Editing", default=False)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
document_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
active_document_id: int
|
||||
documents: bpy.types.bpy_prop_collection_idprop[Document]
|
||||
breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
active_document_index: int
|
||||
is_editing: bool
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bonsai.tool as tool
|
||||
from bpy.types import Panel, UIList
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.helper import draw_attributes
|
||||
from bonsai.bim.module.document.data import DocumentData, ObjectDocumentData
|
||||
|
||||
@@ -33,13 +33,13 @@ class BIM_PT_documents(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return IfcStore.get_file()
|
||||
return tool.Ifc.get()
|
||||
|
||||
def draw(self, context):
|
||||
if not DocumentData.is_loaded:
|
||||
DocumentData.load()
|
||||
|
||||
self.props = context.scene.BIMDocumentProperties
|
||||
self.props = tool.Document.get_document_props()
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="{} Documents Found".format(DocumentData.data["total_information"]), icon="FILE")
|
||||
@@ -90,20 +90,22 @@ class BIM_PT_object_documents(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
if not (obj := context.active_object):
|
||||
return False
|
||||
if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id):
|
||||
if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)):
|
||||
return False
|
||||
return bool(context.active_object.BIMObjectProperties.ifc_definition_id)
|
||||
if not tool.Ifc.get_object_by_identifier(ifc_id):
|
||||
return False
|
||||
return True
|
||||
|
||||
def draw(self, context):
|
||||
if not ObjectDocumentData.is_loaded:
|
||||
ObjectDocumentData.load()
|
||||
|
||||
obj = context.active_object
|
||||
self.oprops = obj.BIMObjectProperties
|
||||
self.props = context.scene.BIMDocumentProperties
|
||||
self.file = IfcStore.get_file()
|
||||
self.oprops = tool.Blender.get_object_bim_props(obj)
|
||||
self.props = tool.Document.get_document_props()
|
||||
self.file = tool.Ifc.get()
|
||||
|
||||
self.draw_add_ui()
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import bpy
|
||||
import math
|
||||
@@ -37,7 +38,7 @@ class Annotator:
|
||||
if related_element is None:
|
||||
location, _, _, _ = Annotator.get_placeholder_coords(bpy.context)
|
||||
else:
|
||||
obj.data.BIMAssignedProductProperties.related_element = related_element
|
||||
curve.BIMAssignedProductProperties.related_element = related_element
|
||||
location = related_element.location
|
||||
obj.location = location
|
||||
obj.hide_render = True
|
||||
@@ -47,9 +48,10 @@ class Annotator:
|
||||
tool.Blender.get_data_dir_path(Path("fonts") / "OpenGost Type B TT.ttf").__str__()
|
||||
)
|
||||
font.name = "OpenGost Type B TT"
|
||||
obj.data.font = font
|
||||
obj.data.BIMTextProperties.font_size = "2.5"
|
||||
collection = bpy.context.scene.camera.BIMObjectProperties.collection
|
||||
curve.font = font
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
props.font_size = "2.5"
|
||||
collection = tool.Blender.get_object_bim_props(bpy.context.scene.camera).collection
|
||||
collection.objects.link(obj)
|
||||
Annotator.resize_text(obj)
|
||||
return obj
|
||||
@@ -66,7 +68,8 @@ class Annotator:
|
||||
return
|
||||
# This is a magic number for OpenGost
|
||||
font_size = 1.6 / 1000
|
||||
font_size *= float(text_obj.data.BIMTextProperties.font_size)
|
||||
props = tool.Drawing.get_text_props(text_obj)
|
||||
font_size *= float(props.font_size)
|
||||
|
||||
font_size /= tool.Drawing.get_scale_ratio(tool.Drawing.get_diagram_scale(camera)["Scale"])
|
||||
|
||||
@@ -123,12 +126,14 @@ class Annotator:
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def get_annotation_obj(drawing: ifcopenshell.entity_instance, object_type: str, data_type: str) -> bpy.types.Object:
|
||||
def get_annotation_obj(
|
||||
drawing: ifcopenshell.entity_instance, object_type: str, data_type: tool.Drawing.ANNOTATION_DATA_TYPE
|
||||
) -> bpy.types.Object:
|
||||
camera = tool.Ifc.get_object(drawing)
|
||||
co1, _, _, _ = Annotator.get_placeholder_coords(camera)
|
||||
matrix_world = tool.Drawing.get_camera_matrix(camera)
|
||||
matrix_world.translation = co1
|
||||
collection = camera.BIMObjectProperties.collection
|
||||
collection = tool.Blender.get_object_bim_props(camera).collection
|
||||
|
||||
if object_type == "TEXT":
|
||||
obj = bpy.data.objects.new(object_type, None)
|
||||
@@ -145,11 +150,17 @@ class Annotator:
|
||||
collection.objects.link(obj)
|
||||
return obj
|
||||
|
||||
if object_type != "ANGLE":
|
||||
for obj in collection.objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element and element.ObjectType == object_type and obj.type == object_type.upper():
|
||||
return obj
|
||||
# TODO: remove as outdated?
|
||||
# Is reusing the same objects preventing the creation of new annotations.
|
||||
# if object_type != "ANGLE":
|
||||
# for obj in collection.objects:
|
||||
# element = tool.Ifc.get_entity(obj)
|
||||
# if (
|
||||
# element
|
||||
# and ifcopenshell.util.element.get_predefined_type(element) == object_type
|
||||
# and obj.type == data_type.upper()
|
||||
# ):
|
||||
# return obj
|
||||
|
||||
if data_type == "mesh":
|
||||
data = bpy.data.meshes.new(object_type)
|
||||
|
||||
@@ -88,7 +88,8 @@ class SheetsData:
|
||||
project = tool.Ifc.get().by_type("IfcProject")[0]
|
||||
titleblocks_dir = ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "TitleblocksDir")
|
||||
if not titleblocks_dir:
|
||||
titleblocks_dir = bpy.context.scene.DocProperties.titleblocks_dir
|
||||
props = tool.Drawing.get_document_props()
|
||||
titleblocks_dir = props.titleblocks_dir
|
||||
titleblocks_dir = tool.Ifc.resolve_uri(titleblocks_dir)
|
||||
if os.path.exists(titleblocks_dir):
|
||||
files.extend([str(f.stem) for f in Path(titleblocks_dir).glob("*.svg")])
|
||||
@@ -120,23 +121,25 @@ class DrawingsData:
|
||||
|
||||
@classmethod
|
||||
def location_hint(cls):
|
||||
if bpy.context.scene.DocProperties.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]:
|
||||
props = tool.Drawing.get_document_props()
|
||||
if props.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
|
||||
elif bpy.context.scene.DocProperties.target_view in ["MODEL_VIEW"]:
|
||||
elif props.target_view in ["MODEL_VIEW"]:
|
||||
return [(h.upper(), h, "") for h in ["Orthographic", "Perspective"]]
|
||||
return [(h.upper(), h, "") for h in ["North", "South", "East", "West"]]
|
||||
|
||||
@classmethod
|
||||
def active_drawing_pset_data(cls):
|
||||
ifc_file = tool.Ifc.get()
|
||||
drawing_id = bpy.context.scene.DocProperties.active_drawing_id
|
||||
props = tool.Drawing.get_document_props()
|
||||
drawing_id = props.active_drawing_id
|
||||
if drawing_id == 0:
|
||||
return {}
|
||||
drawing = ifc_file.by_id(bpy.context.scene.DocProperties.active_drawing_id)
|
||||
drawing = ifc_file.by_id(drawing_id)
|
||||
return ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing")
|
||||
|
||||
|
||||
@@ -233,7 +236,8 @@ class DecoratorData:
|
||||
# stores 1 type of data per object
|
||||
data = {}
|
||||
cut_cache = {}
|
||||
layerset_cache = {}
|
||||
slice_cache = {}
|
||||
fill_cache = {}
|
||||
|
||||
@classmethod
|
||||
def get_batting_thickness(cls, obj):
|
||||
@@ -303,7 +307,7 @@ class DecoratorData:
|
||||
if not element or not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]):
|
||||
return None
|
||||
|
||||
props = obj.BIMTextProperties
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
# getting font size
|
||||
pset_data = ifcopenshell.util.element.get_pset(element, "EPset_Annotation") or {}
|
||||
# use `regular` as default
|
||||
@@ -361,7 +365,11 @@ class DecoratorData:
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
supported_object_types = ("DIMENSION", "DIAMETER", "SECTION_LEVEL", "PLAN_LEVEL", "RADIUS")
|
||||
if not element or not element.is_a("IfcAnnotation") or element.ObjectType not in supported_object_types:
|
||||
if (
|
||||
not element
|
||||
or not element.is_a("IfcAnnotation")
|
||||
or ifcopenshell.util.element.get_predefined_type(element) not in supported_object_types
|
||||
):
|
||||
return None
|
||||
|
||||
dimension_style = "arrow"
|
||||
@@ -379,6 +387,8 @@ class DecoratorData:
|
||||
suppress_zero_inches = pset_data.get("SuppressZeroInches", False)
|
||||
text_prefix = pset_data.get("TextPrefix", None) or ""
|
||||
text_suffix = pset_data.get("TextSuffix", None) or ""
|
||||
custom_unit_list = pset_data.get("CustomUnit", None) or ""
|
||||
custom_unit = custom_unit_list[0] if custom_unit_list else ""
|
||||
|
||||
dimension_data = {
|
||||
"dimension_style": dimension_style,
|
||||
@@ -387,6 +397,7 @@ class DecoratorData:
|
||||
"text_prefix": text_prefix,
|
||||
"text_suffix": text_suffix,
|
||||
"fill_bg": fill_bg,
|
||||
"custom_unit": custom_unit,
|
||||
}
|
||||
cls.data[obj.name] = dimension_data
|
||||
return dimension_data
|
||||
@@ -399,7 +410,7 @@ class AnnotationData:
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.props = bpy.context.scene.BIMAnnotationProperties
|
||||
cls.props = tool.Drawing.get_annotation_props()
|
||||
cls.data["relating_type_id"] = cls.relating_type_id()
|
||||
cls.data["relating_types"] = cls.relating_types()
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import gpu
|
||||
import bpy
|
||||
import blf
|
||||
@@ -29,7 +28,7 @@ import ifcopenshell.util.unit
|
||||
import bonsai.tool as tool
|
||||
import bonsai.bim.module.drawing.helper as helper
|
||||
from pathlib import Path
|
||||
from math import pi, sin, cos, tan, acos, atan, degrees, radians, ceil
|
||||
from math import pi, sin, cos, acos, atan, degrees, radians
|
||||
from bpy.types import SpaceView3D
|
||||
from mathutils import Vector, Matrix
|
||||
from bpy_extras.view3d_utils import location_3d_to_region_2d
|
||||
@@ -319,6 +318,8 @@ class BaseDecorator:
|
||||
color: tuple[float, float, float, float],
|
||||
indices: Optional[list[tuple[int, int]]] = None,
|
||||
) -> None:
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.base_shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
@@ -423,7 +424,8 @@ class BaseDecorator:
|
||||
# font_size = 16 <-- this is a good default
|
||||
# TODO: need to synchronize it better with svg
|
||||
|
||||
magic_font_scale = bpy.context.scene.DocProperties.magic_font_scale
|
||||
props = tool.Drawing.get_document_props()
|
||||
magic_font_scale = props.magic_font_scale
|
||||
font_size_px = int(magic_font_scale * mm_to_px) * font_size_mm / 2.5
|
||||
pos = pos - line_no * font_size_px * rotation_matrix[1]
|
||||
|
||||
@@ -494,14 +496,20 @@ class BaseDecorator:
|
||||
self.draw_label(context, text=text, line_no=line_number_start, multiline=True, **draw_label_kwargs)
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def format_value(self, context, value):
|
||||
def format_value(self, context, value, custom_unit=None):
|
||||
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
|
||||
precision = drawing_pset_data.get("MetricPrecision", None)
|
||||
if not precision:
|
||||
precision = drawing_pset_data.get("ImperialPrecision", None)
|
||||
|
||||
decimal_places = drawing_pset_data.get("DecimalPlaces", None)
|
||||
return format_distance(value, precision=precision, decimal_places=decimal_places)
|
||||
return format_distance(
|
||||
value,
|
||||
precision=precision,
|
||||
decimal_places=decimal_places,
|
||||
suppress_zero_inches=True,
|
||||
custom_unit=custom_unit,
|
||||
)
|
||||
|
||||
def draw_asterisk(self, context: bpy.types.Context, pos: Vector, rotation: float = 0.0, scale: float = 1.0) -> None:
|
||||
"""`pos` is a world space position\n
|
||||
@@ -588,8 +596,9 @@ class BaseDecorator:
|
||||
region3d = context.region_data
|
||||
text_dir = self.get_annotation_direction(context, obj)
|
||||
|
||||
pos = location_3d_to_region_2d(region, region3d, text_world_position)
|
||||
props = obj.BIMTextProperties
|
||||
if not (pos := location_3d_to_region_2d(region, region3d, text_world_position)):
|
||||
return
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
text_data = DecoratorData.get_ifc_text_data(obj)
|
||||
if props.is_editing:
|
||||
text_data = text_data | props.get_text_edited_data()
|
||||
@@ -721,6 +730,8 @@ class DimensionDecorator(BaseDecorator):
|
||||
v1 = Vector(vertices[i1])
|
||||
p0 = location_3d_to_region_2d(region, region3d, v0)
|
||||
p1 = location_3d_to_region_2d(region, region3d, v1)
|
||||
if not p0 or not p1:
|
||||
return
|
||||
text_dir = p1 - p0
|
||||
if text_dir.length < 1:
|
||||
continue
|
||||
@@ -736,7 +747,7 @@ class DimensionDecorator(BaseDecorator):
|
||||
|
||||
if not show_description_only:
|
||||
length = (v1 - v0).length
|
||||
text = self.format_value(context, length)
|
||||
text = self.format_value(context, length, custom_unit=dimension_data["custom_unit"])
|
||||
if isinstance(self, DiameterDecorator):
|
||||
text = "D" + text
|
||||
text = text_prefix + text + text_suffix
|
||||
@@ -871,6 +882,8 @@ class AngleDecorator(BaseDecorator):
|
||||
|
||||
# calculate angle position
|
||||
p0, p1, p2 = [location_3d_to_region_2d(region, region3d, p) for p in vertices[i0 : i1 + 2]]
|
||||
if not p0 or not p1 or not p2:
|
||||
continue
|
||||
edge0 = p0 - p1
|
||||
edge1 = p2 - p1
|
||||
radius = min(edge0.length_squared, edge1.length_squared) ** 0.5
|
||||
@@ -935,6 +948,8 @@ class RadiusDecorator(BaseDecorator):
|
||||
spline_points = self.get_spline_points(obj)
|
||||
|
||||
p0, p1 = [location_3d_to_region_2d(region, region3d, p) for p in self.get_spline_points(obj)]
|
||||
if not p0 or not p1:
|
||||
return
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
description = element.Description
|
||||
dimension_data = DecoratorData.get_dimension_data(obj)
|
||||
@@ -945,7 +960,7 @@ class RadiusDecorator(BaseDecorator):
|
||||
|
||||
def get_text():
|
||||
length = (spline_points[-1] - spline_points[-2]).length
|
||||
return "R" + self.format_value(context, length)
|
||||
return "R" + self.format_value(context, length, custom_unit=dimension_data["custom_unit"])
|
||||
|
||||
self.draw_dimension_text(
|
||||
context, get_text, description, dimension_data, pos=pos, text_dir=Vector((1, 0)), box_alignment="center"
|
||||
@@ -967,7 +982,8 @@ class FallDecorator(BaseDecorator):
|
||||
region = context.region
|
||||
region3d = context.region_data
|
||||
dir = Vector((1, 0))
|
||||
pos = location_3d_to_region_2d(region, region3d, self.get_spline_end(obj))
|
||||
if not (pos := location_3d_to_region_2d(region, region3d, self.get_spline_end(obj))):
|
||||
return
|
||||
|
||||
spline = obj.data.splines[0]
|
||||
spline_points = spline.bezier_points if spline.bezier_points else spline.points
|
||||
@@ -976,6 +992,7 @@ class FallDecorator(BaseDecorator):
|
||||
# same function as in svgwriter.py
|
||||
def get_label_text():
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
B, A = [v.co.xyz for v in spline_points[:2]]
|
||||
rise = abs(A.z - B.z)
|
||||
O = A.copy()
|
||||
@@ -988,13 +1005,14 @@ class FallDecorator(BaseDecorator):
|
||||
angle = 90
|
||||
|
||||
# ues SLOPE_ANGLE as default
|
||||
if element.ObjectType in ("FALL", "SLOPE_ANGLE"):
|
||||
object_type = ifcopenshell.util.element.get_predefined_type(element)
|
||||
if object_type in ("FALL", "SLOPE_ANGLE"):
|
||||
return f"{angle}°"
|
||||
elif element.ObjectType == "SLOPE_FRACTION":
|
||||
elif object_type == "SLOPE_FRACTION":
|
||||
if angle == 90:
|
||||
return "-"
|
||||
return f"{self.format_value(context, rise)} / {self.format_value(context, run)}"
|
||||
elif element.ObjectType == "SLOPE_PERCENT":
|
||||
elif object_type == "SLOPE_PERCENT":
|
||||
if angle == 90:
|
||||
return "-"
|
||||
return f"{round(angle_tg * 100)} %"
|
||||
@@ -1156,7 +1174,6 @@ class PlanLevelDecorator(BaseDecorator):
|
||||
self.draw_labels(context, obj, self.get_splines(obj))
|
||||
|
||||
def draw_labels(self, context, obj, splines):
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
region = context.region
|
||||
region3d = context.region_data
|
||||
|
||||
@@ -1166,6 +1183,8 @@ class PlanLevelDecorator(BaseDecorator):
|
||||
|
||||
for verts in splines:
|
||||
p0, p1 = [location_3d_to_region_2d(region, region3d, v) for v in verts[:2]]
|
||||
if not p0 or not p1:
|
||||
continue
|
||||
text_dir = p1 - p0
|
||||
if text_dir.length < 1:
|
||||
continue
|
||||
@@ -1432,8 +1451,10 @@ class GridDecorator(BaseDecorator):
|
||||
p1 = location_3d_to_region_2d(region, region3d, v1)
|
||||
dir = Vector((1, 0))
|
||||
text = obj.name.split("/")[1].split(".")[0]
|
||||
self.draw_label(context, text, p0, dir, vcenter=True, gap=0)
|
||||
self.draw_label(context, text, p1, dir, vcenter=True, gap=0)
|
||||
if p0:
|
||||
self.draw_label(context, text, p0, dir, vcenter=True, gap=0)
|
||||
if p1:
|
||||
self.draw_label(context, text, p1, dir, vcenter=True, gap=0)
|
||||
|
||||
|
||||
class ElevationDecorator(BaseDecorator):
|
||||
@@ -1586,6 +1607,9 @@ class CutDecorator:
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
DecoratorData.cut_cache.clear()
|
||||
DecoratorData.slice_cache.clear()
|
||||
DecoratorData.fill_cache.clear()
|
||||
if cls.installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
@@ -1600,38 +1624,50 @@ class CutDecorator:
|
||||
cls.installed = None
|
||||
|
||||
def __call__(self, context):
|
||||
if not context.scene.camera:
|
||||
return
|
||||
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
selected_elements_color = self.addon_prefs.decorator_color_selected
|
||||
self.fallback_colour = (0.3, 0.3, 0.3, 1)
|
||||
|
||||
all_vertices = []
|
||||
all_edges = []
|
||||
selected_vertices = []
|
||||
selected_edges = []
|
||||
layer_vertices = []
|
||||
layer_edges = []
|
||||
fills = {}
|
||||
all_vertex_i_offset = 0
|
||||
selected_vertex_i_offset = 0
|
||||
layer_vertex_i_offset = 0
|
||||
|
||||
for obj in [o for o in bpy.context.visible_objects if o.type == "MESH"]:
|
||||
verts, edges = self.decorate(context, obj)
|
||||
if not verts:
|
||||
if not (element := tool.Ifc.get_entity(obj)):
|
||||
continue
|
||||
self.decorate(context, obj, element)
|
||||
|
||||
obj_layer_verts, obj_layer_edges = self.slice_layersets(context, obj, verts, edges)
|
||||
if obj_layer_verts:
|
||||
layer_vertices.extend(obj_layer_verts)
|
||||
layer_edges.extend([[vi + layer_vertex_i_offset for vi in e] for e in obj_layer_edges])
|
||||
layer_vertex_i_offset += len(obj_layer_verts)
|
||||
verts, edges = DecoratorData.cut_cache[element.id()]
|
||||
if verts:
|
||||
if obj.select_get():
|
||||
selected_vertices.extend(verts)
|
||||
selected_edges.extend([[vi + selected_vertex_i_offset for vi in e] for e in edges])
|
||||
selected_vertex_i_offset += len(verts)
|
||||
else:
|
||||
all_vertices.extend(verts)
|
||||
all_edges.extend([[vi + all_vertex_i_offset for vi in e] for e in edges])
|
||||
all_vertex_i_offset += len(verts)
|
||||
|
||||
if obj.select_get():
|
||||
selected_vertices.extend(verts)
|
||||
selected_edges.extend([[vi + selected_vertex_i_offset for vi in e] for e in edges])
|
||||
selected_vertex_i_offset += len(verts)
|
||||
else:
|
||||
all_vertices.extend(verts)
|
||||
all_edges.extend([[vi + all_vertex_i_offset for vi in e] for e in edges])
|
||||
all_vertex_i_offset += len(verts)
|
||||
verts, edges = DecoratorData.slice_cache.get(element.id(), (None, None))
|
||||
if verts:
|
||||
if obj.select_get():
|
||||
selected_vertices.extend(verts)
|
||||
selected_edges.extend([[vi + selected_vertex_i_offset for vi in e] for e in edges])
|
||||
selected_vertex_i_offset += len(verts)
|
||||
else:
|
||||
all_vertices.extend(verts)
|
||||
all_edges.extend([[vi + all_vertex_i_offset for vi in e] for e in edges])
|
||||
all_vertex_i_offset += len(verts)
|
||||
|
||||
for colour, element_fills in DecoratorData.fill_cache[element.id()].items():
|
||||
fills.setdefault(colour, []).append(element_fills)
|
||||
|
||||
gpu.state.point_size_set(1)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
@@ -1649,9 +1685,10 @@ class CutDecorator:
|
||||
|
||||
black = (0, 0, 0, 1)
|
||||
|
||||
if layer_vertices:
|
||||
self.draw_batch("LINES", layer_vertices, black, layer_edges)
|
||||
self.draw_batch("POINTS", layer_vertices, black)
|
||||
for colour, element_fills in fills.items():
|
||||
for verts_tris in element_fills:
|
||||
for verts, tris in verts_tris:
|
||||
self.draw_batch("TRIS", verts, colour, tris)
|
||||
|
||||
gpu.state.point_size_set(2)
|
||||
self.line_shader.uniform_float("lineWidth", 3.0)
|
||||
@@ -1664,315 +1701,183 @@ class CutDecorator:
|
||||
self.draw_batch("POINTS", selected_vertices, selected_elements_color)
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def decorate(self, context, obj):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return None, None
|
||||
def decorate(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||
has_cut_cache = element.id() in DecoratorData.cut_cache
|
||||
has_fill_cache = element.id() in DecoratorData.fill_cache
|
||||
|
||||
# Currently selected objects shall not be cached as they may be being moved / edited.
|
||||
# If the camera is selected, we also disable the cache as the user may be moving the camera.
|
||||
if obj.select_get() or context.scene.camera.select_get():
|
||||
verts, edges = None, None
|
||||
else:
|
||||
verts, edges = DecoratorData.cut_cache.get(element.id(), (None, None))
|
||||
# Currently selected objects must be recalculated as they may be being moved / edited.
|
||||
# If the camera is selected, we also recalculate as the user may be moving the camera.
|
||||
|
||||
if verts is False:
|
||||
return None, None
|
||||
elif verts:
|
||||
return verts, edges
|
||||
if not has_cut_cache or obj.select_get() or context.scene.camera.select_get():
|
||||
self.recalculate_cut(context, obj, element)
|
||||
if not has_fill_cache or obj.select_get() or context.scene.camera.select_get():
|
||||
self.recalculate_fill(context, obj, element)
|
||||
|
||||
def recalculate_cut(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
|
||||
DecoratorData.cut_cache[element.id()] = (False, False)
|
||||
return None, None
|
||||
|
||||
if verts is None:
|
||||
else:
|
||||
verts, edges = tool.Drawing.bisect_mesh(obj, context.scene.camera)
|
||||
DecoratorData.cut_cache[element.id()] = (verts, edges)
|
||||
return verts, edges
|
||||
|
||||
def slice_layersets(self, context, obj, cut_verts, cut_edges):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
# Currently selected objects shall not be cached as they may be being moved / edited.
|
||||
# If the camera is selected, we also disable the cache as the user may be moving the camera.
|
||||
if obj.select_get() or context.scene.camera.select_get():
|
||||
verts, edges = None, None
|
||||
else:
|
||||
verts, edges = DecoratorData.layerset_cache.get(element.id(), (None, None))
|
||||
|
||||
if verts is False:
|
||||
return None, None
|
||||
elif verts is not None:
|
||||
return verts, edges
|
||||
def recalculate_fill(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||
element_id = element.id()
|
||||
|
||||
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
|
||||
DecoratorData.layerset_cache[element.id()] = (False, False)
|
||||
return None, None
|
||||
DecoratorData.fill_cache[element_id] = {}
|
||||
return
|
||||
|
||||
if tool.Model.get_usage_type(element) != "LAYER2":
|
||||
DecoratorData.layerset_cache[element.id()] = (False, False)
|
||||
return None, None
|
||||
DecoratorData.fill_cache[element_id] = {}
|
||||
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
layers = self.get_layer_data(element)
|
||||
mesh = obj.data
|
||||
bm_original = bmesh.new()
|
||||
bm_original.from_mesh(mesh)
|
||||
|
||||
if not layers:
|
||||
DecoratorData.layerset_cache[element.id()] = (False, False)
|
||||
return None, None
|
||||
|
||||
minx = min([co[0] for co in obj.bound_box])
|
||||
maxx = max([co[0] for co in obj.bound_box])
|
||||
min_edge = [Vector((minx, layers["offset"])), Vector((maxx, layers["offset"]))]
|
||||
max_edge = [
|
||||
Vector((minx, layers["offset"] + layers["thickness"])),
|
||||
Vector((maxx, layers["offset"] + layers["thickness"])),
|
||||
]
|
||||
centerline = [
|
||||
Vector((minx, layers["offset"] + layers["thickness"] / 2)),
|
||||
Vector((maxx, layers["offset"] + layers["thickness"] / 2)),
|
||||
]
|
||||
connections = self.get_connections(element, obj, centerline, min_edge, max_edge)
|
||||
|
||||
start_point = None
|
||||
end_point = None
|
||||
if connections["ATSTART"]:
|
||||
boundary_edge = min_edge if connections["ATSTART"]["angle"] > 0 else max_edge
|
||||
rel_dir = connections["ATSTART"]["centerline"][1] - connections["ATSTART"]["centerline"][0]
|
||||
offset, _ = tool.Cad.intersect_edges(boundary_edge, [centerline[0], centerline[0] + rel_dir])
|
||||
offset_x = (offset - centerline[0]).length + abs(offset.x)
|
||||
intersect, _ = tool.Cad.intersect_edges(boundary_edge, connections["ATSTART"]["centerline"])
|
||||
if tool.Cad.is_point_on_edge(intersect, boundary_edge):
|
||||
centerline[0] = centerline[0] + Vector((offset_x, 0))
|
||||
start_point = centerline[0] + rel_dir
|
||||
if connections["ATEND"]:
|
||||
boundary_edge = min_edge if connections["ATEND"]["angle"] > 0 else max_edge
|
||||
rel_dir = connections["ATEND"]["centerline"][1] - connections["ATEND"]["centerline"][0]
|
||||
offset, _ = tool.Cad.intersect_edges(boundary_edge, [centerline[1], centerline[1] + rel_dir])
|
||||
|
||||
offset_x = (offset - centerline[1]).length + abs((maxx - abs(offset.x)))
|
||||
intersect, _ = tool.Cad.intersect_edges(boundary_edge, connections["ATEND"]["centerline"])
|
||||
if tool.Cad.is_point_on_edge(intersect, boundary_edge):
|
||||
centerline[1] = centerline[1] - Vector((offset_x, 0))
|
||||
end_point = centerline[1] + rel_dir
|
||||
|
||||
min_segments = self.get_segments(connections["MINPATH"], centerline, start_point, end_point)
|
||||
max_segments = self.get_segments(connections["MAXPATH"], centerline, start_point, end_point)
|
||||
|
||||
final_segments = []
|
||||
for segment in min_segments:
|
||||
segment_line = shapely.LineString([co for co in segment])
|
||||
for distance in layers["min_layers"]:
|
||||
distance *= -1
|
||||
layer_line = shapely.offset_curve(segment_line, distance, join_style=shapely.BufferJoinStyle.mitre)
|
||||
final_segments.append(list(layer_line.coords))
|
||||
for segment in max_segments:
|
||||
segment_line = shapely.LineString([co for co in segment])
|
||||
for distance in layers["max_layers"]:
|
||||
layer_line = shapely.offset_curve(segment_line, distance, join_style=shapely.BufferJoinStyle.mitre)
|
||||
final_segments.append(list(layer_line.coords))
|
||||
|
||||
# Extrude and bisect layers
|
||||
bm = bmesh.new()
|
||||
verts = []
|
||||
edges = []
|
||||
offset = 0
|
||||
for segment in final_segments:
|
||||
if not segment:
|
||||
# Why does this occur?
|
||||
continue
|
||||
if isinstance(segment[0], tuple):
|
||||
segment = [Vector(co) for co in segment]
|
||||
verts.extend([bm.verts.new(co.to_3d()) for co in segment])
|
||||
[bm.edges.new((verts[i + offset], verts[i + 1 + offset])) for i in range(0, len(segment) - 1)]
|
||||
offset += len(segment)
|
||||
|
||||
extrusion_dir = (0, 0, 3)
|
||||
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=bm.edges[:])
|
||||
bmesh.ops.translate(
|
||||
bm, vec=extrusion_dir, verts=[v for v in extruded_geom["geom"] if isinstance(v, bmesh.types.BMVert)]
|
||||
)
|
||||
|
||||
verts, edges = self.bisect_mesh(obj, bm, context.scene.camera)
|
||||
layer_linestrings = [shapely.LineString((verts[e[0]], verts[e[1]])) for e in edges]
|
||||
|
||||
clipped_linestrings = []
|
||||
|
||||
polygons = shapely.polygonize([shapely.LineString((cut_verts[e[0]], cut_verts[e[1]])) for e in cut_edges])
|
||||
for polygon in polygons.geoms:
|
||||
clipped_linestrings.extend([polygon.intersection(ls) for ls in layer_linestrings])
|
||||
|
||||
verts = []
|
||||
edges = []
|
||||
offset = 0
|
||||
for linestring in clipped_linestrings:
|
||||
try:
|
||||
linestring = list(linestring.coords)
|
||||
except:
|
||||
print("Failed ... ", linestring)
|
||||
continue
|
||||
verts.extend(linestring)
|
||||
edges.extend([(i + offset, i + 1 + offset) for i in range(0, len(linestring) - 1)])
|
||||
offset += len(linestring)
|
||||
|
||||
DecoratorData.layerset_cache[element.id()] = (verts, edges)
|
||||
return verts, edges
|
||||
|
||||
def bisect_mesh(self, obj, bm, camera):
|
||||
camera_matrix = obj.matrix_world.inverted() @ camera.matrix_world
|
||||
# Slice our mesh into a 2D drawing cut (2D is always easier)
|
||||
camera_matrix = obj.matrix_world.inverted() @ context.scene.camera.matrix_world
|
||||
global_offset = context.scene.camera.matrix_world.col[2].xyz * -context.scene.camera.data.clip_start
|
||||
plane_co = camera_matrix.translation
|
||||
plane_no = camera_matrix.col[2].xyz
|
||||
geom = bm_original.verts[:] + bm_original.edges[:] + bm_original.faces[:]
|
||||
bmesh.ops.bisect_plane(
|
||||
bm_original,
|
||||
geom=geom,
|
||||
dist=0.0001,
|
||||
plane_co=plane_co,
|
||||
plane_no=plane_no,
|
||||
clear_outer=True,
|
||||
clear_inner=True,
|
||||
)
|
||||
bmesh.ops.remove_doubles(bm_original, verts=bm_original.verts, dist=0.000001)
|
||||
bmesh.ops.triangle_fill(bm_original, use_dissolve=True, edges=bm_original.edges)
|
||||
|
||||
global_offset = camera.matrix_world.col[2].xyz * -camera.data.clip_start
|
||||
if not (material := ifcopenshell.util.element.get_material(element)):
|
||||
verts, tris = self.get_bmesh_tris(obj, bm_original, context.scene.camera)
|
||||
DecoratorData.fill_cache[element_id].setdefault(self.fallback_colour, []).append((verts, tris))
|
||||
return
|
||||
|
||||
# Run the bisect operation
|
||||
geom = bm.verts[:] + bm.edges[:] + bm.faces[:]
|
||||
results = bmesh.ops.bisect_plane(bm, geom=geom, dist=0.0001, plane_co=plane_co, plane_no=plane_no)
|
||||
if material.is_a() not in ("IfcMaterialLayerSet", "IfcMaterialLayerSetUsage"):
|
||||
# Constituents, lists, and item styles not supported yet
|
||||
material = ifcopenshell.util.element.get_materials(element)[0]
|
||||
verts, tris = self.get_bmesh_tris(obj, bm_original, context.scene.camera)
|
||||
colour = self.get_material_colour(material)
|
||||
DecoratorData.fill_cache[element_id].setdefault(colour, []).append((verts, tris))
|
||||
return
|
||||
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
|
||||
if material.is_a("IfcMaterialLayerSetUsage"):
|
||||
usage = material
|
||||
layer_set = material.ForLayerSet
|
||||
offset = usage.OffsetFromReferenceLine * self.unit_scale
|
||||
sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1
|
||||
elif material.is_a("IfcMaterialLayerSet"):
|
||||
usage = None
|
||||
layer_set = material
|
||||
offset = 0
|
||||
sense_factor = 1
|
||||
|
||||
if len(layer_set.MaterialLayers) == 1:
|
||||
material = layer_set.MaterialLayers[0].Material
|
||||
verts, tris = self.get_bmesh_tris(obj, bm_original, context.scene.camera)
|
||||
colour = self.get_material_colour(material)
|
||||
DecoratorData.fill_cache[element_id].setdefault(colour, []).append((verts, tris))
|
||||
return
|
||||
|
||||
bm = bm_original.copy()
|
||||
prev_co = None
|
||||
if not usage:
|
||||
sense_factor = 1 # Assume the extrusion vector points in the direction sense
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
elif usage.LayerSetDirection == "AXIS2":
|
||||
co = Vector((0.0, offset, 0.0))
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
no = no.cross(Vector([1.0, 0.0, 0.0]))
|
||||
elif usage.LayerSetDirection == "AXIS3":
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
no = Vector([0.0, 0.0, 1.0])
|
||||
elif usage.LayerSetDirection == "AXIS1":
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
no = Vector([1.0, 0.0, 0.0])
|
||||
no *= sense_factor
|
||||
last_i = len(layer_set.MaterialLayers) - 1
|
||||
|
||||
vert_map = {}
|
||||
verts = []
|
||||
edges = []
|
||||
j = 0
|
||||
|
||||
for i, layer in enumerate(layer_set.MaterialLayers):
|
||||
prev_co = co.copy()
|
||||
co += no * layer.LayerThickness * self.unit_scale
|
||||
if i != last_i:
|
||||
bisect = bmesh.ops.bisect_plane(
|
||||
bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=no
|
||||
)
|
||||
for geom in bisect["geom_cut"]:
|
||||
if isinstance(geom, bmesh.types.BMVert):
|
||||
verts.append(tuple((obj.matrix_world @ geom.co) + global_offset))
|
||||
vert_map[geom.index] = j
|
||||
j += 1
|
||||
else:
|
||||
# It seems as though edges always appear after verts
|
||||
edges.append([vert_map[v.index] for v in geom.verts])
|
||||
|
||||
colour = self.get_material_colour(layer.Material)
|
||||
bm_fill = bm_original.copy()
|
||||
if i != last_i:
|
||||
geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:]
|
||||
bmesh.ops.bisect_plane(bm_fill, geom=geom, dist=0.0001, plane_co=co, plane_no=no, clear_outer=True)
|
||||
if i != 0:
|
||||
geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:]
|
||||
bmesh.ops.bisect_plane(bm_fill, geom=geom, dist=0.0001, plane_co=prev_co, plane_no=no, clear_inner=True)
|
||||
|
||||
DecoratorData.fill_cache[element_id].setdefault(colour, []).append(
|
||||
self.get_bmesh_tris(obj, bm_fill, context.scene.camera)
|
||||
)
|
||||
|
||||
DecoratorData.slice_cache[element.id()] = (verts, edges)
|
||||
bm_original.free()
|
||||
|
||||
def get_material_colour(self, material: ifcopenshell.entity_instance) -> tuple[float, float, float, float]:
|
||||
body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
||||
style = ifcopenshell.util.representation.get_material_style(material, body)
|
||||
if not style:
|
||||
return self.fallback_colour
|
||||
if not (styles := [s for s in style.Styles if s.is_a("IfcSurfaceStyleShading")]):
|
||||
return self.fallback_colour
|
||||
colour = styles[0].SurfaceColour
|
||||
return (colour.Red, colour.Green, colour.Blue, 1)
|
||||
|
||||
def get_bmesh_tris(self, obj, bm, camera):
|
||||
global_offset = camera.matrix_world.col[2].xyz * -camera.data.clip_start
|
||||
triangulate = bmesh.ops.triangulate(bm, faces=bm.faces)
|
||||
vert_map = {}
|
||||
verts = []
|
||||
tris = []
|
||||
i = 0
|
||||
for geom in results["geom_cut"]:
|
||||
if isinstance(geom, bmesh.types.BMVert):
|
||||
verts.append(tuple((obj.matrix_world @ geom.co) + global_offset))
|
||||
vert_map[geom.index] = i
|
||||
i += 1
|
||||
else:
|
||||
# It seems as though edges always appear after verts
|
||||
edges.append([vert_map[v.index] for v in geom.verts])
|
||||
|
||||
bm.free()
|
||||
|
||||
return verts, edges
|
||||
|
||||
def get_layer_data(self, element):
|
||||
usage = ifcopenshell.util.element.get_material(element)
|
||||
offset = usage.OffsetFromReferenceLine * self.unit_scale
|
||||
layer_set = usage.ForLayerSet
|
||||
|
||||
if len(layer_set.MaterialLayers) == 1:
|
||||
return # No use slicing if there's only one layer
|
||||
|
||||
total_thickness = layer_set.TotalThickness
|
||||
half_thickness = total_thickness / 2
|
||||
min_layers = []
|
||||
max_layers = []
|
||||
current_thickness = 0
|
||||
for layer in layer_set.MaterialLayers:
|
||||
current_thickness += layer.LayerThickness
|
||||
if current_thickness / total_thickness < 0.5:
|
||||
min_layers.append((half_thickness - current_thickness) * self.unit_scale)
|
||||
else:
|
||||
max_layers.append((current_thickness - half_thickness) * self.unit_scale)
|
||||
min_layers.reverse()
|
||||
max_layers.pop()
|
||||
if usage.DirectionSense == "NEGATIVE":
|
||||
total_thickness *= -1
|
||||
offset *= -1
|
||||
min_layers = [l * -1 for l in min_layers]
|
||||
max_layers = [l * -1 for l in max_layers]
|
||||
return {
|
||||
"offset": offset,
|
||||
"min_layers": min_layers,
|
||||
"max_layers": max_layers,
|
||||
"thickness": total_thickness * self.unit_scale,
|
||||
}
|
||||
|
||||
def get_segments(self, connections, centerline, start_point, end_point):
|
||||
segments = []
|
||||
total_connections = len(connections)
|
||||
if total_connections:
|
||||
for i in range(0, total_connections + 1):
|
||||
if i == 0:
|
||||
connection = connections[i]
|
||||
segments.append([centerline[0], connection["intersection"], connection["out_point"]])
|
||||
elif i == total_connections:
|
||||
segments.append([segments[-1][-1], segments[-1][-2], centerline[1]])
|
||||
for face in triangulate["faces"]:
|
||||
tri = []
|
||||
for vert in face.verts:
|
||||
if index := vert_map.get(vert.index, None):
|
||||
tri.append(index)
|
||||
else:
|
||||
connection = connections[i]
|
||||
segments.append(
|
||||
[segments[-1][-1], segments[-1][-2], connection["intersection"], connection["out_point"]]
|
||||
)
|
||||
else:
|
||||
segments.append([centerline[0], centerline[1]])
|
||||
if start_point:
|
||||
segments[0].insert(0, start_point)
|
||||
if end_point:
|
||||
segments[-1].append(end_point)
|
||||
return segments
|
||||
|
||||
def get_connections(self, wall, obj, centerline, min_edge, max_edge):
|
||||
connections = {"ATEND": None, "ATSTART": None, "ATPATH": [], "MINPATH": [], "MAXPATH": []}
|
||||
for rel in wall.ConnectedTo:
|
||||
# How do you join to a non layered element? Not sure.
|
||||
if tool.Model.get_usage_type(rel.RelatedElement) != "LAYER2":
|
||||
continue
|
||||
if rel.RelatingConnectionType == "ATPATH":
|
||||
metadata = self.get_connection_metadata(obj, rel.RelatedElement, centerline, min_edge, max_edge)
|
||||
if not metadata:
|
||||
continue
|
||||
connections["ATPATH"].append(metadata)
|
||||
else:
|
||||
metadata = self.get_connection_metadata(obj, rel.RelatedElement, centerline, min_edge, max_edge)
|
||||
if not metadata:
|
||||
continue
|
||||
connections[rel.RelatingConnectionType] = metadata
|
||||
for rel in wall.ConnectedFrom:
|
||||
if tool.Model.get_usage_type(rel.RelatingElement) != "LAYER2":
|
||||
continue
|
||||
# We only consider ATPATH since in this situation, we have the
|
||||
# priority. The non-priority wall never has any layers that need to
|
||||
# "turn a corner".
|
||||
if rel.RelatedConnectionType == "ATPATH":
|
||||
metadata = self.get_connection_metadata(obj, rel.RelatingElement, centerline, min_edge, max_edge)
|
||||
if not metadata:
|
||||
continue
|
||||
connections["ATPATH"].append(metadata)
|
||||
connections["ATPATH"] = sorted(connections["ATPATH"], key=lambda c: c["intersection"].x)
|
||||
for connection in connections["ATPATH"]:
|
||||
if connection["angle"] > 0:
|
||||
connections["MINPATH"].append(connection)
|
||||
else:
|
||||
connections["MAXPATH"].append(connection)
|
||||
return connections
|
||||
|
||||
def get_connection_metadata(self, obj, rel_element, centerline, min_edge, max_edge):
|
||||
rel_obj = tool.Ifc.get_object(rel_element)
|
||||
layers = self.get_layer_data(rel_element)
|
||||
if not layers:
|
||||
return
|
||||
minx = min([co[0] for co in rel_obj.bound_box])
|
||||
maxx = max([co[0] for co in rel_obj.bound_box])
|
||||
rel_centerline = [
|
||||
Vector((minx, layers["offset"] + layers["thickness"] / 2)),
|
||||
Vector((maxx, layers["offset"] + layers["thickness"] / 2)),
|
||||
]
|
||||
rel_centerline = [obj.matrix_world.inverted() @ rel_obj.matrix_world @ v.to_3d() for v in rel_centerline]
|
||||
rel_centerline = [v.to_2d() for v in rel_centerline]
|
||||
intersection = tool.Cad.intersect_edges(centerline, rel_centerline)
|
||||
if intersection:
|
||||
intersection, _ = intersection
|
||||
else:
|
||||
return
|
||||
closest_centerline_point = tool.Cad.closest_vector(intersection, tuple(rel_centerline))
|
||||
if closest_centerline_point == rel_centerline[1]:
|
||||
rel_centerline = [rel_centerline[1], rel_centerline[0]]
|
||||
angle = tool.Cad.angle_edges(centerline, rel_centerline, signed=True)
|
||||
# A little extreme, but maybe it's OK?
|
||||
out_point = tool.Cad.furthest_vector(intersection, tuple(rel_centerline))
|
||||
return {
|
||||
"element": rel_element,
|
||||
"obj": rel_obj,
|
||||
"centerline": rel_centerline,
|
||||
"intersection": intersection,
|
||||
"angle": angle,
|
||||
"out_point": out_point,
|
||||
}
|
||||
verts.append(tuple((obj.matrix_world @ vert.co) + global_offset))
|
||||
vert_map[vert.index] = i
|
||||
tri.append(i)
|
||||
i += 1
|
||||
tris.append(tri)
|
||||
return verts, tris
|
||||
|
||||
|
||||
class DecorationsHandler:
|
||||
@@ -2022,7 +1927,8 @@ class DecorationsHandler:
|
||||
for object_type in ("SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"):
|
||||
self.decorators[object_type] = self.decorators["FALL"]
|
||||
self.decorators["MULTI_SYMBOL"] = self.decorators["SYMBOL"]
|
||||
if drawing_font := bpy.context.scene.DocProperties.drawing_font:
|
||||
props = tool.Drawing.get_document_props()
|
||||
if drawing_font := props.drawing_font:
|
||||
drawing_font_path = tool.Blender.get_data_dir_path(Path("fonts") / drawing_font)
|
||||
if drawing_font_path.is_file():
|
||||
font_id = blf.load(drawing_font_path.__str__())
|
||||
@@ -2045,7 +1951,7 @@ class DecorationsHandler:
|
||||
if not element.is_a("IfcAnnotation"):
|
||||
continue
|
||||
|
||||
object_type: Union[str, None] = element.ObjectType
|
||||
object_type: Union[str, None] = ifcopenshell.util.element.get_predefined_type(element)
|
||||
if object_type == "DRAWING":
|
||||
continue
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import bpy
|
||||
import blf
|
||||
import gpu
|
||||
import bonsai.tool as tool
|
||||
from bpy import types
|
||||
from mathutils import Vector
|
||||
from mathutils import geometry
|
||||
@@ -413,6 +414,8 @@ class ExtrusionGuidesGizmo(CustomGizmo, types.Gizmo):
|
||||
shader_wrapper = ExtrusionGuidesShader()
|
||||
verts = [Vector((0, 0, 0)), Vector((0, 0, 1))]
|
||||
verts, edges = shader_wrapper.process_geometry(verts)
|
||||
if not tool.Blender.validate_shader_batch_data(verts, edges):
|
||||
verts, edges = [], []
|
||||
self.custom_shape = shader_wrapper, shader_wrapper.batch(
|
||||
pos=verts,
|
||||
indices=edges,
|
||||
@@ -478,21 +481,25 @@ class ExtrusionWidget(types.GizmoGroup):
|
||||
bl_options = {"3D", "PERSISTENT", "SHOW_MODAL_ALL"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, ctx):
|
||||
obj = ctx.object
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
return (
|
||||
obj
|
||||
and obj.type == "MESH"
|
||||
and obj.data.BIMMeshProperties.ifc_parameters.get("IfcExtrudedAreaSolid/Depth") is not None
|
||||
and (data := obj.data)
|
||||
and isinstance(data, bpy.types.Mesh)
|
||||
and tool.Geometry.get_mesh_props(data).ifc_parameters.get("IfcExtrudedAreaSolid/Depth") is not None
|
||||
)
|
||||
|
||||
def setup(self, ctx):
|
||||
target = ctx.object
|
||||
prop = target.data.BIMMeshProperties.ifc_parameters.get("IfcExtrudedAreaSolid/Depth")
|
||||
def setup(self, context: bpy.types.Context) -> None:
|
||||
target = context.object
|
||||
assert target
|
||||
mesh = target.data
|
||||
assert isinstance(mesh, bpy.types.Mesh)
|
||||
prop = tool.Geometry.get_mesh_props(mesh).ifc_parameters.get("IfcExtrudedAreaSolid/Depth")
|
||||
|
||||
basis = target.matrix_world.normalized()
|
||||
theme = ctx.preferences.themes[0].user_interface
|
||||
scale_value = self.get_scale_value(ctx.scene.unit_settings.system, ctx.scene.unit_settings.length_unit)
|
||||
theme = context.preferences.themes[0].user_interface
|
||||
scale_value = self.get_scale_value(context.scene.unit_settings.system, context.scene.unit_settings.length_unit)
|
||||
|
||||
# setup handle
|
||||
gz = self.handle = self.gizmos.new("BIM_GT_uglydot_3d")
|
||||
@@ -521,23 +528,26 @@ class ExtrusionWidget(types.GizmoGroup):
|
||||
# gz.use_draw_modal = True
|
||||
# gz.target_set_prop('value', target.demo, 'depth')
|
||||
|
||||
def refresh(self, ctx):
|
||||
def refresh(self, context: bpy.types.Context) -> None:
|
||||
"""updating gizmos"""
|
||||
target = ctx.object
|
||||
target = context.active_object
|
||||
basis = target.matrix_world.normalized()
|
||||
self.handle.matrix_basis = basis
|
||||
self.guides.matrix_basis = basis
|
||||
|
||||
def update(self, ctx):
|
||||
def update(self, context: bpy.types.Context) -> None:
|
||||
"""updating object"""
|
||||
bpy.ops.bim.update_parametric_representation()
|
||||
target = ctx.object
|
||||
prop = target.data.BIMMeshProperties.ifc_parameters.get("IfcExtrudedAreaSolid/Depth")
|
||||
target = context.active_object
|
||||
assert target
|
||||
mesh = target.data
|
||||
assert isinstance(mesh, bpy.types.Mesh)
|
||||
prop = tool.Geometry.get_mesh_props(mesh).ifc_parameters.get("IfcExtrudedAreaSolid/Depth")
|
||||
self.handle.target_set_prop("offset", prop, "value")
|
||||
self.guides.target_set_prop("depth", prop, "value")
|
||||
|
||||
@staticmethod
|
||||
def get_scale_value(system, length_unit):
|
||||
def get_scale_value(system: str, length_unit: str) -> float:
|
||||
scale_value = 1
|
||||
if system == "METRIC":
|
||||
if length_unit == "KILOMETERS":
|
||||
|
||||
@@ -24,7 +24,8 @@ from bpy.app.handlers import persistent
|
||||
|
||||
@persistent
|
||||
def load_post(*args):
|
||||
if bpy.context.scene.DocProperties.should_draw_decorations:
|
||||
props = tool.Drawing.get_document_props()
|
||||
if props.should_draw_decorations:
|
||||
decoration.DecorationsHandler.install(bpy.context)
|
||||
else:
|
||||
decoration.DecorationsHandler.uninstall()
|
||||
@@ -35,9 +36,11 @@ def depsgraph_update_pre_handler(scene):
|
||||
set_active_camera_resolution(scene)
|
||||
|
||||
|
||||
def set_active_camera_resolution(scene):
|
||||
if not scene.camera or "/" not in scene.camera.name or not scene.DocProperties.drawings:
|
||||
def set_active_camera_resolution(scene: bpy.types.Scene) -> None:
|
||||
props = tool.Drawing.get_document_props()
|
||||
if not scene.camera or "/" not in scene.camera.name or not props.drawings:
|
||||
return
|
||||
assert isinstance(scene.camera.data, bpy.types.Camera)
|
||||
props = scene.camera.data.BIMCameraProperties
|
||||
ortho_scale = max((props.width, props.height))
|
||||
aspect_ratio = props.width / props.height
|
||||
@@ -60,5 +63,3 @@ def set_active_camera_resolution(scene):
|
||||
|
||||
scene.render.resolution_x = scene.camera.data.BIMCameraProperties.raster_x = int(raster_x)
|
||||
scene.render.resolution_y = scene.camera.data.BIMCameraProperties.raster_y = int(raster_y)
|
||||
|
||||
current_drawing = scene.DocProperties.drawings[scene.DocProperties.current_drawing_index]
|
||||
|
||||
@@ -19,8 +19,11 @@
|
||||
import bpy
|
||||
import math
|
||||
import mathutils.geometry
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.unit
|
||||
import bonsai.tool as tool
|
||||
from mathutils import Vector
|
||||
from typing import Union
|
||||
|
||||
# Code taken and updated from https://blenderartists.org/t/detecting-intersection-of-bounding-boxes/457520/2
|
||||
|
||||
@@ -128,15 +131,53 @@ def format_distance(
|
||||
decimal_places=None,
|
||||
suppress_zero_inches=False,
|
||||
in_unit_length=False,
|
||||
custom_unit=None,
|
||||
):
|
||||
s_code = "\u00b2" # Superscript two THIS IS LEGACY (but being kept for when Area Measurements are re-implimented)
|
||||
|
||||
# Get Scene Unit Settings
|
||||
scaleFactor = bpy.context.scene.unit_settings.scale_length
|
||||
# Get Blender Scene Unit Settings
|
||||
unit_scale = bpy.context.scene.unit_settings.scale_length
|
||||
unit_system = bpy.context.scene.unit_settings.system
|
||||
unit_length = bpy.context.scene.unit_settings.length_unit
|
||||
area_unit_symbol = " m2" if unit_system == "METRIC" else " ft2"
|
||||
|
||||
value *= scaleFactor
|
||||
# Get IFC Unit Settings
|
||||
if tool.Ifc.get():
|
||||
unit_scale = 1
|
||||
if length_unit := ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "LENGTHUNIT"):
|
||||
unit_system = "METRIC" if length_unit.Name == "METRE" else "IMPERIAL"
|
||||
unit_length = length_unit.Name.upper()
|
||||
if hasattr(length_unit, "Prefix") and length_unit.Prefix:
|
||||
unit_length = length_unit.Prefix + length_unit.Name
|
||||
unit_length_mapping = {
|
||||
"FOOT": "FEET",
|
||||
"INCH": "INCHES",
|
||||
"METRE": "METERS",
|
||||
"DECIMETRE": "DECIMETERS",
|
||||
"CENTIMETRE": "CENTIMETERS",
|
||||
"MILLIMETRE": "MILLIMETERS",
|
||||
}
|
||||
unit_length = unit_length_mapping[unit_length]
|
||||
# For now we only format area in IFC Units
|
||||
if area_unit := ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "AREAUNIT"):
|
||||
area_unit_symbol = " " + ifcopenshell.util.unit.get_unit_symbol(area_unit)
|
||||
|
||||
unit_fraction = True if unit_system == "IMPERIAL" else False
|
||||
|
||||
# Custom Unit Settings
|
||||
if custom_unit:
|
||||
unit_mapping = {
|
||||
"Feet and Inches - Fractional": ("IMPERIAL", "FEET", True),
|
||||
"Feet - Decimal": ("IMPERIAL", "FEET", False),
|
||||
"Inches - Fractional": ("IMPERIAL", "INCHES", True),
|
||||
"Inches - Decimal": ("IMPERIAL", "INCHES", False),
|
||||
"Meters": ("METRIC", "METERS", False),
|
||||
"Decimeters": ("METRIC", "DECIMETERS", False),
|
||||
"Centimeters": ("METRIC", "CENTIMETERS", False),
|
||||
"Millimeters": ("METRIC", "MILLIMETERS", False),
|
||||
}
|
||||
if custom_unit in unit_mapping:
|
||||
unit_system, unit_length, unit_fraction = unit_mapping[custom_unit]
|
||||
|
||||
value *= unit_scale
|
||||
|
||||
# Imperial Formatting
|
||||
if unit_system == "IMPERIAL":
|
||||
@@ -161,10 +202,14 @@ def format_distance(
|
||||
|
||||
base = int(precision)
|
||||
decInches = value * toInches
|
||||
decFeet = decInches / 12
|
||||
|
||||
# Separate ft and inches
|
||||
# Unless Inches are the specified Length Unit
|
||||
if unit_length != "INCHES":
|
||||
# Unless Inches are the specified Length Unit or unit_fraction is False
|
||||
if unit_length == "FEET" and not unit_fraction:
|
||||
feet = round(decInches / inPerFoot, 3) # keep decimal
|
||||
decInches = 0
|
||||
elif unit_length != "INCHES":
|
||||
feet = int(decInches / inPerFoot) # remove decimal
|
||||
decInches -= feet * inPerFoot
|
||||
else:
|
||||
@@ -199,6 +244,10 @@ def format_distance(
|
||||
feet += 1
|
||||
inches = 0
|
||||
|
||||
# Check whether decimal or fractional
|
||||
if not unit_fraction:
|
||||
inches = round(decInches, 3)
|
||||
frac = None
|
||||
if not isArea:
|
||||
add_inches = bool(inches) or not suppress_zero_inches or (inches == 0 and frac)
|
||||
tx_dist = ""
|
||||
@@ -224,12 +273,33 @@ def format_distance(
|
||||
tx_dist += str(frac) + "/" + str(base)
|
||||
if add_inches or frac:
|
||||
tx_dist += '"'
|
||||
if precision == "12" and unit_system == "IMPERIAL":
|
||||
tx_dist = str(round(decFeet)) + "'"
|
||||
else:
|
||||
tx_dist = str("%1.3f" % (value * toInches / inPerFoot)) + " sq. ft."
|
||||
fmt = "%1.3f"
|
||||
sq_feet = round(value * toInches / inPerFoot, 4)
|
||||
tx_dist = ""
|
||||
if area_unit_symbol == " ft2":
|
||||
fmt += area_unit_symbol
|
||||
tx_dist = fmt % sq_feet
|
||||
if area_unit_symbol == " in2":
|
||||
sq_inch = sq_feet * 144
|
||||
fmt += area_unit_symbol
|
||||
tx_dist = fmt % sq_inch
|
||||
if area_unit_symbol == " yd2":
|
||||
sq_yard = sq_feet / 9
|
||||
fmt += area_unit_symbol
|
||||
tx_dist = fmt % sq_yard
|
||||
if area_unit_symbol == " mi2":
|
||||
sq_mile = sq_feet / 27878400
|
||||
fmt += area_unit_symbol
|
||||
tx_dist = fmt % sq_mile
|
||||
|
||||
# METRIC FORMATTING
|
||||
elif unit_system == "METRIC":
|
||||
if in_unit_length:
|
||||
if unit_length == "DECIMETERS":
|
||||
value = value / 10
|
||||
if unit_length == "CENTIMETERS":
|
||||
value = value / 100
|
||||
if unit_length == "MILLIMETERS":
|
||||
@@ -248,6 +318,14 @@ def format_distance(
|
||||
if hide_units is False:
|
||||
fmt += " m"
|
||||
tx_dist = fmt % value
|
||||
# Decimeters
|
||||
elif unit_length == "DECIMETERS":
|
||||
if decimal_places is None:
|
||||
fmt = "%1.1f"
|
||||
if hide_units is False:
|
||||
fmt += " dm"
|
||||
d_dm = value * (10)
|
||||
tx_dist = fmt % d_dm
|
||||
# Centimeters
|
||||
elif unit_length == "CENTIMETERS":
|
||||
if decimal_places is None:
|
||||
@@ -287,19 +365,41 @@ def format_distance(
|
||||
d_mm = value * (1000)
|
||||
tx_dist = fmt % d_mm
|
||||
if isArea:
|
||||
tx_dist += s_code
|
||||
if area_unit_symbol == " m2":
|
||||
if decimal_places is None:
|
||||
fmt = "%1.3f"
|
||||
if hide_units is False:
|
||||
fmt += area_unit_symbol
|
||||
tx_dist = fmt % value
|
||||
if area_unit_symbol == " cm2":
|
||||
if decimal_places is None:
|
||||
fmt = "%1.1f"
|
||||
if hide_units is False:
|
||||
fmt += area_unit_symbol
|
||||
d_cm = value * (10000)
|
||||
tx_dist = fmt % d_cm
|
||||
if area_unit_symbol == " mm2":
|
||||
if decimal_places is None:
|
||||
fmt = "%1.0f"
|
||||
if hide_units is False:
|
||||
fmt += area_unit_symbol
|
||||
d_cm = value * (1000000)
|
||||
tx_dist = fmt % d_cm
|
||||
|
||||
else:
|
||||
tx_dist = fmt % value
|
||||
|
||||
return tx_dist
|
||||
|
||||
|
||||
def get_active_drawing(scene):
|
||||
def get_active_drawing(
|
||||
scene: bpy.types.Scene,
|
||||
) -> Union[tuple[bpy.types.Collection, bpy.types.Camera], tuple[None, None]]:
|
||||
"""Get active drawing collection and camera"""
|
||||
props = scene.DocProperties
|
||||
props = tool.Drawing.get_document_props()
|
||||
try:
|
||||
camera = tool.Ifc.get_object(tool.Ifc.get().by_id(props.active_drawing_id))
|
||||
return camera.BIMObjectProperties.collection, camera
|
||||
return tool.Blender.get_object_bim_props(camera).collection, camera
|
||||
except:
|
||||
return None, None
|
||||
|
||||
@@ -416,7 +516,7 @@ def elevate_segment(bounds, segm):
|
||||
return [Vector((x, ymin, zmin)), Vector((x, ymax, zmin))]
|
||||
|
||||
|
||||
def add_newline_between_words(text, newline_at):
|
||||
def add_newline_between_words(text: str, newline_at: int) -> str:
|
||||
result = []
|
||||
start = 0
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -44,7 +44,7 @@ from bpy.props import (
|
||||
CollectionProperty,
|
||||
BoolVectorProperty,
|
||||
)
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
from typing import TYPE_CHECKING, Literal, Any
|
||||
|
||||
|
||||
diagram_scales_enum = []
|
||||
@@ -76,7 +76,7 @@ def update_diagram_scale(self, context):
|
||||
try:
|
||||
element = (
|
||||
tool.Ifc.get()
|
||||
.by_id(self.id_data.BIMMeshProperties.ifc_definition_id)
|
||||
.by_id(tool.Geometry.get_mesh_props(self.id_data).ifc_definition_id)
|
||||
.OfProductRepresentation[0]
|
||||
.ShapeOfProduct[0]
|
||||
)
|
||||
@@ -93,7 +93,7 @@ def update_diagram_scale(self, context):
|
||||
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties=diagram_scale)
|
||||
|
||||
|
||||
def update_is_nts(self, context):
|
||||
def update_is_nts(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
|
||||
if not self.update_props:
|
||||
return
|
||||
if not context.scene.camera or context.scene.camera.data != self.id_data:
|
||||
@@ -104,7 +104,7 @@ def update_is_nts(self, context):
|
||||
try:
|
||||
element = (
|
||||
tool.Ifc.get()
|
||||
.by_id(self.id_data.BIMMeshProperties.ifc_definition_id)
|
||||
.by_id(tool.Geometry.get_mesh_props(self.id_data).ifc_definition_id)
|
||||
.OfProductRepresentation[0]
|
||||
.ShapeOfProduct[0]
|
||||
)
|
||||
@@ -192,8 +192,8 @@ def get_drawing_style_name(self: "DrawingStyle"):
|
||||
|
||||
def set_drawing_style_name(self: "DrawingStyle", new_value: str) -> None:
|
||||
"""ensure the name is unique"""
|
||||
scene = bpy.context.scene
|
||||
drawing_styles = [s.name for s in scene.DocProperties.drawing_styles if s.name != self.name]
|
||||
props = tool.Drawing.get_document_props()
|
||||
drawing_styles = [s.name for s in props.drawing_styles if s.name != self.name]
|
||||
new_value = tool.Blender.ensure_unique_name(new_value, drawing_styles)
|
||||
old_value = self.name
|
||||
self["name"] = new_value
|
||||
@@ -266,7 +266,7 @@ def update_titleblocks(self, context):
|
||||
def update_should_draw_decorations(self, context: bpy.types.Context) -> None:
|
||||
if self.should_draw_decorations:
|
||||
# TODO: design a proper text variable templating renderer
|
||||
collection = context.scene.camera.BIMObjectProperties.collection
|
||||
collection = tool.Blender.get_object_bim_props(context.scene.camera).collection
|
||||
for obj in collection.objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]):
|
||||
@@ -613,7 +613,13 @@ class BIMTextProperties(PropertyGroup):
|
||||
)
|
||||
newline_at: IntProperty(name="Newline At")
|
||||
|
||||
def get_text_edited_data(self):
|
||||
if TYPE_CHECKING:
|
||||
is_editing: bool
|
||||
literals: bpy.types.bpy_prop_collection_idprop[LiteralProps]
|
||||
font_size: str
|
||||
newline_at: int
|
||||
|
||||
def get_text_edited_data(self) -> dict[str, Any]:
|
||||
"""should be called only if `is_editing`
|
||||
otherwise should use `DecoratorData.get_ifc_text_data(obj)` instead
|
||||
because this data could be out of date
|
||||
@@ -676,3 +682,10 @@ class BIMAnnotationProperties(PropertyGroup):
|
||||
)
|
||||
is_adding_type: bpy.props.BoolProperty(default=False)
|
||||
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
object_type: str
|
||||
relating_type_id: str
|
||||
create_representation_for_type: bool
|
||||
is_adding_type: bool
|
||||
type_name: str
|
||||
|
||||
@@ -56,7 +56,7 @@ def a1_to_rc(cell):
|
||||
|
||||
|
||||
class Scheduler:
|
||||
def schedule(self, infile, outfile):
|
||||
def schedule(self, infile: str, outfile: str) -> None:
|
||||
self.svg = svgwrite.Drawing(
|
||||
outfile,
|
||||
debug=False,
|
||||
@@ -71,11 +71,12 @@ class Scheduler:
|
||||
elif infile.endswith("xlsx"):
|
||||
self.schedule_xlsx(infile, outfile)
|
||||
|
||||
def parse_css(self, infile):
|
||||
def parse_css(self, infile: str) -> None:
|
||||
props = tool.Drawing.get_document_props()
|
||||
stylesheet_path = os.path.splitext(infile)[0] + ".css"
|
||||
if not os.path.exists(stylesheet_path):
|
||||
stylesheet_rel_path = getattr(bpy.context.scene.DocProperties, "schedules_stylesheet_path")
|
||||
ifc_file_path = os.path.dirname(IfcStore.path)
|
||||
stylesheet_rel_path = props.schedules_stylesheet_path
|
||||
ifc_file_path = os.path.dirname(tool.Ifc.get_path())
|
||||
stylesheet_path = ifc_file_path + "\\" + stylesheet_rel_path
|
||||
if not os.path.exists(stylesheet_path):
|
||||
stylesheet_path = tool.Blender.get_data_dir_path(Path("assets") / "schedule.css")
|
||||
@@ -91,7 +92,7 @@ class Scheduler:
|
||||
|
||||
self.svg.defs.add(self.svg.style(css))
|
||||
|
||||
def schedule_xlsx(self, infile, outfile):
|
||||
def schedule_xlsx(self, infile: str, outfile: str) -> None:
|
||||
workbook = openpyxl.open(infile, data_only=True)
|
||||
sheet = workbook.active
|
||||
|
||||
@@ -236,7 +237,7 @@ class Scheduler:
|
||||
self.svg["viewBox"] = "0 0 {} {}".format(total_width, total_height)
|
||||
self.svg.save(pretty=True)
|
||||
|
||||
def schedule_ods(self, infile, outfile):
|
||||
def schedule_ods(self, infile: str, outfile: str) -> None:
|
||||
doc = load_ods(infile)
|
||||
|
||||
# useful for debugging ods
|
||||
@@ -495,11 +496,11 @@ class Scheduler:
|
||||
self.svg["viewBox"] = "0 0 {} {}".format(total_width, total_height)
|
||||
self.svg.save(pretty=True)
|
||||
|
||||
def get_style(self, style_name, styles):
|
||||
def get_style(self, style_name: str, styles: dict) -> dict:
|
||||
style = styles[style_name] if style_name else {}
|
||||
return style
|
||||
|
||||
def get_box_alignment(self, style):
|
||||
def get_box_alignment(self, style: dict) -> str:
|
||||
if style and "vertical-align" in style and style["vertical-align"] != "automatic":
|
||||
vertical_align = style["vertical-align"]
|
||||
else:
|
||||
|
||||
@@ -32,6 +32,7 @@ from mathutils import Vector
|
||||
import re
|
||||
|
||||
VIEW_TITLE_OFFSET_Y = 5
|
||||
DRAWING_PADDING = 10
|
||||
DEFAULT_POSITION = Vector((30, 30))
|
||||
SVG = "{http://www.w3.org/2000/svg}"
|
||||
XLINK = "{http://www.w3.org/1999/xlink}"
|
||||
@@ -113,13 +114,15 @@ class SheetBuilder:
|
||||
view_width = self.convert_to_mm(view_root.attrib.get("width"))
|
||||
view_height = self.convert_to_mm(view_root.attrib.get("height"))
|
||||
|
||||
x, y = self.next_drawing_location(layout_root, view_width)
|
||||
|
||||
# add background
|
||||
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, layout_dir)
|
||||
background.attrib["x"] = str(DEFAULT_POSITION.x)
|
||||
background.attrib["y"] = str(DEFAULT_POSITION.y)
|
||||
background.attrib["x"] = str(x)
|
||||
background.attrib["y"] = str(y)
|
||||
background.attrib["width"] = str(view_width)
|
||||
background.attrib["height"] = str(view_height)
|
||||
|
||||
@@ -128,16 +131,49 @@ class SheetBuilder:
|
||||
foreground = ET.SubElement(view, "image")
|
||||
foreground.attrib["data-type"] = "foreground"
|
||||
foreground.attrib["xlink:href"] = os.path.relpath(drawing_path, layout_dir)
|
||||
foreground.attrib["x"] = str(DEFAULT_POSITION.x)
|
||||
foreground.attrib["y"] = str(DEFAULT_POSITION.y)
|
||||
foreground.attrib["x"] = str(x)
|
||||
foreground.attrib["y"] = str(y)
|
||||
foreground.attrib["width"] = str(view_width)
|
||||
foreground.attrib["height"] = str(view_height)
|
||||
|
||||
self.add_view_title(
|
||||
DEFAULT_POSITION.x, view_height + DEFAULT_POSITION.y + VIEW_TITLE_OFFSET_Y, view, layout_dir
|
||||
)
|
||||
self.add_view_title(x, view_height + y + VIEW_TITLE_OFFSET_Y, view, layout_dir)
|
||||
layout_tree.write(layout_path)
|
||||
|
||||
def next_drawing_location(self, layout_root: ET.Element, next_width: float) -> list:
|
||||
titleblocks = layout_root.findall(f'{SVG}g[@data-type="titleblock"]')
|
||||
drawings = layout_root.findall(f'{SVG}g[@data-type="drawing"]')
|
||||
|
||||
# how wide is the title block frame
|
||||
try:
|
||||
titleblock_width = self.convert_to_mm(titleblocks[0][0].attrib.get("width"))
|
||||
except (IndexError, AttributeError):
|
||||
titleblock_width = 840.0
|
||||
|
||||
# where does the last drawing finish
|
||||
try:
|
||||
last = drawings[-1][0]
|
||||
last_width = self.convert_to_mm(last.attrib.get("width"))
|
||||
last_x = self.convert_to_mm(last.attrib.get("x"))
|
||||
last_y = self.convert_to_mm(last.attrib.get("y"))
|
||||
except (IndexError, AttributeError):
|
||||
return [DEFAULT_POSITION.x, DEFAULT_POSITION.y]
|
||||
|
||||
# check if the new drawing fits in the current row
|
||||
if last_x + last_width + DRAWING_PADDING + next_width + DEFAULT_POSITION.x < titleblock_width:
|
||||
return [last_x + last_width + DRAWING_PADDING, last_y]
|
||||
|
||||
# start a new row, find the y
|
||||
for drawing in drawings:
|
||||
for image in drawing:
|
||||
try:
|
||||
image_y = self.convert_to_mm(image.attrib.get("y"))
|
||||
image_height = self.convert_to_mm(image.attrib.get("height"))
|
||||
except AttributeError:
|
||||
return [DEFAULT_POSITION.x, DEFAULT_POSITION.y]
|
||||
if image_y + image_height + DRAWING_PADDING > last_y:
|
||||
last_y = image_y + image_height + DRAWING_PADDING
|
||||
return [DEFAULT_POSITION.x, last_y]
|
||||
|
||||
def update_sheet_drawing_sizes(self, sheet: ifcopenshell.entity_instance) -> None:
|
||||
ET.register_namespace("", "http://www.w3.org/2000/svg")
|
||||
|
||||
@@ -221,6 +257,8 @@ class SheetBuilder:
|
||||
view_width = self.convert_to_mm(view_root.attrib.get("width"))
|
||||
view_height = self.convert_to_mm(view_root.attrib.get("height"))
|
||||
|
||||
x, y = self.next_drawing_location(layout_root, view_width)
|
||||
|
||||
view = ET.SubElement(layout_root, "g")
|
||||
view.attrib["data-id"] = str(reference.id())
|
||||
view.attrib["data-type"] = document.Scope.lower()
|
||||
@@ -229,14 +267,12 @@ class SheetBuilder:
|
||||
foreground = ET.SubElement(view, "image")
|
||||
foreground.attrib["data-type"] = "content"
|
||||
foreground.attrib["xlink:href"] = os.path.relpath(view_path, layout_dir)
|
||||
foreground.attrib["x"] = str(DEFAULT_POSITION.x)
|
||||
foreground.attrib["y"] = str(DEFAULT_POSITION.y)
|
||||
foreground.attrib["x"] = str(x)
|
||||
foreground.attrib["y"] = str(y)
|
||||
foreground.attrib["width"] = str(view_width)
|
||||
foreground.attrib["height"] = str(view_height)
|
||||
|
||||
self.add_view_title(
|
||||
DEFAULT_POSITION.x, view_height + DEFAULT_POSITION.y + VIEW_TITLE_OFFSET_Y, view, layout_dir
|
||||
)
|
||||
self.add_view_title(x, view_height + y + VIEW_TITLE_OFFSET_Y, view, layout_dir)
|
||||
layout_tree.write(layout_path)
|
||||
|
||||
def add_view_title(self, x: float, y: float, parent: ET.Element, layout_dir: str) -> None:
|
||||
|
||||
@@ -181,35 +181,35 @@ class SvgWriter:
|
||||
self.decimal_places = decimal_places
|
||||
for element in annotations:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if not obj or element.ObjectType == "DRAWING":
|
||||
if not obj or (object_type := ifcopenshell.util.element.get_predefined_type(element)) == "DRAWING":
|
||||
continue
|
||||
elif element.ObjectType == "GRID":
|
||||
elif object_type == "GRID":
|
||||
self.draw_grid_annotation(obj)
|
||||
elif element.ObjectType == "TEXT_LEADER":
|
||||
elif object_type == "TEXT_LEADER":
|
||||
self.draw_leader_annotation(obj)
|
||||
elif element.ObjectType == "STAIR_ARROW":
|
||||
elif object_type == "STAIR_ARROW":
|
||||
self.draw_stair_annotation(obj)
|
||||
elif element.ObjectType == "DIMENSION":
|
||||
elif object_type == "DIMENSION":
|
||||
self.draw_dimension_annotations(obj)
|
||||
elif element.ObjectType == "ANGLE":
|
||||
elif object_type == "ANGLE":
|
||||
self.draw_angle_annotations(obj)
|
||||
elif element.ObjectType == "RADIUS":
|
||||
elif object_type == "RADIUS":
|
||||
self.draw_radius_annotations(obj)
|
||||
elif element.ObjectType == "DIAMETER":
|
||||
elif object_type == "DIAMETER":
|
||||
self.draw_diameter_annotations(obj)
|
||||
elif element.ObjectType == "ELEVATION":
|
||||
elif object_type == "ELEVATION":
|
||||
self.draw_elevation_annotation(obj)
|
||||
elif element.ObjectType == "SECTION":
|
||||
elif object_type == "SECTION":
|
||||
self.draw_section_annotation(obj)
|
||||
elif element.ObjectType == "BREAKLINE":
|
||||
elif object_type == "BREAKLINE":
|
||||
self.draw_break_annotations(obj)
|
||||
elif element.ObjectType == "PLAN_LEVEL":
|
||||
elif object_type == "PLAN_LEVEL":
|
||||
self.draw_plan_level_annotation(obj)
|
||||
elif element.ObjectType == "SECTION_LEVEL":
|
||||
elif object_type == "SECTION_LEVEL":
|
||||
self.draw_section_level_annotation(obj)
|
||||
elif element.ObjectType == "TEXT":
|
||||
elif object_type == "TEXT":
|
||||
self.draw_text_annotation(obj, obj.location)
|
||||
elif element.ObjectType in ("FALL", "SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"):
|
||||
elif object_type in ("FALL", "SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"):
|
||||
self.draw_fall_annotations(obj)
|
||||
else:
|
||||
self.draw_misc_annotation(obj)
|
||||
@@ -1128,7 +1128,12 @@ class SvgWriter:
|
||||
|
||||
def get_text():
|
||||
radius = (points[-1].co - points[-2].co).length
|
||||
radius = helper.format_distance(radius, precision=self.precision, decimal_places=self.decimal_places)
|
||||
radius = helper.format_distance(
|
||||
radius,
|
||||
precision=self.precision,
|
||||
decimal_places=self.decimal_places,
|
||||
custom_unit=dimension_data["custom_unit"],
|
||||
)
|
||||
text = f"R{radius}"
|
||||
return text
|
||||
|
||||
@@ -1207,13 +1212,14 @@ class SvgWriter:
|
||||
angle = 90
|
||||
|
||||
# ues SLOPE_ANGLE as default
|
||||
if element.ObjectType in ("FALL", "SLOPE_ANGLE"):
|
||||
object_type = ifcopenshell.util.element.get_predefined_type(element)
|
||||
if object_type in ("FALL", "SLOPE_ANGLE"):
|
||||
return f"{angle}°"
|
||||
elif element.ObjectType == "SLOPE_FRACTION":
|
||||
elif object_type == "SLOPE_FRACTION":
|
||||
if angle == 90:
|
||||
return "-"
|
||||
return f"{helper.format_distance(rise, precision=self.precision, decimal_places=self.decimal_places)} / {helper.format_distance(run, precision=self.precision, decimal_places=self.decimal_places)}"
|
||||
elif element.ObjectType == "SLOPE_PERCENT":
|
||||
elif object_type == "SLOPE_PERCENT":
|
||||
if angle == 90:
|
||||
return "-"
|
||||
return f"{round(angle_tg * 100)} %"
|
||||
@@ -1255,6 +1261,7 @@ class SvgWriter:
|
||||
text_prefix=dimension_data["text_prefix"],
|
||||
text_suffix=dimension_data["text_suffix"],
|
||||
fill_bg=dimension_data["fill_bg"],
|
||||
custom_unit=dimension_data["custom_unit"],
|
||||
)
|
||||
|
||||
def draw_dimension_annotations(self, obj):
|
||||
@@ -1279,6 +1286,7 @@ class SvgWriter:
|
||||
text_prefix=dimension_data["text_prefix"],
|
||||
text_suffix=dimension_data["text_suffix"],
|
||||
fill_bg=dimension_data["fill_bg"],
|
||||
custom_unit=dimension_data["custom_unit"],
|
||||
)
|
||||
|
||||
def draw_measureit_arch_dimension_annotations(self):
|
||||
@@ -1305,6 +1313,7 @@ class SvgWriter:
|
||||
text_prefix="",
|
||||
text_suffix="",
|
||||
fill_bg=False,
|
||||
custom_unit=None,
|
||||
):
|
||||
offset = Vector([self.raw_width, self.raw_height]) / 2
|
||||
v0 = self.project_point_onto_camera(v0_global)
|
||||
@@ -1338,6 +1347,7 @@ class SvgWriter:
|
||||
precision=self.precision,
|
||||
decimal_places=self.decimal_places,
|
||||
suppress_zero_inches=suppress_zero_inches,
|
||||
custom_unit=custom_unit,
|
||||
)
|
||||
text = text_prefix + str(dimension) + text_suffix
|
||||
else:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user